diff options
Diffstat (limited to '')
131 files changed, 53618 insertions, 0 deletions
diff --git a/src/ui/tool-factory.cpp b/src/ui/tool-factory.cpp new file mode 100644 index 0000000..2b3b49b --- /dev/null +++ b/src/ui/tool-factory.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Factory for ToolBase tree + * + * Authors: + * Markus Engel + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tool-factory.h" + +#include "ui/tools/arc-tool.h" +#include "ui/tools/box3d-tool.h" +#include "ui/tools/calligraphic-tool.h" +#include "ui/tools/connector-tool.h" +#include "ui/tools/dropper-tool.h" +#include "ui/tools/eraser-tool.h" +#include "ui/tools/flood-tool.h" +#include "ui/tools/gradient-tool.h" +#include "ui/tools/lpe-tool.h" +#include "ui/tools/measure-tool.h" +#include "ui/tools/mesh-tool.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/pages-tool.h" +#include "ui/tools/pencil-tool.h" +#include "ui/tools/rect-tool.h" +#include "ui/tools/marker-tool.h" +#include "ui/tools/select-tool.h" +#include "ui/tools/spiral-tool.h" +#include "ui/tools/spray-tool.h" +#include "ui/tools/star-tool.h" +#include "ui/tools/text-tool.h" +#include "ui/tools/tweak-tool.h" +#include "ui/tools/zoom-tool.h" + +using namespace Inkscape::UI::Tools; + +ToolBase *ToolFactory::createObject(SPDesktop *desktop, std::string const &id) +{ + ToolBase *tool = nullptr; + + if (id == "/tools/shapes/arc") + tool = new ArcTool(desktop); + else if (id == "/tools/shapes/3dbox") + tool = new Box3dTool(desktop); + else if (id == "/tools/calligraphic") + tool = new CalligraphicTool(desktop); + else if (id == "/tools/connector") + tool = new ConnectorTool(desktop); + else if (id == "/tools/dropper") + tool = new DropperTool(desktop); + else if (id == "/tools/eraser") + tool = new EraserTool(desktop); + else if (id == "/tools/paintbucket") + tool = new FloodTool(desktop); + else if (id == "/tools/gradient") + tool = new GradientTool(desktop); + else if (id == "/tools/lpetool") + tool = new LpeTool(desktop); + else if (id == "/tools/marker") + tool = new MarkerTool(desktop); + else if (id == "/tools/measure") + tool = new MeasureTool(desktop); + else if (id == "/tools/mesh") + tool = new MeshTool(desktop); + else if (id == "/tools/nodes") + tool = new NodeTool(desktop); + else if (id == "/tools/pages") + tool = new PagesTool(desktop); + else if (id == "/tools/freehand/pencil") + tool = new PencilTool(desktop); + else if (id == "/tools/freehand/pen") + tool = new PenTool(desktop); + else if (id == "/tools/shapes/rect") + tool = new RectTool(desktop); + else if (id == "/tools/select") + tool = new SelectTool(desktop); + else if (id == "/tools/shapes/spiral") + tool = new SpiralTool(desktop); + else if (id == "/tools/spray") + tool = new SprayTool(desktop); + else if (id == "/tools/shapes/star") + tool = new StarTool(desktop); + else if (id == "/tools/text") + tool = new TextTool(desktop); + else if (id == "/tools/tweak") + tool = new TweakTool(desktop); + else if (id == "/tools/zoom") + tool = new ZoomTool(desktop); + else + fprintf(stderr, "WARNING: unknown tool: %s", id.c_str()); + + return tool; +} + +/* + 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-factory.h b/src/ui/tool-factory.h new file mode 100644 index 0000000..0addc0f --- /dev/null +++ b/src/ui/tool-factory.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Factory for ToolBase tree + * + * Authors: + * Markus Engel + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef TOOL_FACTORY_SEEN +#define TOOL_FACTORY_SEEN + +#include <string> + +class SPDesktop; +namespace Inkscape { +namespace UI { +namespace Tools { +class ToolBase; +} +} +} + +struct ToolFactory { + static Inkscape::UI::Tools::ToolBase *createObject(SPDesktop *desktop, std::string const &id); +}; + + +#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/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 <tweenk.pl@gmail.com> + * + * 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 <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <boost/none.hpp> +#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 <gdk/gdkkeysyms.h> + +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::iterator, bool> ControlPointSelection::insert(const value_type &x, bool notify, bool to_update) +{ + iterator found = _points.find(x); + if (found != _points.end()) { + return std::pair<iterator, bool>(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<key_type>(1, x), true); + } + + return std::pair<iterator, bool>(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<key_type>(1, k), false); + } + return 1; +} +void ControlPointSelection::erase(iterator first, iterator last) +{ + std::vector<SelectableControlPoint *> 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<SelectableControlPoint *> 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<SelectableControlPoint *> 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<SelectableControlPoint *> 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<SelectableControlPoint *> 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<value_type>(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<Geom::Dim2>((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<double, SelectableControlPoint*> 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<SelectableControlPoint*>(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<SelectableControlPoint*>(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<SelectableControlPoint*>(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<Inkscape::SnapCandidatePoint> &pts) +{ + pts.clear(); + for (auto _point : _points) { + pts.emplace_back(_original_positions[_point], SNAPSOURCE_NODE_HANDLE); + } +} + +void ControlPointSelection::getUnselectedPoints(std::vector<Inkscape::SnapCandidatePoint> &pts) +{ + pts.clear(); + ControlPointSelection::Set &nodes = this->allPoints(); + for (auto node : nodes) { + if (!node->selected()) { + Node *n = static_cast<Node*>(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 <tweenk.pl@gmail.com> + * + * 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 <list> +#include <memory> +#include <unordered_map> +#include <unordered_set> +#include <optional> +#include <cstddef> +#include <sigc++/sigc++.h> +#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<SelectableControlPoint *> 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<iterator, bool> insert(const value_type& x, bool notify = true, bool to_update = true); + template <class InputIterator> + void insert(InputIterator first, InputIterator last) { + for (; first != last; ++first) { + insert(*first, false, false); + } + _update(); + signal_selection_changed.emit(std::vector<key_type>(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<void> 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<void, std::vector<SelectableControlPoint *>, bool> signal_selection_changed; + sigc::signal<void, CommitEvent> signal_commit; + + void getOriginalPoints(std::vector<Inkscape::SnapCandidatePoint> &pts); + void getUnselectedPoints(std::vector<Inkscape::SnapCandidatePoint> &pts); + void setOriginalPoints(); + //the purpose of this list is to keep track of first and last selected + std::list<SelectableControlPoint *> _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<SelectableControlPoint *, Geom::Point> _original_positions; + std::unordered_map<SelectableControlPoint *, Geom::Affine> _last_trans; + std::optional<double> _rot_radius; + std::optional<double> _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 <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iostream> + +#include <gdk/gdkkeysyms.h> +#include <gdkmm.h> + +#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<void, ControlPoint*> 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<Gdk::Pixbuf> 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<Gdk::Pixbuf> 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<TransformHandle*>(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<TransformHandle*>(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 <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * 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 <gdkmm/pixbuf.h> +#include <boost/utility.hpp> +#include <cstddef> +#include <sigc++/signal.h> +#include <sigc++/trackable.h> +#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: + * - <tt>grabbed()</tt> + * - <tt>dragged()</tt> + * - <tt>dragged()</tt> + * - <tt>dragged()</tt> + * - ... + * - <tt>dragged()</tt> + * - <tt>ungrabbed()</tt> + * + * 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 <tt>signal_dragged</tt> 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 <tt>invisible_cset</tt> 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<void, ControlPoint*> 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<Gdk::Pixbuf> 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<Gdk::Pixbuf>); + + /** + * 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 <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tool/curve-drag-point.h" +#include <glib/gi18n.h> +#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<SPItem *>(_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", + "<b>Shift</b>: drag to open or move BSpline handles"); + } + if (state_held_shift(state)) { + return C_("Path segment tip", + "<b>Shift</b>: click to toggle segment selection"); + } + if (state_held_control(state) && state_held_alt(state)) { + return C_("Path segment tip", + "<b>Ctrl+Alt</b>: click to insert a node"); + } + if (state_held_control(state)) { + return C_("Path segment tip", + "<b>Ctrl</b>: click to change line type"); + } + if(_pm._isBSpline()){ + return C_("Path segment tip", + "<b>BSpline segment</b>: drag to shape the segment, doubleclick to insert node, " + "click to select (more: Shift, Ctrl+Alt)"); + } + if (linear) { + return C_("Path segment tip", + "<b>Linear segment</b>: drag to convert to a Bezier segment, " + "doubleclick to insert node, click to select (more: Shift, Ctrl+Alt)"); + } else { + return C_("Path segment tip", + "<b>Bezier segment</b>: 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 <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * 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 <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> +#include <gdkmm/display.h> +#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 <tweenk.pl@gmail.com> + * + * 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 <gdk/gdk.h> +#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 <unsigned button> +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 <typename E> +inline bool held_shift(E const &event) { + return state_held_shift(event.state); +} + +/** Checks whether Control was held when the event was generated. */ +template <typename E> +inline bool held_control(E const &event) { + return state_held_control(event.state); +} + +/** Checks whether Alt was held when the event was generated. */ +template <typename E> +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 <typename E> +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 <typename E> +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 <typename E> +inline bool held_only_alt(E const &event) { + return state_held_only_alt(event.state); +} + +template <typename E> +inline bool held_no_modifiers(E const &event) { + return state_held_no_modifiers(event.state); +} + +template <typename E> +inline bool held_any_modifiers(E const &event) { + return state_held_any_modifiers(event.state); +} + +template <typename E> +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 <unsigned button, typename E> +inline bool held_button(E const &event) { + return state_held_button<button>(event.state); +} + +guint shortcut_key(GdkEventKey const &event); +unsigned state_after_event(GdkEvent *event); + +} // 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/manipulator.h b/src/ui/tool/manipulator.h new file mode 100644 index 0000000..308ad1c --- /dev/null +++ b/src/ui/tool/manipulator.h @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Manipulator - edits something on-canvas + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_MANIPULATOR_H +#define SEEN_UI_TOOL_MANIPULATOR_H + +#include <set> +#include <map> +#include <cstddef> +#include <sigc++/sigc++.h> +#include <glib.h> +#include <gdk/gdk.h> +#include "ui/tools/tool-base.h" + +class SPDesktop; +namespace Inkscape { +namespace UI { + +class ManipulatorGroup; +class ControlPointSelection; + +/** + * @brief Tool component that processes events and does something in response to them. + * Note: this class is probably redundant. + */ +class Manipulator { +friend class ManipulatorGroup; +public: + Manipulator(SPDesktop *d) + : _desktop(d) + {} + virtual ~Manipulator() = default; + + /// Handle input event. Returns true if handled. + virtual bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *)=0; + SPDesktop *const _desktop; +}; + +/** + * @brief Tool component that edits something on the canvas using selectable control points. + * Note: this class is probably redundant. + */ +class PointManipulator : public Manipulator, public sigc::trackable { +public: + PointManipulator(SPDesktop *d, ControlPointSelection &sel) + : Manipulator(d) + , _selection(sel) + {} + + /// Type of extremum points to add in PathManipulator::insertNodeAtExtremum + enum ExtremumType { + EXTR_MIN_X = 0, + EXTR_MAX_X, + EXTR_MIN_Y, + EXTR_MAX_Y + }; +protected: + ControlPointSelection &_selection; +}; + +/** Manipulator that aggregates several manipulators of the same type. + * The order of invoking events on the member manipulators is undefined. + * To make this class more useful, derive from it and add actions that can be performed + * on all manipulators in the set. + * + * This is not used at the moment and is probably useless. */ +template <typename T> +class MultiManipulator : public PointManipulator { +public: + //typedef typename T::ItemType ItemType; + typedef typename std::pair<void*, std::shared_ptr<T> > MapPair; + typedef typename std::map<void*, std::shared_ptr<T> > MapType; + + MultiManipulator(SPDesktop *d, ControlPointSelection &sel) + : PointManipulator(d, sel) + {} + void addItem(void *item) { + std::shared_ptr<T> m(_createManipulator(item)); + _mmap.insert(MapPair(item, m)); + } + void removeItem(void *item) { + _mmap.erase(item); + } + void clear() { + _mmap.clear(); + } + bool contains(void *item) { + return _mmap.find(item) != _mmap.end(); + } + bool empty() { + return _mmap.empty(); + } + + void setItems(std::vector<gpointer> list) { // this function is not called anywhere ... delete ? + std::set<void*> to_remove; + for (typename MapType::iterator mi = _mmap.begin(); mi != _mmap.end(); ++mi) { + to_remove.insert(mi->first); + } + for (auto i:list) { + if (_isItemType(i)) { + // erase returns the number of items removed + // if nothing was removed, it means this item did not have a manipulator - add it + if (!to_remove.erase(i)) addItem(i); + } + } + for (auto ri : to_remove) { + removeItem(ri); + } + } + + /** Invoke a method on all managed manipulators. + * Example: + * @code m.invokeForAll(&SomeManipulator::someMethod); @endcode + */ + template <typename R> + void invokeForAll(R (T::*method)()) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(); + } + } + template <typename R, typename A> + void invokeForAll(R (T::*method)(A), A a) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a); + } + } + template <typename R, typename A> + void invokeForAll(R (T::*method)(A const &), A const &a) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a); + } + } + template <typename R, typename A, typename B> + void invokeForAll(R (T::*method)(A,B), A a, B b) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a, b); + } + } + + bool event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + if ((*i).second->event(event_context, event)) return true; + } + return false; + } +protected: + virtual T *_createManipulator(void *item) = 0; + virtual bool _isItemType(void *item) = 0; + MapType _mmap; +}; + +} // 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/modifier-tracker.cpp b/src/ui/tool/modifier-tracker.cpp new file mode 100644 index 0000000..70c85a6 --- /dev/null +++ b/src/ui/tool/modifier-tracker.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Fine-grained modifier tracker for event handling. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> +#include "ui/tool/event-utils.h" +#include "ui/tool/modifier-tracker.h" + +namespace Inkscape { +namespace UI { + +ModifierTracker::ModifierTracker() + : _left_shift(false) + , _right_shift(false) + , _left_ctrl(false) + , _right_ctrl(false) + , _left_alt(false) + , _right_alt(false) +{} + +bool ModifierTracker::event(GdkEvent *event) +{ + switch (event->type) { + case GDK_KEY_PRESS: + switch (shortcut_key(event->key)) { + case GDK_KEY_Shift_L: + _left_shift = true; + break; + case GDK_KEY_Shift_R: + _right_shift = true; + break; + case GDK_KEY_Control_L: + _left_ctrl = true; + break; + case GDK_KEY_Control_R: + _right_ctrl = true; + break; + case GDK_KEY_Alt_L: + _left_alt = true; + break; + case GDK_KEY_Alt_R: + _right_alt = true; + break; + } + break; + case GDK_KEY_RELEASE: + switch (shortcut_key(event->key)) { + case GDK_KEY_Shift_L: + _left_shift = false; + break; + case GDK_KEY_Shift_R: + _right_shift = false; + break; + case GDK_KEY_Control_L: + _left_ctrl = false; + break; + case GDK_KEY_Control_R: + _right_ctrl = false; + break; + case GDK_KEY_Alt_L: + _left_alt = false; + break; + case GDK_KEY_Alt_R: + _right_alt = false; + break; + } + break; + default: break; + } + + 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/modifier-tracker.h b/src/ui/tool/modifier-tracker.h new file mode 100644 index 0000000..c5762e5 --- /dev/null +++ b/src/ui/tool/modifier-tracker.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Fine-grained modifier tracker for event handling. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_MODIFIER_TRACKER_H +#define SEEN_UI_TOOL_MODIFIER_TRACKER_H + +#include <gdk/gdk.h> + +namespace Inkscape { +namespace UI { + +class ModifierTracker { +public: + ModifierTracker(); + bool event(GdkEvent *); + + bool leftShift() const { return _left_shift; } + bool rightShift() const { return _right_shift; } + bool leftControl() const { return _left_ctrl; } + bool rightControl() const { return _right_ctrl; } + bool leftAlt() const { return _left_alt; } + bool rightAlt() const { return _right_alt; } + +private: + bool _left_shift; + bool _right_shift; + bool _left_ctrl; + bool _right_ctrl; + bool _left_alt; + bool _right_alt; +}; + +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_UI_TOOL_MODIFIER_TRACKER_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/tool/multi-path-manipulator.cpp b/src/ui/tool/multi-path-manipulator.cpp new file mode 100644 index 0000000..c3bf969 --- /dev/null +++ b/src/ui/tool/multi-path-manipulator.cpp @@ -0,0 +1,899 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Multi path manipulator - implementation. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <unordered_set> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "message-stack.h" +#include "node.h" + +#include "live_effects/lpeobject.h" + +#include "object/sp-path.h" + +#include "ui/icon-names.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" + +namespace Inkscape { +namespace UI { + +namespace { + +struct hash_nodelist_iterator + : public std::unary_function<NodeList::iterator, std::size_t> +{ + std::size_t operator()(NodeList::iterator i) const { + return std::hash<NodeList::iterator::pointer>()(&*i); + } +}; + +typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair; +typedef std::vector<IterPair> IterPairList; +typedef std::unordered_set<NodeList::iterator, hash_nodelist_iterator> IterSet; +typedef std::multimap<double, IterPair> DistanceMap; +typedef std::pair<double, IterPair> DistanceMapItem; + +/** Find pairs of selected endnodes suitable for joining. */ +void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs) +{ + IterSet join_iters; + + // find all endnodes in selection + for (auto i : sel) { + Node *node = dynamic_cast<Node*>(i); + if (!node) continue; + NodeList::iterator iter = NodeList::get_iterator(node); + if (!iter.next() || !iter.prev()) join_iters.insert(iter); + } + + if (join_iters.size() < 2) return; + + // Below we find the closest pairs. The algorithm is O(N^3). + // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs + // with their distances in a multimap (not worth it IMO). + while (join_iters.size() >= 2) { + double closest = DBL_MAX; + IterPair closest_pair; + for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) { + for (IterSet::iterator j = join_iters.begin(); j != i; ++j) { + double dist = Geom::distance(**i, **j); + if (dist < closest) { + closest = dist; + closest_pair = std::make_pair(*i, *j); + } + } + } + pairs.push_back(closest_pair); + join_iters.erase(closest_pair.first); + join_iters.erase(closest_pair.second); + } +} + +/** After this function, first should be at the end of path and second at the beginning. + * @returns True if the nodes are in the same subpath */ +bool prepare_join(IterPair &join_iters) +{ + if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) { + if (join_iters.first.next()) // if first is begin, swap the iterators + std::swap(join_iters.first, join_iters.second); + return true; + } + + NodeList &sp_first = NodeList::get(join_iters.first); + NodeList &sp_second = NodeList::get(join_iters.second); + if (join_iters.first.next()) { // first is begin + if (join_iters.second.next()) { // second is begin + sp_first.reverse(); + } else { // second is end + std::swap(join_iters.first, join_iters.second); + } + } else { // first is end + if (join_iters.second.next()) { // second is begin + // do nothing + } else { // second is end + sp_second.reverse(); + } + } + return false; +} +} // anonymous namespace + + +MultiPathManipulator::MultiPathManipulator(PathSharedData &data, sigc::connection &chg) + : PointManipulator(data.node_data.desktop, *data.node_data.selection) + , _path_data(data) + , _changed(chg) +{ + _selection.signal_commit.connect( + sigc::mem_fun(*this, &MultiPathManipulator::_commit)); + _selection.signal_selection_changed.connect( + sigc::hide( sigc::hide( + signal_coords_changed.make_slot()))); +} + +MultiPathManipulator::~MultiPathManipulator() +{ + _mmap.clear(); +} + +/** Remove empty manipulators. */ +void MultiPathManipulator::cleanup() +{ + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) { + if (i->second->empty()) i = _mmap.erase(i); + else ++i; + } +} + +/** + * Change the set of items to edit. + * + * This method attempts to preserve as much of the state as possible. + */ +void MultiPathManipulator::setItems(std::set<ShapeRecord> const &s) +{ + std::set<ShapeRecord> shapes(s); + + // iterate over currently edited items, modifying / removing them as necessary + for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) { + std::set<ShapeRecord>::iterator si = shapes.find(i->first); + if (si == shapes.end()) { + // This item is no longer supposed to be edited - remove its manipulator + i = _mmap.erase(i); + } else { + ShapeRecord const &sr = i->first; + ShapeRecord const &sr_new = *si; + // if the shape record differs, replace the key only and modify other values + if (sr.edit_transform != sr_new.edit_transform || + sr.role != sr_new.role) + { + std::shared_ptr<PathManipulator> hold(i->second); + if (sr.edit_transform != sr_new.edit_transform) + hold->setControlsTransform(sr_new.edit_transform); + if (sr.role != sr_new.role) { + //hold->setOutlineColor(_getOutlineColor(sr_new.role)); + } + i = _mmap.erase(i); + _mmap.insert(std::make_pair(sr_new, hold)); + } else { + ++i; + } + shapes.erase(si); // remove the processed record + } + } + + // add newly selected items + for (const auto & r : shapes) { + LivePathEffectObject *lpobj = dynamic_cast<LivePathEffectObject *>(r.object); + if (!SP_IS_PATH(r.object) && !lpobj) continue; + std::shared_ptr<PathManipulator> newpm(new PathManipulator(*this, (SPPath*) r.object, + r.edit_transform, _getOutlineColor(r.role, r.object), r.lpe_key)); + newpm->showHandles(_show_handles); + // always show outlines for clips and masks + newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL); + newpm->showPathDirection(_show_path_direction); + newpm->setLiveOutline(_live_outline); + newpm->setLiveObjects(_live_objects); + _mmap.insert(std::make_pair(r, newpm)); + } +} + +void MultiPathManipulator::selectSubpaths() +{ + if (_selection.empty()) { + _selection.selectAll(); + } else { + invokeForAll(&PathManipulator::selectSubpaths); + } +} + +void MultiPathManipulator::shiftSelection(int dir) +{ + if (empty()) return; + + // 1. find last selected node + // 2. select the next node; if the last node or nothing is selected, + // select first node + MapType::iterator last_i; + SubpathList::iterator last_j; + NodeList::iterator last_k; + bool anything_found = false; + bool anynode_found = false; + + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + SubpathList &sp = i->second->subpathList(); + for (SubpathList::iterator j = sp.begin(); j != sp.end(); ++j) { + anynode_found = true; + for (NodeList::iterator k = (*j)->begin(); k != (*j)->end(); ++k) { + if (k->selected()) { + last_i = i; + last_j = j; + last_k = k; + anything_found = true; + // when tabbing backwards, we want the first node + if (dir == -1) goto exit_loop; + } + } + } + } + exit_loop: + + // NOTE: we should not assume the _selection contains only nodes + // in future it might also contain handles and other types of control points + // this is why we use a flag instead in the loop above, instead of calling + // selection.empty() + if (!anything_found) { + // select first / last node + // this should never fail because there must be at least 1 non-empty manipulator + if (anynode_found) { + if (dir == 1) { + _selection.insert((*_mmap.begin()->second->subpathList().begin())->begin().ptr()); + } else { + _selection.insert((--(*--(--_mmap.end())->second->subpathList().end())->end()).ptr()); + } + } + return; + } + + // three levels deep - w00t! + if (dir == 1) { + if (++last_k == (*last_j)->end()) { + // here, last_k points to the node to be selected + ++last_j; + if (last_j == last_i->second->subpathList().end()) { + ++last_i; + if (last_i == _mmap.end()) { + last_i = _mmap.begin(); + } + last_j = last_i->second->subpathList().begin(); + } + last_k = (*last_j)->begin(); + } + } else { + if (!last_k || last_k == (*last_j)->begin()) { + if (last_j == last_i->second->subpathList().begin()) { + if (last_i == _mmap.begin()) { + last_i = _mmap.end(); + } + --last_i; + last_j = last_i->second->subpathList().end(); + } + --last_j; + last_k = (*last_j)->end(); + } + --last_k; + } + _selection.clear(); + _selection.insert(last_k.ptr()); +} + +void MultiPathManipulator::invertSelectionInSubpaths() +{ + invokeForAll(&PathManipulator::invertSelectionInSubpaths); +} + +void MultiPathManipulator::setNodeType(NodeType type) +{ + if (_selection.empty()) return; + + // When all selected nodes are already cusp, retract their handles + bool retract_handles = (type == NODE_CUSP); + + for (auto i : _selection) { + Node *node = dynamic_cast<Node*>(i); + if (node) { + retract_handles &= (node->type() == NODE_CUSP); + node->setType(type); + } + } + + if (retract_handles) { + for (auto i : _selection) { + Node *node = dynamic_cast<Node*>(i); + if (node) { + node->front()->retract(); + node->back()->retract(); + } + } + } + + _done(retract_handles ? _("Retract handles") : _("Change node type")); +} + +void MultiPathManipulator::setSegmentType(SegmentType type) +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::setSegmentType, type); + if (type == SEGMENT_STRAIGHT) { + _done(_("Straighten segments")); + } else { + _done(_("Make segments curves")); + } +} + +void MultiPathManipulator::insertNodes() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::insertNodes); + _done(_("Add nodes")); +} +void MultiPathManipulator::insertNodesAtExtrema(ExtremumType extremum) +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::insertNodeAtExtremum, extremum); + _done(_("Add extremum nodes")); +} + +void MultiPathManipulator::insertNode(Geom::Point pt) +{ + // When double clicking to insert nodes, we might not have a selection of nodes (and we don't need one) + // so don't check for "_selection.empty()" here, contrary to the other methods above and below this one + invokeForAll(&PathManipulator::insertNode, pt); + _done(_("Add nodes")); +} + +void MultiPathManipulator::duplicateNodes() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::duplicateNodes); + _done(_("Duplicate nodes")); +} + +void MultiPathManipulator::copySelectedPath(Geom::PathBuilder *builder) +{ + if (_selection.empty()) + return; + invokeForAll(&PathManipulator::copySelectedPath, builder); + _done(_("Copy nodes")); +} + +void MultiPathManipulator::joinNodes() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::hideDragPoint); + // Node join has two parts. In the first one we join two subpaths by fusing endpoints + // into one. In the second we fuse nodes in each subpath. + IterPairList joins; + NodeList::iterator preserve_pos; + Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point); + if (mouseover_node) { + preserve_pos = NodeList::get_iterator(mouseover_node); + } + find_join_iterators(_selection, joins); + + for (auto & join : joins) { + bool same_path = prepare_join(join); + NodeList &sp_first = NodeList::get(join.first); + NodeList &sp_second = NodeList::get(join.second); + join.first->setType(NODE_CUSP, false); + + Geom::Point joined_pos, pos_handle_front, pos_handle_back; + pos_handle_front = *join.second->front(); + pos_handle_back = *join.first->back(); + + // When we encounter the mouseover node, we unset the iterator - it will be invalidated + if (join.first == preserve_pos) { + joined_pos = *join.first; + preserve_pos = NodeList::iterator(); + } else if (join.second == preserve_pos) { + joined_pos = *join.second; + preserve_pos = NodeList::iterator(); + } else { + joined_pos = Geom::middle_point(*join.first, *join.second); + } + + // if the handles aren't degenerate, don't move them + join.first->move(joined_pos); + Node *joined_node = join.first.ptr(); + if (!join.second->front()->isDegenerate()) { + joined_node->front()->setPosition(pos_handle_front); + } + if (!join.first->back()->isDegenerate()) { + joined_node->back()->setPosition(pos_handle_back); + } + sp_second.erase(join.second); + + if (same_path) { + sp_first.setClosed(true); + } else { + sp_first.splice(sp_first.end(), sp_second); + sp_second.kill(); + } + _selection.insert(join.first.ptr()); + } + + if (joins.empty()) { + // Second part replaces contiguous selections of nodes with single nodes + invokeForAll(&PathManipulator::weldNodes, preserve_pos); + } + + _doneWithCleanup(_("Join nodes"), true); +} + +void MultiPathManipulator::breakNodes() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::breakNodes); + _done(_("Break nodes"), true); +} + +void MultiPathManipulator::deleteNodes(bool keep_shape) +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::deleteNodes, keep_shape); + _doneWithCleanup(_("Delete nodes"), true); +} + +/** Join selected endpoints to create segments. */ +void MultiPathManipulator::joinSegments() +{ + if (_selection.empty()) return; + IterPairList joins; + find_join_iterators(_selection, joins); + + for (auto & join : joins) { + bool same_path = prepare_join(join); + NodeList &sp_first = NodeList::get(join.first); + NodeList &sp_second = NodeList::get(join.second); + join.first->setType(NODE_CUSP, false); + join.second->setType(NODE_CUSP, false); + if (same_path) { + sp_first.setClosed(true); + } else { + sp_first.splice(sp_first.end(), sp_second); + sp_second.kill(); + } + } + + if (joins.empty()) { + invokeForAll(&PathManipulator::weldSegments); + } + _doneWithCleanup("Join segments", true); +} + +void MultiPathManipulator::deleteSegments() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::deleteSegments); + _doneWithCleanup("Delete segments", true); +} + +void MultiPathManipulator::alignNodes(Geom::Dim2 d, AlignTargetNode target) +{ + if (_selection.empty()) return; + _selection.align(d, target); + if (d == Geom::X) { + _done("Align nodes to a horizontal line"); + } else { + _done("Align nodes to a vertical line"); + } +} + +void MultiPathManipulator::distributeNodes(Geom::Dim2 d) +{ + if (_selection.empty()) return; + _selection.distribute(d); + if (d == Geom::X) { + _done("Distribute nodes horizontally"); + } else { + _done("Distribute nodes vertically"); + } +} + +void MultiPathManipulator::reverseSubpaths() +{ + if (_selection.empty()) { + invokeForAll(&PathManipulator::reverseSubpaths, false); + _done("Reverse subpaths"); + } else { + invokeForAll(&PathManipulator::reverseSubpaths, true); + _done("Reverse selected subpaths"); + } +} + +void MultiPathManipulator::move(Geom::Point const &delta) +{ + if (_selection.empty()) return; + _selection.transform(Geom::Translate(delta)); + _done("Move nodes"); +} + +void MultiPathManipulator::showOutline(bool show) +{ + for (auto & i : _mmap) { + // always show outlines for clipping paths and masks + i.second->showOutline(show || i.first.role != SHAPE_ROLE_NORMAL); + } + _show_outline = show; +} + +void MultiPathManipulator::showHandles(bool show) +{ + invokeForAll(&PathManipulator::showHandles, show); + _show_handles = show; +} + +void MultiPathManipulator::showPathDirection(bool show) +{ + invokeForAll(&PathManipulator::showPathDirection, show); + _show_path_direction = show; +} + +/** + * Set live outline update status. + * When set to true, outline will be updated continuously when dragging + * or transforming nodes. Otherwise it will only update when changes are committed + * to XML. + */ +void MultiPathManipulator::setLiveOutline(bool set) +{ + invokeForAll(&PathManipulator::setLiveOutline, set); + _live_outline = set; +} + +/** + * Set live object update status. + * When set to true, objects will be updated continuously when dragging + * or transforming nodes. Otherwise they will only update when changes are committed + * to XML. + */ +void MultiPathManipulator::setLiveObjects(bool set) +{ + invokeForAll(&PathManipulator::setLiveObjects, set); + _live_objects = set; +} + +void MultiPathManipulator::updateOutlineColors() +{ + //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + // i->second->setOutlineColor(_getOutlineColor(i->first.role)); + //} +} + +void MultiPathManipulator::updateHandles() +{ + invokeForAll(&PathManipulator::updateHandles); +} + +void MultiPathManipulator::updatePaths() +{ + invokeForAll(&PathManipulator::updatePath); +} + +bool MultiPathManipulator::event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + _tracker.event(event); + guint key = 0; + if (event->type == GDK_KEY_PRESS) { + key = shortcut_key(event->key); + } + + // Single handle adjustments go here. + if (_selection.size() == 1 && event->type == GDK_KEY_PRESS) { + do { + Node *n = dynamic_cast<Node *>(*_selection.begin()); + if (!n) break; + + PathManipulator &pm = n->nodeList().subpathList().pm(); + + int which = 0; + if (_tracker.rightAlt() || _tracker.rightControl()) { + which = 1; + } + if (_tracker.leftAlt() || _tracker.leftControl()) { + if (which != 0) break; // ambiguous + which = -1; + } + if (which == 0) break; // no handle chosen + bool one_pixel = _tracker.leftAlt() || _tracker.rightAlt(); + bool handled = true; + + switch (key) { + // single handle functions + // rotation + case GDK_KEY_bracketleft: + case GDK_KEY_braceleft: + pm.rotateHandle(n, which, -_desktop->yaxisdir(), one_pixel); + break; + case GDK_KEY_bracketright: + case GDK_KEY_braceright: + pm.rotateHandle(n, which, _desktop->yaxisdir(), one_pixel); + break; + // adjust length + case GDK_KEY_period: + case GDK_KEY_greater: + pm.scaleHandle(n, which, 1, one_pixel); + break; + case GDK_KEY_comma: + case GDK_KEY_less: + pm.scaleHandle(n, which, -1, one_pixel); + break; + default: + handled = false; + break; + } + + if (handled) return true; + } while(false); + } + + + switch (event->type) { + case GDK_KEY_PRESS: + switch (key) { + case GDK_KEY_Insert: + case GDK_KEY_KP_Insert: + // Insert - insert nodes in the middle of selected segments + insertNodes(); + return true; + case GDK_KEY_i: + case GDK_KEY_I: + if (held_only_shift(event->key)) { + // Shift+I - insert nodes (alternate keybinding for Mac keyboards + // that don't have the Insert key) + insertNodes(); + return true; + } + break; + case GDK_KEY_d: + case GDK_KEY_D: + if (held_only_shift(event->key)) { + duplicateNodes(); + return true; + } + case GDK_KEY_j: + case GDK_KEY_J: + if (held_only_shift(event->key)) { + // Shift+J - join nodes + joinNodes(); + return true; + } + if (held_only_alt(event->key)) { + // Alt+J - join segments + joinSegments(); + return true; + } + break; + case GDK_KEY_b: + case GDK_KEY_B: + if (held_only_shift(event->key)) { + // Shift+B - break nodes + breakNodes(); + return true; + } + break; + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + if (held_shift(event->key)) break; + if (held_alt(event->key)) { + // Alt+Delete - delete segments + deleteSegments(); + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool del_preserves_shape = prefs->getBool("/tools/nodes/delete_preserves_shape", true); + // pass keep_shape = true when: + // a) del preserves shape, and control is not pressed + // b) ctrl+del preserves shape (del_preserves_shape is false), and control is pressed + // Hence xor + guint mode = prefs->getInt("/tools/freehand/pen/freehand-mode", 0); + + //if the trace is bspline ( mode 2) + if(mode==2){ + // is this correct ? + if(del_preserves_shape ^ held_control(event->key)){ + deleteNodes(false); + } else { + deleteNodes(true); + } + } else { + deleteNodes(del_preserves_shape ^ held_control(event->key)); + } + + // Delete any selected gradient nodes as well + event_context->deleteSelectedDrag(held_control(event->key)); + } + return true; + case GDK_KEY_c: + case GDK_KEY_C: + if (held_only_shift(event->key)) { + // Shift+C - make nodes cusp + setNodeType(NODE_CUSP); + return true; + } + break; + case GDK_KEY_s: + case GDK_KEY_S: + if (held_only_shift(event->key)) { + // Shift+S - make nodes smooth + setNodeType(NODE_SMOOTH); + return true; + } + break; + case GDK_KEY_a: + case GDK_KEY_A: + if (held_only_shift(event->key)) { + // Shift+A - make nodes auto-smooth + setNodeType(NODE_AUTO); + return true; + } + break; + case GDK_KEY_y: + case GDK_KEY_Y: + if (held_only_shift(event->key)) { + // Shift+Y - make nodes symmetric + setNodeType(NODE_SYMMETRIC); + return true; + } + break; + case GDK_KEY_r: + case GDK_KEY_R: + if (held_only_shift(event->key)) { + // Shift+R - reverse subpaths + reverseSubpaths(); + return true; + } + break; + case GDK_KEY_l: + case GDK_KEY_L: + if (held_only_shift(event->key)) { + // Shift+L - make segments linear + setSegmentType(SEGMENT_STRAIGHT); + return true; + } + case GDK_KEY_u: + case GDK_KEY_U: + if (held_only_shift(event->key)) { + // Shift+U - make segments curves + setSegmentType(SEGMENT_CUBIC_BEZIER); + return true; + } + default: + break; + } + break; + case GDK_MOTION_NOTIFY: + for (auto & i : _mmap) { + if (i.second->event(event_context, event)) return true; + } + break; + default: break; + } + + return false; +} + +/** Commit changes to XML and add undo stack entry based on the action that was done. Invoked + * by sub-manipulators, for example TransformHandleSet and ControlPointSelection. */ +void MultiPathManipulator::_commit(CommitEvent cps) +{ + gchar const *reason = nullptr; + gchar const *key = nullptr; + switch(cps) { + case COMMIT_MOUSE_MOVE: + reason = _("Move nodes"); + break; + case COMMIT_KEYBOARD_MOVE_X: + reason = _("Move nodes horizontally"); + key = "node:move:x"; + break; + case COMMIT_KEYBOARD_MOVE_Y: + reason = _("Move nodes vertically"); + key = "node:move:y"; + break; + case COMMIT_MOUSE_ROTATE: + reason = _("Rotate nodes"); + break; + case COMMIT_KEYBOARD_ROTATE: + reason = _("Rotate nodes"); + key = "node:rotate"; + break; + case COMMIT_MOUSE_SCALE_UNIFORM: + reason = _("Scale nodes uniformly"); + break; + case COMMIT_MOUSE_SCALE: + reason = _("Scale nodes"); + break; + case COMMIT_KEYBOARD_SCALE_UNIFORM: + reason = _("Scale nodes uniformly"); + key = "node:scale:uniform"; + break; + case COMMIT_KEYBOARD_SCALE_X: + reason = _("Scale nodes horizontally"); + key = "node:scale:x"; + break; + case COMMIT_KEYBOARD_SCALE_Y: + reason = _("Scale nodes vertically"); + key = "node:scale:y"; + break; + case COMMIT_MOUSE_SKEW_X: + reason = _("Skew nodes horizontally"); + key = "node:skew:x"; + break; + case COMMIT_MOUSE_SKEW_Y: + reason = _("Skew nodes vertically"); + key = "node:skew:y"; + break; + case COMMIT_FLIP_X: + reason = _("Flip nodes horizontally"); + break; + case COMMIT_FLIP_Y: + reason = _("Flip nodes vertically"); + break; + default: return; + } + + _selection.signal_update.emit(); + invokeForAll(&PathManipulator::writeXML); + if (key) { + DocumentUndo::maybeDone(_desktop->getDocument(), key, reason, INKSCAPE_ICON("tool-node-editor")); + } else { + DocumentUndo::done(_desktop->getDocument(), reason, INKSCAPE_ICON("tool-node-editor")); + } + signal_coords_changed.emit(); +} + +/** Commits changes to XML and adds undo stack entry. */ +void MultiPathManipulator::_done(gchar const *reason, bool alert_LPE) { + invokeForAll(&PathManipulator::update, alert_LPE); + invokeForAll(&PathManipulator::writeXML); + DocumentUndo::done(_desktop->getDocument(), reason, INKSCAPE_ICON("tool-node-editor")); + signal_coords_changed.emit(); +} + +/** Commits changes to XML, adds undo stack entry and removes empty manipulators. */ +void MultiPathManipulator::_doneWithCleanup(gchar const *reason, bool alert_LPE) { + _changed.block(); + _done(reason, alert_LPE); + cleanup(); + _changed.unblock(); +} + +/** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */ +guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role, SPObject *object) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch(role) { + case SHAPE_ROLE_CLIPPING_PATH: + return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff); + case SHAPE_ROLE_MASK: + return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff); + case SHAPE_ROLE_LPE_PARAM: + return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff); + case SHAPE_ROLE_NORMAL: + default: + return SP_ITEM(object)->highlight_color(); + } +} + +} // 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/multi-path-manipulator.h b/src/ui/tool/multi-path-manipulator.h new file mode 100644 index 0000000..c00436c --- /dev/null +++ b/src/ui/tool/multi-path-manipulator.h @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Multi path manipulator - a tool component that edits multiple paths at once + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_MULTI_PATH_MANIPULATOR_H +#define SEEN_UI_TOOL_MULTI_PATH_MANIPULATOR_H + +#include <cstddef> +#include <sigc++/connection.h> +#include <2geom/path-sink.h> +#include "node.h" +#include "commit-events.h" +#include "manipulator.h" +#include "modifier-tracker.h" +#include "node-types.h" +#include "shape-record.h" + +struct SPCanvasGroup; + +namespace Inkscape { +namespace UI { + +class PathManipulator; +class MultiPathManipulator; +struct PathSharedData; + +/** + * Manipulator that manages multiple path manipulators active at the same time. + */ +class MultiPathManipulator : public PointManipulator { +public: + MultiPathManipulator(PathSharedData &data, sigc::connection &chg); + ~MultiPathManipulator() override; + bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *event) override; + + bool empty() { return _mmap.empty(); } + unsigned size() { return _mmap.size(); } + void setItems(std::set<ShapeRecord> const &); + void clear() { _mmap.clear(); } + void cleanup(); + + void selectSubpaths(); + void shiftSelection(int dir); + void invertSelectionInSubpaths(); + + void setNodeType(NodeType t); + void setSegmentType(SegmentType t); + + void insertNodesAtExtrema(ExtremumType extremum); + void insertNodes(); + void insertNode(Geom::Point pt); + void alertLPE(); + void duplicateNodes(); + void copySelectedPath(Geom::PathBuilder *builder); + void joinNodes(); + void breakNodes(); + void deleteNodes(bool keep_shape = true); + void joinSegments(); + void deleteSegments(); + void alignNodes(Geom::Dim2 d, AlignTargetNode target = AlignTargetNode::MID_NODE); + void distributeNodes(Geom::Dim2 d); + void reverseSubpaths(); + void move(Geom::Point const &delta); + + void showOutline(bool show); + void showHandles(bool show); + void showPathDirection(bool show); + void setLiveOutline(bool set); + void setLiveObjects(bool set); + void updateOutlineColors(); + void updateHandles(); + void updatePaths(); + + sigc::signal<void> signal_coords_changed; /// Emitted whenever the coordinates + /// shown in the status bar need updating +private: + typedef std::pair<ShapeRecord, std::shared_ptr<PathManipulator> > MapPair; + typedef std::map<ShapeRecord, std::shared_ptr<PathManipulator> > MapType; + + template <typename R> + void invokeForAll(R (PathManipulator::*method)()) { + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) { + // Sometimes the PathManipulator got freed at loop end, thus + // invalidating the iterator so make sure that next_i will + // be a valid iterator and then assign i to it. + MapType::iterator next_i = i; + ++next_i; + // i->second is a std::shared_ptr so try to hold on to it so + // it won't get freed prematurely by the WriteXML() method or + // whatever. See https://bugs.launchpad.net/inkscape/+bug/1617615 + // Applicable to empty paths. + std::shared_ptr<PathManipulator> hold(i->second); + ((hold.get())->*method)(); + i = next_i; + } + } + template <typename R, typename A> + void invokeForAll(R (PathManipulator::*method)(A), A a) { + for (auto & i : _mmap) { + ((i.second.get())->*method)(a); + } + } + template <typename R, typename A> + void invokeForAll(R (PathManipulator::*method)(A const &), A const &a) { + for (auto & i : _mmap) { + ((i.second.get())->*method)(a); + } + } + template <typename R, typename A, typename B> + void invokeForAll(R (PathManipulator::*method)(A,B), A a, B b) { + for (auto & i : _mmap) { + ((i.second.get())->*method)(a, b); + } + } + + void _commit(CommitEvent cps); + void _done(gchar const *reason, bool alert_LPE = true); + void _doneWithCleanup(gchar const *reason, bool alert_LPE = false); + guint32 _getOutlineColor(ShapeRole role, SPObject *object); + + MapType _mmap; +public: + PathSharedData const &_path_data; +private: + sigc::connection &_changed; + ModifierTracker _tracker; + bool _show_handles; + bool _show_outline; + bool _show_path_direction; + bool _live_outline; + bool _live_objects; + + friend class PathManipulator; +}; + +} // 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/node-types.h b/src/ui/tool/node-types.h new file mode 100644 index 0000000..bad6a5c --- /dev/null +++ b/src/ui/tool/node-types.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Node types and other small enums. + * This file exists to reduce the number of includes pulled in by toolbox.cpp. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_NODE_TYPES_H +#define SEEN_UI_TOOL_NODE_TYPES_H + +namespace Inkscape { +namespace UI { + +/** Types of nodes supported in the node tool. */ +enum NodeType { + NODE_CUSP, ///< Cusp node - no handle constraints + NODE_SMOOTH, ///< Smooth node - handles must be colinear + NODE_AUTO, ///< Auto node - handles adjusted automatically based on neighboring nodes + NODE_SYMMETRIC, ///< Symmetric node - handles must be colinear and of equal length + NODE_LAST_REAL_TYPE, ///< Last real type of node - used for ctrl+click on a node + NODE_PICK_BEST = 100 ///< Select type based on handle positions +}; + +/** Types of segments supported in the node tool. */ +enum SegmentType { + SEGMENT_STRAIGHT, ///< Straight linear segment + SEGMENT_CUBIC_BEZIER ///< Bezier curve with two control points +}; + +enum class AlignTargetNode { + LAST_NODE, + FIRST_NODE, + MID_NODE, + MIN_NODE, + MAX_NODE +}; + +} // 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/node.cpp b/src/ui/tool/node.cpp new file mode 100644 index 0000000..57be0af --- /dev/null +++ b/src/ui/tool/node.cpp @@ -0,0 +1,1923 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <atomic> +#include <iostream> +#include <stdexcept> +#include <boost/utility.hpp> + +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include <2geom/bezier-utils.h> + +#include "desktop.h" +#include "multi-path-manipulator.h" +#include "snap.h" + +#include "display/control/canvas-item-group.h" +#include "display/control/canvas-item-curve.h" + +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/canvas.h" + +namespace { + +Inkscape::CanvasItemCtrlType nodeTypeToCtrlType(Inkscape::UI::NodeType type) +{ + Inkscape::CanvasItemCtrlType result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_CUSP; + switch(type) { + case Inkscape::UI::NODE_SMOOTH: + result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH; + break; + case Inkscape::UI::NODE_AUTO: + result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_AUTO; + break; + case Inkscape::UI::NODE_SYMMETRIC: + result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL; + break; + case Inkscape::UI::NODE_CUSP: + default: + result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_CUSP; + break; + } + return result; +} + +/** + * @brief provides means to estimate float point rounding error due to serialization to svg + * + * Keeps cached value up to date with preferences option `/options/svgoutput/numericprecision` + * to avoid costly direct reads + * */ +class SvgOutputPrecisionWatcher : public Inkscape::Preferences::Observer { +public: + /// Returns absolute \a value`s rounding serialization error based on current preferences settings + static double error_of(double value) { + return value * instance().rel_error; + } + + void notify(const Inkscape::Preferences::Entry &new_val) override { + int digits = new_val.getIntLimited(6, 1, 16); + set_numeric_precision(digits); + } + +private: + SvgOutputPrecisionWatcher() : Observer("/options/svgoutput/numericprecision"), rel_error(1) { + Inkscape::Preferences::get()->addObserver(*this); + int digits = Inkscape::Preferences::get()->getIntLimited("/options/svgoutput/numericprecision", 6, 1, 16); + set_numeric_precision(digits); + } + + ~SvgOutputPrecisionWatcher() override { + Inkscape::Preferences::get()->removeObserver(*this); + } + /// Update cached value of relative error with number of significant digits + void set_numeric_precision(int digits) { + double relative_error = 0.5; // the error is half of last digit + while (digits > 0) { + relative_error /= 10; + digits--; + } + rel_error = relative_error; + } + + static SvgOutputPrecisionWatcher &instance() { + static SvgOutputPrecisionWatcher _instance; + return _instance; + } + + std::atomic<double> rel_error; /// Cached relative error +}; + +/// Returns absolute error of \a point as if serialized to svg with current preferences +double serializing_error_of(const Geom::Point &point) { + return SvgOutputPrecisionWatcher::error_of(point.length()); +} + +/** + * @brief Returns true if three points are collinear within current serializing precision + * + * The algorithm of collinearity check is explicitly used to calculate the check error. + * + * This function can be sufficiently reduced or even removed completely if `Geom::are_collinear` + * would declare it's check algorithm as part of the public API. + * + * */ +bool are_collinear_within_serializing_error(const Geom::Point &A, const Geom::Point &B, const Geom::Point &C) { + const double tolerance_factor = 10; // to account other factors which increase uncertainty + const double tolerance_A = serializing_error_of(A) * tolerance_factor; + const double tolerance_B = serializing_error_of(B) * tolerance_factor; + const double tolerance_C = serializing_error_of(C) * tolerance_factor; + const double CB_length = (B - C).length(); + const double AB_length = (B - A).length(); + Geom::Point C_reflect_scaled = B + (B - C) / CB_length * AB_length; + double tolerance_C_reflect_scaled = tolerance_B + + (tolerance_B + tolerance_C) + * (1 + (tolerance_A + tolerance_B) / AB_length) + * (1 + (tolerance_C + tolerance_B) / CB_length); + return Geom::are_near(C_reflect_scaled, A, tolerance_C_reflect_scaled + tolerance_A); +} + +} // namespace + +namespace Inkscape { +namespace UI { + +const double NO_POWER = 0.0; +const double DEFAULT_START_POWER = 1.0/3.0; + +ControlPoint::ColorSet Node::node_colors = { + {0xbfbfbf00, 0x000000ff}, // normal fill, stroke + {0xff000000, 0x000000ff}, // mouseover fill, stroke + {0xff000000, 0x000000ff}, // 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::ColorSet Handle::_handle_colors = { + {0xffffffff, 0x000000ff}, // normal fill, stroke + {0xff000000, 0x000000ff}, // mouseover fill, stroke + {0xff000000, 0x000000ff}, // clicked fill, stroke + // + {0xffffffff, 0x000000ff}, // normal fill, stroke + {0xff000000, 0x000000ff}, // mouseover fill, stroke + {0xff000000, 0x000000ff} // clicked fill, stroke +}; + +std::ostream &operator<<(std::ostream &out, NodeType type) +{ + switch(type) { + case NODE_CUSP: out << 'c'; break; + case NODE_SMOOTH: out << 's'; break; + case NODE_AUTO: out << 'a'; break; + case NODE_SYMMETRIC: out << 'z'; break; + default: out << 'b'; break; + } + return out; +} + +/** Computes an unit vector of the direction from first to second control point */ +static Geom::Point direction(Geom::Point const &first, Geom::Point const &second) { + return Geom::unit_vector(second - first); +} + +Geom::Point Handle::_saved_other_pos(0, 0); + +double Handle::_saved_length = 0.0; + +bool Handle::_drag_out = false; + +Handle::Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent) + : ControlPoint(data.desktop, initial_pos, SP_ANCHOR_CENTER, + Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, + _handle_colors, data.handle_group) + , _handle_line(new Inkscape::CanvasItemCurve(data.handle_line_group)) + , _parent(parent) + , _degenerate(true) +{ + setVisible(false); +} + +Handle::~Handle() +{ + delete _handle_line; +} + +void Handle::setVisible(bool v) +{ + ControlPoint::setVisible(v); + if (v) { + _handle_line->show(); + } else { + _handle_line->hide(); + } +} + +void Handle::move(Geom::Point const &new_pos) +{ + Handle *other = this->other(); + Node *node_towards = _parent->nodeToward(this); // node in direction of this handle + Node *node_away = _parent->nodeAwayFrom(this); // node in the opposite direction + Handle *towards = node_towards ? node_towards->handleAwayFrom(_parent) : nullptr; + Handle *towards_second = node_towards ? node_towards->handleToward(_parent) : nullptr; + double bspline_weight = 0.0; + + if (Geom::are_near(new_pos, _parent->position())) { + // The handle becomes degenerate. + // Adjust node type as necessary. + if (other->isDegenerate()) { + // If both handles become degenerate, convert to parent cusp node + _parent->setType(NODE_CUSP, false); + } else { + // Only 1 handle becomes degenerate + switch (_parent->type()) { + case NODE_AUTO: + case NODE_SYMMETRIC: + _parent->setType(NODE_SMOOTH, false); + break; + default: + // do nothing for other node types + break; + } + } + // If the segment between the handle and the node in its direction becomes linear, + // and there are smooth nodes at its ends, make their handles collinear with the segment. + if (towards && towards_second->isDegenerate()) { + if (node_towards->type() == NODE_SMOOTH) { + towards->setDirection(*_parent, *node_towards); + } + if (_parent->type() == NODE_SMOOTH) { + other->setDirection(*node_towards, *_parent); + } + } + setPosition(new_pos); + + // move the handle and its opposite the same proportion + if(_pm()._isBSpline()){ + setPosition(_pm()._bsplineHandleReposition(this, false)); + bspline_weight = _pm()._bsplineHandlePosition(this, false); + this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight)); + } + return; + } + + if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) { + // restrict movement to the line joining the nodes + Geom::Point direction = _parent->position() - node_away->position(); + Geom::Point delta = new_pos - _parent->position(); + // project the relative position on the direction line + Geom::Coord direction_length = Geom::L2sq(direction); + Geom::Point new_delta; + if (direction_length == 0) { + // joining line has zero length - any direction is okay, prevent division by zero + new_delta = delta; + } else { + new_delta = (Geom::dot(delta, direction) / direction_length) * direction; + } + setRelativePos(new_delta); + + // move the handle and its opposite the same proportion + if(_pm()._isBSpline()){ + setPosition(_pm()._bsplineHandleReposition(this, false)); + bspline_weight = _pm()._bsplineHandlePosition(this, false); + this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight)); + } + + return; + } + + switch (_parent->type()) { + case NODE_AUTO: + _parent->setType(NODE_SMOOTH, false); + // fall through - auto nodes degrade into smooth nodes + case NODE_SMOOTH: { + // for smooth nodes, we need to rotate the opposite handle + // so that it's collinear with the dragged one, while conserving length. + other->setDirection(new_pos, *_parent); + } break; + case NODE_SYMMETRIC: + // for symmetric nodes, place the other handle on the opposite side + other->setRelativePos(-(new_pos - _parent->position())); + break; + default: break; + } + setPosition(new_pos); + + // move the handle and its opposite the same proportion + if(_pm()._isBSpline()){ + setPosition(_pm()._bsplineHandleReposition(this, false)); + bspline_weight = _pm()._bsplineHandlePosition(this, false); + this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight)); + } + Inkscape::UI::Tools::sp_update_helperpath(_desktop); +} + +void Handle::setPosition(Geom::Point const &p) +{ + ControlPoint::setPosition(p); + _handle_line->set_coords(_parent->position(), position()); + + // update degeneration info and visibility + if (Geom::are_near(position(), _parent->position())) + _degenerate = true; + else _degenerate = false; + + if (_parent->_handles_shown && _parent->visible() && !_degenerate) { + setVisible(true); + } else { + setVisible(false); + } +} + +void Handle::setLength(double len) +{ + if (isDegenerate()) return; + Geom::Point dir = Geom::unit_vector(relativePos()); + setRelativePos(dir * len); +} + +void Handle::retract() +{ + move(_parent->position()); +} + +void Handle::setDirection(Geom::Point const &from, Geom::Point const &to) +{ + setDirection(to - from); +} + +void Handle::setDirection(Geom::Point const &dir) +{ + Geom::Point unitdir = Geom::unit_vector(dir); + setRelativePos(unitdir * length()); +} + +/** + * See also: Node::node_type_to_localized_string(NodeType type) + */ +char const *Handle::handle_type_to_localized_string(NodeType type) +{ + switch(type) { + case NODE_CUSP: + return _("Corner node handle"); + case NODE_SMOOTH: + return _("Smooth node handle"); + case NODE_SYMMETRIC: + return _("Symmetric node handle"); + case NODE_AUTO: + return _("Auto-smooth node handle"); + default: + return ""; + } +} + +bool Handle::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + switch (event->type) + { + case GDK_KEY_PRESS: + + switch (shortcut_key(event->key)) + { + case GDK_KEY_s: + case GDK_KEY_S: + + /* if Shift+S is pressed while hovering over a cusp node handle, + hold the handle in place; otherwise, process normally. + this handle is guaranteed not to be degenerate. */ + + if (held_only_shift(event->key) && _parent->_type == NODE_CUSP) { + + // make opposite handle collinear, + // but preserve length, unless degenerate + if (other()->isDegenerate()) + other()->setRelativePos(-relativePos()); + else + other()->setDirection(-relativePos()); + _parent->setType(NODE_SMOOTH, false); + + // update display + _parent->_pm().update(); + + // update undo history + _parent->_pm()._commit(_("Change node type")); + + return true; + } + break; + + case GDK_KEY_y: + case GDK_KEY_Y: + + /* if Shift+Y is pressed while hovering over a cusp, smooth, or auto node handle, + hold the handle in place; otherwise, process normally. + this handle is guaranteed not to be degenerate. */ + + if (held_only_shift(event->key) && (_parent->_type == NODE_CUSP || + _parent->_type == NODE_SMOOTH || + _parent->_type == NODE_AUTO)) { + + // make opposite handle collinear, and of equal length + other()->setRelativePos(-relativePos()); + _parent->setType(NODE_SYMMETRIC, false); + + // update display + _parent->_pm().update(); + + // update undo history + _parent->_pm()._commit(_("Change node type")); + + return true; + } + break; + } + break; + + case GDK_2BUTTON_PRESS: + + // double-click event to set the handles of a node + // to the position specified by DEFAULT_START_POWER + handle_2button_press(); + break; + } + + return ControlPoint::_eventHandler(event_context, event); +} + +// this function moves the handle and its opposite to the position specified by DEFAULT_START_POWER +void Handle::handle_2button_press(){ + if(_pm()._isBSpline()){ + setPosition(_pm()._bsplineHandleReposition(this, DEFAULT_START_POWER)); + this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), DEFAULT_START_POWER)); + _pm().update(); + } +} + +bool Handle::grabbed(GdkEventMotion *) +{ + _saved_other_pos = other()->position(); + _saved_length = _drag_out ? 0 : length(); + _pm()._handleGrabbed(); + return false; +} + +void Handle::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + Geom::Point parent_pos = _parent->position(); + Geom::Point origin = _last_drag_origin(); + SnapManager &sm = _desktop->namedview->snap_manager; + bool snap = held_shift(*event) ? false : sm.someSnapperMightSnap(); + std::optional<Inkscape::Snapper::SnapConstraint> ctrl_constraint; + + // with Alt, preserve length + if (held_alt(*event)) { + new_pos = parent_pos + Geom::unit_vector(new_pos - parent_pos) * _saved_length; + snap = false; + } + // with Ctrl, constrain to M_PI/rotationsnapsperpi increments from vertical + // and the original position. + if (held_control(*event)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = 2 * prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + + // note: if snapping to the original position is only desired in the original + // direction of the handle, change to Ray instead of Line + Geom::Line original_line(parent_pos, origin); + Geom::Line perp_line(parent_pos, parent_pos + Geom::rot90(origin - parent_pos)); + Geom::Point snap_pos = parent_pos + Geom::constrain_angle( + Geom::Point(0,0), new_pos - parent_pos, snaps, Geom::Point(1,0)); + Geom::Point orig_pos = original_line.pointAt(original_line.nearestTime(new_pos)); + Geom::Point perp_pos = perp_line.pointAt(perp_line.nearestTime(new_pos)); + + Geom::Point result = snap_pos; + ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - snap_pos); + if (Geom::distance(orig_pos, new_pos) < Geom::distance(result, new_pos)) { + result = orig_pos; + ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - orig_pos); + } + if (Geom::distance(perp_pos, new_pos) < Geom::distance(result, new_pos)) { + result = perp_pos; + ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - perp_pos); + } + new_pos = result; + // move the handle and its opposite in X fixed positions depending on parameter "steps with control" + // by default in live BSpline + if(_pm()._isBSpline()){ + setPosition(new_pos); + int steps = _pm()._bsplineGetSteps(); + new_pos=_pm()._bsplineHandleReposition(this,ceilf(_pm()._bsplineHandlePosition(this, false)*steps)/steps); + } + } + + std::vector<Inkscape::SnapCandidatePoint> unselected; + // if the snap adjustment is activated and it is not BSpline + if (snap && !_pm()._isBSpline()) { + ControlPointSelection::Set &nodes = _parent->_selection.allPoints(); + for (auto node : nodes) { + Node *n = static_cast<Node*>(node); + unselected.push_back(n->snapCandidatePoint()); + } + sm.setupIgnoreSelection(_desktop, true, &unselected); + + Node *node_away = _parent->nodeAwayFrom(this); + if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) { + Inkscape::Snapper::SnapConstraint cl(_parent->position(), + _parent->position() - node_away->position()); + Inkscape::SnappedPoint p; + p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), cl); + new_pos = p.getPoint(); + } else if (ctrl_constraint) { + // NOTE: this is subtly wrong. + // We should get all possible constraints and snap along them using + // multipleConstrainedSnaps, instead of first snapping to angle and then to objects + Inkscape::SnappedPoint p; + p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), *ctrl_constraint); + new_pos = p.getPoint(); + } else { + sm.freeSnapReturnByRef(new_pos, SNAPSOURCE_NODE_HANDLE); + } + sm.unSetup(); + } + + + // with Shift, if the node is cusp, rotate the other handle as well + if (_parent->type() == NODE_CUSP && !_drag_out) { + if (held_shift(*event)) { + Geom::Point other_relpos = _saved_other_pos - parent_pos; + other_relpos *= Geom::Rotate(Geom::angle_between(origin - parent_pos, new_pos - parent_pos)); + other()->setRelativePos(other_relpos); + } else { + // restore the position + other()->setPosition(_saved_other_pos); + } + } + // if it is BSpline, but SHIFT or CONTROL are not pressed, fix it in the original position + if(_pm()._isBSpline() && !held_shift(*event) && !held_control(*event)){ + new_pos=_last_drag_origin(); + } + _pm().update(); +} + +void Handle::ungrabbed(GdkEventButton *event) +{ + // hide the handle if it's less than dragtolerance away from the node + // however, never do this for cancelled drag / broken grab + // TODO is this actually a good idea? + if (event) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + Geom::Point dist = _desktop->d2w(_parent->position()) - _desktop->d2w(position()); + if (dist.length() <= drag_tolerance) { + move(_parent->position()); + } + } + + // HACK: If the handle was dragged out, call parent's ungrabbed handler, + // so that transform handles reappear + if (_drag_out) { + _parent->ungrabbed(event); + } + _drag_out = false; + + _pm()._handleUngrabbed(); +} + +bool Handle::clicked(GdkEventButton *event) +{ + _pm()._handleClicked(this, event); + return true; +} + +Handle const *Handle::other() const +{ + return const_cast<Handle *>(this)->other(); +} + +Handle *Handle::other() +{ + if (this == &_parent->_front) { + return &_parent->_back; + } else { + return &_parent->_front; + } +} + +static double snap_increment_degrees() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + return 180.0 / snaps; +} + +Glib::ustring Handle::_getTip(unsigned state) const +{ + /* a trick to mark as BSpline if the node has no strength; + we are going to use it later to show the appropriate messages. + we cannot do it in any different way because the function is constant. */ + Handle *h = const_cast<Handle *>(this); + bool isBSpline = _pm()._isBSpline(); + bool can_shift_rotate = _parent->type() == NODE_CUSP && !other()->isDegenerate(); + Glib::ustring s = C_("Path handle tip", + "node control handle"); // not expected + + if (state_held_alt(state) && !isBSpline) { + if (state_held_control(state)) { + if (state_held_shift(state) && can_shift_rotate) { + s = format_tip(C_("Path handle tip", + "<b>Shift+Ctrl+Alt</b>: " + "preserve length and snap rotation angle to %g° increments, " + "and rotate both handles"), + snap_increment_degrees()); + } + else { + s = format_tip(C_("Path handle tip", + "<b>Ctrl+Alt</b>: " + "preserve length and snap rotation angle to %g° increments"), + snap_increment_degrees()); + } + } + else { + if (state_held_shift(state) && can_shift_rotate) { + s = C_("Path handle tip", + "<b>Shift+Alt</b>: preserve handle length and rotate both handles"); + } + else { + s = C_("Path handle tip", + "<b>Alt</b>: preserve handle length while dragging"); + } + } + } + else { + if (state_held_control(state)) { + if (state_held_shift(state) && can_shift_rotate && !isBSpline) { + s = format_tip(C_("Path handle tip", + "<b>Shift+Ctrl</b>: " + "snap rotation angle to %g° increments, and rotate both handles"), + snap_increment_degrees()); + } + else if (isBSpline) { + s = C_("Path handle tip", + "<b>Ctrl</b>: " + "Snap handle to steps defined in BSpline Live Path Effect"); + } + else { + s = format_tip(C_("Path handle tip", + "<b>Ctrl</b>: " + "snap rotation angle to %g° increments, click to retract"), + snap_increment_degrees()); + } + } + else if (state_held_shift(state) && can_shift_rotate && !isBSpline) { + s = C_("Path handle tip", + "<b>Shift</b>: rotate both handles by the same angle"); + } + else if (state_held_shift(state) && isBSpline) { + s = C_("Path handle tip", + "<b>Shift</b>: move handle"); + } + else { + char const *handletype = handle_type_to_localized_string(_parent->_type); + char const *more; + + if (can_shift_rotate && !isBSpline) { + more = C_("Path handle tip", + "Shift, Ctrl, Alt"); + } + else if (isBSpline) { + more = C_("Path handle tip", + "Ctrl"); + } + else { + more = C_("Path handle tip", + "Ctrl, Alt"); + } + + if (_parent->type() == NODE_CUSP) { + s = format_tip(C_("Path handle tip", + "<b>%s</b>: " + "drag to shape the path" ", " + "hover to lock" ", " + "Shift+S to make smooth" ", " + "Shift+Y to make symmetric" ". " + "(more: %s)"), + handletype, more); + } + else if (_parent->type() == NODE_SMOOTH) { + s = format_tip(C_("Path handle tip", + "<b>%s</b>: " + "drag to shape the path" ", " + "hover to lock" ", " + "Shift+Y to make symmetric" ". " + "(more: %s)"), + handletype, more); + } + else if (_parent->type() == NODE_AUTO) { + s = format_tip(C_("Path handle tip", + "<b>%s</b>: " + "drag to make smooth, " + "hover to lock" ", " + "Shift+Y to make symmetric" ". " + "(more: %s)"), + handletype, more); + } + else if (_parent->type() == NODE_SYMMETRIC) { + s = format_tip(C_("Path handle tip", + "<b>%s</b>: " + "drag to shape the path" ". " + "(more: %s)"), + handletype, more); + } + else if (isBSpline) { + double power = _pm()._bsplineHandlePosition(h); + s = format_tip(C_("Path handle tip", + "<b>BSpline node handle</b> (%.3g power): " + "Shift-drag to move, " + "double-click to reset. " + "(more: %s)"), + power, more); + } + else { + s = C_("Path handle tip", + "<b>unknown node handle</b>"); // not expected + } + } + } + + return (s); +} + +Glib::ustring Handle::_getDragTip(GdkEventMotion */*event*/) const +{ + Geom::Point dist = position() - _last_drag_origin(); + // report angle in mathematical convention + double angle = Geom::angle_between(Geom::Point(-1,0), position() - _parent->position()); + angle += M_PI; // angle is (-M_PI...M_PI] - offset by +pi and scale to 0...360 + angle *= 360.0 / (2 * M_PI); + + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dist[Geom::X], "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dist[Geom::Y], "px"); + Inkscape::Util::Quantity len_q = Inkscape::Util::Quantity(length(), "px"); + Glib::ustring x = x_q.string(_desktop->namedview->display_units); + Glib::ustring y = y_q.string(_desktop->namedview->display_units); + Glib::ustring len = len_q.string(_desktop->namedview->display_units); + Glib::ustring ret = format_tip(C_("Path handle tip", + "Move handle by %s, %s; angle %.2f°, length %s"), x.c_str(), y.c_str(), angle, len.c_str()); + return ret; +} + +Node::Node(NodeSharedData const &data, Geom::Point const &initial_pos) : + SelectableControlPoint(data.desktop, initial_pos, SP_ANCHOR_CENTER, + Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_CUSP, + *data.selection, + node_colors, data.node_group), + _front(data, initial_pos, this), + _back(data, initial_pos, this), + _type(NODE_CUSP), + _handles_shown(false) +{ + _canvas_item_ctrl->set_name("CanvasItemCtrl:Node"); + // NOTE we do not set type here, because the handles are still degenerate +} + +Node const *Node::_next() const +{ + return const_cast<Node*>(this)->_next(); +} + +// NOTE: not using iterators won't make this much quicker because iterators can be 100% inlined. +Node *Node::_next() +{ + NodeList::iterator n = NodeList::get_iterator(this).next(); + if (n) { + return n.ptr(); + } else { + return nullptr; + } +} + +Node const *Node::_prev() const +{ + return const_cast<Node *>(this)->_prev(); +} + +Node *Node::_prev() +{ + NodeList::iterator p = NodeList::get_iterator(this).prev(); + if (p) { + return p.ptr(); + } else { + return nullptr; + } +} + +void Node::move(Geom::Point const &new_pos) +{ + // move handles when the node moves. + Geom::Point old_pos = position(); + Geom::Point delta = new_pos - position(); + + // save the previous nodes strength to apply it again once the node is moved + double nodeWeight = NO_POWER; + double nextNodeWeight = NO_POWER; + double prevNodeWeight = NO_POWER; + Node *n = this; + Node * nextNode = n->nodeToward(n->front()); + Node * prevNode = n->nodeToward(n->back()); + nodeWeight = fmax(_pm()._bsplineHandlePosition(n->front(), false),_pm()._bsplineHandlePosition(n->back(), false)); + if(prevNode){ + prevNodeWeight = _pm()._bsplineHandlePosition(prevNode->front()); + } + if(nextNode){ + nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back()); + } + + // Save original position for post-processing + _unfixed_pos = std::optional<Geom::Point>(position()); + + setPosition(new_pos); + _front.setPosition(_front.position() + delta); + _back.setPosition(_back.position() + delta); + + // move the affected handles. First the node ones, later the adjoining ones. + if(_pm()._isBSpline()){ + _front.setPosition(_pm()._bsplineHandleReposition(this->front(),nodeWeight)); + _back.setPosition(_pm()._bsplineHandleReposition(this->back(),nodeWeight)); + if(prevNode){ + prevNode->front()->setPosition(_pm()._bsplineHandleReposition(prevNode->front(), prevNodeWeight)); + } + if(nextNode){ + nextNode->back()->setPosition(_pm()._bsplineHandleReposition(nextNode->back(), nextNodeWeight)); + } + } +} + +void Node::transform(Geom::Affine const &m) +{ + // save the previous nodes strength to apply it again once the node is moved + double nodeWeight = NO_POWER; + double nextNodeWeight = NO_POWER; + double prevNodeWeight = NO_POWER; + Node *n = this; + Node * nextNode = n->nodeToward(n->front()); + Node * prevNode = n->nodeToward(n->back()); + nodeWeight = _pm()._bsplineHandlePosition(n->front()); + if(prevNode){ + prevNodeWeight = _pm()._bsplineHandlePosition(prevNode->front()); + } + if(nextNode){ + nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back()); + } + + // Save original position for post-processing + _unfixed_pos = std::optional<Geom::Point>(position()); + + setPosition(position() * m); + _front.setPosition(_front.position() * m); + _back.setPosition(_back.position() * m); + + // move the involved handles. First the node ones, later the adjoining ones. + if(_pm()._isBSpline()){ + _front.setPosition(_pm()._bsplineHandleReposition(this->front(), nodeWeight)); + _back.setPosition(_pm()._bsplineHandleReposition(this->back(), nodeWeight)); + if(prevNode){ + prevNode->front()->setPosition(_pm()._bsplineHandleReposition(prevNode->front(), prevNodeWeight)); + } + if(nextNode){ + nextNode->back()->setPosition(_pm()._bsplineHandleReposition(nextNode->back(), nextNodeWeight)); + } + } +} + +Geom::Rect Node::bounds() const +{ + Geom::Rect b(position(), position()); + b.expandTo(_front.position()); + b.expandTo(_back.position()); + return b; +} + +/** + * Affine transforms keep handle invariants for smooth and symmetric nodes, + * but smooth nodes at ends of linear segments and auto nodes need special treatment + * + * Call this function once you have finished called ::move or ::transform on ALL nodes + * that are being transformed in that one operation to avoid problematic bugs. + */ +void Node::fixNeighbors() +{ + if (!_unfixed_pos) + return; + + Geom::Point const new_pos = position(); + + // This method restores handle invariants for neighboring nodes, + // and invariants that are based on positions of those nodes for this one. + + // Fix auto handles + if (_type == NODE_AUTO) _updateAutoHandles(); + if (*_unfixed_pos != new_pos) { + if (_next() && _next()->_type == NODE_AUTO) _next()->_updateAutoHandles(); + if (_prev() && _prev()->_type == NODE_AUTO) _prev()->_updateAutoHandles(); + } + + /* Fix smooth handles at the ends of linear segments. + Rotate the appropriate handle to be collinear with the segment. + If there is a smooth node at the other end of the segment, rotate it too. */ + Handle *handle, *other_handle; + Node *other; + if (_is_line_segment(this, _next())) { + handle = &_back; + other = _next(); + other_handle = &_next()->_front; + } else if (_is_line_segment(_prev(), this)) { + handle = &_front; + other = _prev(); + other_handle = &_prev()->_back; + } else return; + + if (_type == NODE_SMOOTH && !handle->isDegenerate()) { + handle->setDirection(other->position(), new_pos); + } + // also update the handle on the other end of the segment + if (other->_type == NODE_SMOOTH && !other_handle->isDegenerate()) { + other_handle->setDirection(new_pos, other->position()); + } + + _unfixed_pos.reset(); +} + +void Node::_updateAutoHandles() +{ + // Recompute the position of automatic handles. For endnodes, retract both handles. + // (It's only possible to create an end auto node through the XML editor.) + if (isEndNode()) { + _front.retract(); + _back.retract(); + return; + } + + // auto nodes automatically adjust their handles to give + // an appearance of smoothness, no matter what their surroundings are. + Geom::Point vec_next = _next()->position() - position(); + Geom::Point vec_prev = _prev()->position() - position(); + double len_next = vec_next.length(), len_prev = vec_prev.length(); + if (len_next > 0 && len_prev > 0) { + // "dir" is an unit vector perpendicular to the bisector of the angle created + // by the previous node, this auto node and the next node. + Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev); + // Handle lengths are equal to 1/3 of the distance from the adjacent node. + _back.setRelativePos(-dir * (len_prev / 3)); + _front.setRelativePos(dir * (len_next / 3)); + } else { + // If any of the adjacent nodes coincides, retract both handles. + _front.retract(); + _back.retract(); + } +} + +void Node::showHandles(bool v) +{ + _handles_shown = v; + if (!_front.isDegenerate()) { + _front.setVisible(v); + } + if (!_back.isDegenerate()) { + _back.setVisible(v); + } + +} + +void Node::updateHandles() +{ + _handleControlStyling(); + + _front._handleControlStyling(); + _back._handleControlStyling(); +} + + +void Node::setType(NodeType type, bool update_handles) +{ + if (type == NODE_PICK_BEST) { + pickBestType(); + updateState(); // The size of the control might have changed + return; + } + + // if update_handles is true, adjust handle positions to match the node type + // handle degenerate handles appropriately + if (update_handles) { + switch (type) { + case NODE_CUSP: + // nothing to do + break; + case NODE_AUTO: + // auto handles make no sense for endnodes + if (isEndNode()) return; + _updateAutoHandles(); + break; + case NODE_SMOOTH: { + // ignore attempts to make smooth endnodes. + if (isEndNode()) return; + // rotate handles to be collinear + // for degenerate nodes set positions like auto handles + bool prev_line = _is_line_segment(_prev(), this); + bool next_line = _is_line_segment(this, _next()); + if (_type == NODE_SMOOTH) { + // For a node that is already smooth and has a degenerate handle, + // drag out the second handle without changing the direction of the first one. + if (_front.isDegenerate()) { + double dist = Geom::distance(_next()->position(), position()); + _front.setRelativePos(Geom::unit_vector(-_back.relativePos()) * dist / 3); + } + if (_back.isDegenerate()) { + double dist = Geom::distance(_prev()->position(), position()); + _back.setRelativePos(Geom::unit_vector(-_front.relativePos()) * dist / 3); + } + } else if (isDegenerate()) { + _updateAutoHandles(); + } else if (_front.isDegenerate()) { + // if the front handle is degenerate and next path segment is a line, make back collinear; + // otherwise, pull out the other handle to 1/3 of distance to prev. + if (next_line) { + _back.setDirection(*_next(), *this); + } else if (_prev()) { + Geom::Point dir = direction(_back, *this); + _front.setRelativePos(Geom::distance(_prev()->position(), position()) / 3 * dir); + } + } else if (_back.isDegenerate()) { + if (prev_line) { + _front.setDirection(*_prev(), *this); + } else if (_next()) { + Geom::Point dir = direction(_front, *this); + _back.setRelativePos(Geom::distance(_next()->position(), position()) / 3 * dir); + } + } else { + /* both handles are extended. make collinear while keeping length. + first make back collinear with the vector front ---> back, + then make front collinear with back ---> node. + (not back ---> front, because back's position was changed in the first call) */ + _back.setDirection(_front, _back); + _front.setDirection(_back, *this); + } + } break; + case NODE_SYMMETRIC: + if (isEndNode()) return; // symmetric handles make no sense for endnodes + if (isDegenerate()) { + // similar to auto handles but set the same length for both + Geom::Point vec_next = _next()->position() - position(); + Geom::Point vec_prev = _prev()->position() - position(); + double len_next = vec_next.length(), len_prev = vec_prev.length(); + double len = (len_next + len_prev) / 6; // take 1/3 of average + if (len == 0) return; + + Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev); + _back.setRelativePos(-dir * len); + _front.setRelativePos(dir * len); + } else { + // Both handles are extended. Compute average length, use direction from + // back handle to front handle. This also works correctly for degenerates + double len = (_front.length() + _back.length()) / 2; + Geom::Point dir = direction(_back, _front); + _front.setRelativePos(dir * len); + _back.setRelativePos(-dir * len); + } + break; + default: break; + } + // in node type changes, for BSpline traces, we can either maintain them + // with NO_POWER power in border mode, or give them the default power in curve mode. + if(_pm()._isBSpline()){ + double weight = NO_POWER; + if(_pm()._bsplineHandlePosition(this->front()) != NO_POWER ){ + weight = DEFAULT_START_POWER; + } + _front.setPosition(_pm()._bsplineHandleReposition(this->front(), weight)); + _back.setPosition(_pm()._bsplineHandleReposition(this->back(), weight)); + } + } + _type = type; + _setControlType(nodeTypeToCtrlType(_type)); + updateState(); +} + +void Node::pickBestType() +{ + _type = NODE_CUSP; + bool front_degen = _front.isDegenerate(); + bool back_degen = _back.isDegenerate(); + bool both_degen = front_degen && back_degen; + bool neither_degen = !front_degen && !back_degen; + do { + // if both handles are degenerate, do nothing + if (both_degen) break; + // if neither are degenerate, check their respective positions + if (neither_degen) { + // for now do not automatically make nodes symmetric, it can be annoying + /*if (Geom::are_near(front_delta, -back_delta)) { + _type = NODE_SYMMETRIC; + break; + }*/ + if (are_collinear_within_serializing_error(_front.position(), position(), _back.position())) { + _type = NODE_SMOOTH; + break; + } + } + // check whether the handle aligns with the previous line segment. + // we know that if front is degenerate, back isn't, because + // both_degen was false + if (front_degen && _next() && _next()->_back.isDegenerate()) { + if (are_collinear_within_serializing_error(_next()->position(), position(), _back.position())) { + _type = NODE_SMOOTH; + break; + } + } else if (back_degen && _prev() && _prev()->_front.isDegenerate()) { + if (are_collinear_within_serializing_error(_prev()->position(), position(), _front.position())) { + _type = NODE_SMOOTH; + break; + } + } + } while (false); + _setControlType(nodeTypeToCtrlType(_type)); + updateState(); +} + +bool Node::isEndNode() const +{ + return !_prev() || !_next(); +} + +void Node::sink() +{ + _canvas_item_ctrl->set_z_position(0); +} + +NodeType Node::parse_nodetype(char x) +{ + switch (x) { + case 'a': return NODE_AUTO; + case 'c': return NODE_CUSP; + case 's': return NODE_SMOOTH; + case 'z': return NODE_SYMMETRIC; + default: return NODE_PICK_BEST; + } +} + +bool Node::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + int dir = 0; + + switch (event->type) + { + case GDK_SCROLL: + if (event->scroll.direction == GDK_SCROLL_UP) { + dir = 1; + } else if (event->scroll.direction == GDK_SCROLL_DOWN) { + dir = -1; + } else if (event->scroll.direction == GDK_SCROLL_SMOOTH) { + dir = event->scroll.delta_y > 0 ? -1 : 1; + } else { + break; + } + if (held_control(event->scroll)) { + _linearGrow(dir); + } else { + _selection.spatialGrow(this, dir); + } + return true; + case GDK_KEY_PRESS: + switch (shortcut_key(event->key)) + { + case GDK_KEY_Page_Up: + dir = 1; + break; + case GDK_KEY_Page_Down: + dir = -1; + break; + default: goto bail_out; + } + + if (held_control(event->key)) { + _linearGrow(dir); + } else { + _selection.spatialGrow(this, dir); + } + return true; + + default: + break; + } + + bail_out: + return ControlPoint::_eventHandler(event_context, event); +} + +void Node::_linearGrow(int dir) +{ + // Interestingly, we do not need any help from PathManipulator when doing linear grow. + // First handle the trivial case of growing over an unselected node. + if (!selected() && dir > 0) { + _selection.insert(this); + return; + } + + NodeList::iterator this_iter = NodeList::get_iterator(this); + NodeList::iterator fwd = this_iter, rev = this_iter; + double distance_back = 0, distance_front = 0; + + // Linear grow is simple. We find the first unselected nodes in each direction + // and compare the linear distances to them. + if (dir > 0) { + if (!selected()) { + _selection.insert(this); + return; + } + + // find first unselected nodes on both sides + while (fwd && fwd->selected()) { + NodeList::iterator n = fwd.next(); + distance_front += Geom::bezier_length(*fwd, fwd->_front, n->_back, *n); + fwd = n; + if (fwd == this_iter) + // there is no unselected node in this cyclic subpath + return; + } + // do the same for the second direction. Do not check for equality with + // this node, because there is at least one unselected node in the subpath, + // so we are guaranteed to stop. + while (rev && rev->selected()) { + NodeList::iterator p = rev.prev(); + distance_back += Geom::bezier_length(*rev, rev->_back, p->_front, *p); + rev = p; + } + + NodeList::iterator t; // node to select + if (fwd && rev) { + if (distance_front <= distance_back) t = fwd; + else t = rev; + } else { + if (fwd) t = fwd; + if (rev) t = rev; + } + if (t) _selection.insert(t.ptr()); + + // Linear shrink is more complicated. We need to find the farthest selected node. + // This means we have to check the entire subpath. We go in the direction in which + // the distance we traveled is lower. We do this until we run out of nodes (ends of path) + // or the two iterators meet. On the way, we store the last selected node and its distance + // in each direction (if any). At the end, we choose the one that is farther and deselect it. + } else { + // both iterators that store last selected nodes are initially empty + NodeList::iterator last_fwd, last_rev; + double last_distance_back = 0, last_distance_front = 0; + + while (rev || fwd) { + if (fwd && (!rev || distance_front <= distance_back)) { + if (fwd->selected()) { + last_fwd = fwd; + last_distance_front = distance_front; + } + NodeList::iterator n = fwd.next(); + if (n) distance_front += Geom::bezier_length(*fwd, fwd->_front, n->_back, *n); + fwd = n; + } else if (rev && (!fwd || distance_front > distance_back)) { + if (rev->selected()) { + last_rev = rev; + last_distance_back = distance_back; + } + NodeList::iterator p = rev.prev(); + if (p) distance_back += Geom::bezier_length(*rev, rev->_back, p->_front, *p); + rev = p; + } + // Check whether we walked the entire cyclic subpath. + // This is initially true because both iterators start from this node, + // so this check cannot go in the while condition. + // When this happens, we need to check the last node, pointed to by the iterators. + if (fwd && fwd == rev) { + if (!fwd->selected()) break; + NodeList::iterator fwdp = fwd.prev(), revn = rev.next(); + double df = distance_front + Geom::bezier_length(*fwdp, fwdp->_front, fwd->_back, *fwd); + double db = distance_back + Geom::bezier_length(*revn, revn->_back, rev->_front, *rev); + if (df > db) { + last_fwd = fwd; + last_distance_front = df; + } else { + last_rev = rev; + last_distance_back = db; + } + break; + } + } + + NodeList::iterator t; + if (last_fwd && last_rev) { + if (last_distance_front >= last_distance_back) t = last_fwd; + else t = last_rev; + } else { + if (last_fwd) t = last_fwd; + if (last_rev) t = last_rev; + } + if (t) _selection.erase(t.ptr()); + } +} + +void Node::_setState(State state) +{ + // change node size to match type and selection state + _canvas_item_ctrl->set_size_extra(selected() ? 2 : 0); + switch (state) { + // These were used to set "active" and "prelight" flags but the flags weren't being used. + case STATE_NORMAL: + case STATE_MOUSEOVER: + break; + case STATE_CLICKED: + // show the handles when selecting the nodes + if(_pm()._isBSpline()){ + this->front()->setPosition(_pm()._bsplineHandleReposition(this->front())); + this->back()->setPosition(_pm()._bsplineHandleReposition(this->back())); + } + break; + } + SelectableControlPoint::_setState(state); +} + +bool Node::grabbed(GdkEventMotion *event) +{ + if (SelectableControlPoint::grabbed(event)) { + return true; + } + + // Dragging out handles with Shift + drag on a node. + if (!held_shift(*event)) { + return false; + } + + Geom::Point evp = event_point(*event); + Geom::Point rel_evp = evp - _last_click_event_point(); + + // This should work even if dragtolerance is zero and evp coincides with node position. + double angle_next = HUGE_VAL; + double angle_prev = HUGE_VAL; + bool has_degenerate = false; + // determine which handle to drag out based on degeneration and the direction of drag + if (_front.isDegenerate() && _next()) { + Geom::Point next_relpos = _desktop->d2w(_next()->position()) + - _desktop->d2w(position()); + angle_next = fabs(Geom::angle_between(rel_evp, next_relpos)); + has_degenerate = true; + } + if (_back.isDegenerate() && _prev()) { + Geom::Point prev_relpos = _desktop->d2w(_prev()->position()) + - _desktop->d2w(position()); + angle_prev = fabs(Geom::angle_between(rel_evp, prev_relpos)); + has_degenerate = true; + } + if (!has_degenerate) { + return false; + } + + Handle *h = angle_next < angle_prev ? &_front : &_back; + + h->setPosition(_desktop->w2d(evp)); + h->setVisible(true); + h->transferGrab(this, event); + Handle::_drag_out = true; + return true; +} + +void Node::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + // For a note on how snapping is implemented in Inkscape, see snap.h. + SnapManager &sm = _desktop->namedview->snap_manager; + // even if we won't really snap, we might still call the one of the + // constrainedSnap() methods to enforce the constraints, so we need + // to setup the snapmanager anyway; this is also required for someSnapperMightSnap() + sm.setup(_desktop); + + // do not snap when Shift is pressed + bool snap = !held_shift(*event) && sm.someSnapperMightSnap(); + + Inkscape::SnappedPoint sp; + std::vector<Inkscape::SnapCandidatePoint> unselected; + if (snap) { + /* setup + * TODO We are doing this every time a snap happens. It should once be done only once + * per drag - maybe in the grabbed handler? + * TODO Unselected nodes vector must be valid during the snap run, because it is not + * copied. Fix this in snap.h and snap.cpp, then the above. + * TODO Snapping to unselected segments of selected paths doesn't work yet. */ + + // Build the list of unselected nodes. + typedef ControlPointSelection::Set Set; + Set &nodes = _selection.allPoints(); + for (auto node : nodes) { + if (!node->selected()) { + Node *n = static_cast<Node*>(node); + Inkscape::SnapCandidatePoint p(n->position(), n->_snapSourceType(), n->_snapTargetType()); + unselected.push_back(p); + } + } + sm.unSetup(); + sm.setupIgnoreSelection(_desktop, true, &unselected); + } + + // Snap candidate point for free snapping; this will consider snapping tangentially + // and perpendicularly and therefore the origin or direction vector must be set + Inkscape::SnapCandidatePoint scp_free(new_pos, _snapSourceType()); + + std::optional<Geom::Point> front_point, back_point; + Geom::Point origin = _last_drag_origin(); + Geom::Point dummy_cp; + if (_front.isDegenerate()) { // If there is no handle for the path segment towards the next node, then this segment is straight + if (_is_line_segment(this, _next())) { + front_point = _next()->position() - origin; + if (_next()->selected()) { + dummy_cp = _next()->position() - position(); + scp_free.addVector(dummy_cp); + } else { + dummy_cp = _next()->position(); + scp_free.addOrigin(dummy_cp); + } + } + } else { // .. this path segment is curved + front_point = _front.relativePos(); + scp_free.addVector(*front_point); + } + if (_back.isDegenerate()) { // If there is no handle for the path segment towards the previous node, then this segment is straight + if (_is_line_segment(_prev(), this)) { + back_point = _prev()->position() - origin; + if (_prev()->selected()) { + dummy_cp = _prev()->position() - position(); + scp_free.addVector(dummy_cp); + } else { + dummy_cp = _prev()->position(); + scp_free.addOrigin(dummy_cp); + } + } + } else { // .. this path segment is curved + back_point = _back.relativePos(); + scp_free.addVector(*back_point); + } + + if (held_control(*event)) { + // We're about to consider a constrained snap, which is already limited to 1D + // Therefore tangential or perpendicular snapping will not be considered, and therefore + // all calls above to scp_free.addVector() and scp_free.addOrigin() can be neglected + std::vector<Inkscape::Snapper::SnapConstraint> constraints; + if (held_alt(*event)) { // with Ctrl+Alt, constrain to handle lines + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + double min_angle = M_PI / snaps; + + // If the node is cusp and both handles are retracted (degenerate; straight line segment at both sides of the node), then snap to those line segments + if (isDegenerate()) { // Cusp node with both handles retracted + if (front_point) { + constraints.emplace_back(origin, *front_point); + } + if (back_point) { + constraints.emplace_back(origin, *back_point); + } + } + + // For smooth nodes, we will also snap to normals of handle lines. For cusp nodes this would be unintuitive and confusing + // Only snap to the normals when they are further than snap increment away from the second handle constraint + if (_type != NODE_CUSP) { + std::optional<Geom::Point> fperp_point = Geom::rot90(*front_point); + if (fperp_point && (!back_point || + (fabs(Geom::angle_between(*fperp_point, *back_point)) > min_angle && + fabs(Geom::angle_between(*fperp_point, *back_point)) < M_PI - min_angle))) + { + constraints.emplace_back(origin, *fperp_point); + } + + std::optional<Geom::Point> bperp_point = Geom::rot90(*back_point); + if (bperp_point && (!front_point || + (fabs(Geom::angle_between(*bperp_point, *front_point)) > min_angle && + fabs(Geom::angle_between(*bperp_point, *front_point)) < M_PI - min_angle))) + { + constraints.emplace_back(origin, *bperp_point); + } + } + + sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event)); + } else { + // with Ctrl, constrain to axes + constraints.emplace_back(origin, Geom::Point(1, 0)); + constraints.emplace_back(origin, Geom::Point(0, 1)); + sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event)); + } + new_pos = sp.getPoint(); + } else if (snap) { + Inkscape::SnappedPoint sp = sm.freeSnap(scp_free); + new_pos = sp.getPoint(); + } + + sm.unSetup(); + + SelectableControlPoint::dragged(new_pos, event); +} + +bool Node::clicked(GdkEventButton *event) +{ + if(_pm()._nodeClicked(this, event)) + return true; + return SelectableControlPoint::clicked(event); +} + +Inkscape::SnapSourceType Node::_snapSourceType() const +{ + if (_type == NODE_SMOOTH || _type == NODE_AUTO) + return SNAPSOURCE_NODE_SMOOTH; + return SNAPSOURCE_NODE_CUSP; +} +Inkscape::SnapTargetType Node::_snapTargetType() const +{ + if (_type == NODE_SMOOTH || _type == NODE_AUTO) + return SNAPTARGET_NODE_SMOOTH; + return SNAPTARGET_NODE_CUSP; +} + +Inkscape::SnapCandidatePoint Node::snapCandidatePoint() +{ + return SnapCandidatePoint(position(), _snapSourceType(), _snapTargetType()); +} + +Handle *Node::handleToward(Node *to) +{ + if (_next() == to) { + return front(); + } + if (_prev() == to) { + return back(); + } + g_error("Node::handleToward(): second node is not adjacent!"); + return nullptr; +} + +Node *Node::nodeToward(Handle *dir) +{ + if (front() == dir) { + return _next(); + } + if (back() == dir) { + return _prev(); + } + g_error("Node::nodeToward(): handle is not a child of this node!"); + return nullptr; +} + +Handle *Node::handleAwayFrom(Node *to) +{ + if (_next() == to) { + return back(); + } + if (_prev() == to) { + return front(); + } + g_error("Node::handleAwayFrom(): second node is not adjacent!"); + return nullptr; +} + +Node *Node::nodeAwayFrom(Handle *h) +{ + if (front() == h) { + return _prev(); + } + if (back() == h) { + return _next(); + } + g_error("Node::nodeAwayFrom(): handle is not a child of this node!"); + return nullptr; +} + +Glib::ustring Node::_getTip(unsigned state) const +{ + bool isBSpline = _pm()._isBSpline(); + Handle *h = const_cast<Handle *>(&_front); + Glib::ustring s = C_("Path node tip", + "node handle"); // not expected + + if (state_held_shift(state)) { + bool can_drag_out = (_next() && _front.isDegenerate()) || + (_prev() && _back.isDegenerate()); + + if (can_drag_out) { + /*if (state_held_control(state)) { + s = format_tip(C_("Path node tip", + "<b>Shift+Ctrl:</b> drag out a handle and snap its angle " + "to %f° increments"), snap_increment_degrees()); + }*/ + s = C_("Path node tip", + "<b>Shift</b>: drag out a handle, click to toggle selection"); + } + else { + s = C_("Path node tip", + "<b>Shift</b>: click to toggle selection"); + } + } + + else if (state_held_control(state)) { + if (state_held_alt(state)) { + s = C_("Path node tip", + "<b>Ctrl+Alt</b>: move along handle lines, click to delete node"); + } + else { + s = C_("Path node tip", + "<b>Ctrl</b>: move along axes, click to change node type"); + } + } + + else if (state_held_alt(state)) { + s = C_("Path node tip", + "<b>Alt</b>: sculpt nodes"); + } + + else { // No modifiers: assemble tip from node type + char const *nodetype = node_type_to_localized_string(_type); + double power = _pm()._bsplineHandlePosition(h); + + if (_selection.transformHandlesEnabled() && selected()) { + if (_selection.size() == 1) { + if (!isBSpline) { + s = format_tip(C_("Path node tip", + "<b>%s</b>: " + "drag to shape the path" ". " + "(more: Shift, Ctrl, Alt)"), + nodetype); + } + else { + s = format_tip(C_("Path node tip", + "<b>BSpline node</b> (%.3g power): " + "drag to shape the path" ". " + "(more: Shift, Ctrl, Alt)"), + power); + } + } + else { + s = format_tip(C_("Path node tip", + "<b>%s</b>: " + "drag to shape the path" ", " + "click to toggle scale/rotation handles" ". " + "(more: Shift, Ctrl, Alt)"), + nodetype); + } + } + else if (!isBSpline) { + s = format_tip(C_("Path node tip", + "<b>%s</b>: " + "drag to shape the path" ", " + "click to select only this node" ". " + "(more: Shift, Ctrl, Alt)"), + nodetype); + } + else { + s = format_tip(C_("Path node tip", + "<b>BSpline node</b> (%.3g power): " + "drag to shape the path" ", " + "click to select only this node" ". " + "(more: Shift, Ctrl, Alt)"), + power); + } + } + + return (s); +} + +Glib::ustring Node::_getDragTip(GdkEventMotion */*event*/) const +{ + Geom::Point dist = position() - _last_drag_origin(); + + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dist[Geom::X], "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dist[Geom::Y], "px"); + Glib::ustring x = x_q.string(_desktop->namedview->display_units); + Glib::ustring y = y_q.string(_desktop->namedview->display_units); + Glib::ustring ret = format_tip(C_("Path node tip", "Move node by %s, %s"), x.c_str(), y.c_str()); + return ret; +} + +/** + * See also: Handle::handle_type_to_localized_string(NodeType type) + */ +char const *Node::node_type_to_localized_string(NodeType type) +{ + switch (type) { + case NODE_CUSP: + return _("Corner node"); + case NODE_SMOOTH: + return _("Smooth node"); + case NODE_SYMMETRIC: + return _("Symmetric node"); + case NODE_AUTO: + return _("Auto-smooth node"); + default: + return ""; + } +} + +bool Node::_is_line_segment(Node *first, Node *second) +{ + if (!first || !second) return false; + if (first->_next() == second) + return first->_front.isDegenerate() && second->_back.isDegenerate(); + if (second->_next() == first) + return second->_front.isDegenerate() && first->_back.isDegenerate(); + return false; +} + +NodeList::NodeList(SubpathList &splist) + : _list(splist) + , _closed(false) +{ + this->ln_list = this; + this->ln_next = this; + this->ln_prev = this; +} + +NodeList::~NodeList() +{ + clear(); +} + +bool NodeList::empty() +{ + return ln_next == this; +} + +NodeList::size_type NodeList::size() +{ + size_type sz = 0; + for (ListNode *ln = ln_next; ln != this; ln = ln->ln_next) ++sz; + return sz; +} + +bool NodeList::closed() +{ + return _closed; +} + +bool NodeList::degenerate() +{ + return closed() ? empty() : ++begin() == end(); +} + +NodeList::iterator NodeList::before(double t, double *fracpart) +{ + double intpart; + *fracpart = std::modf(t, &intpart); + int index = intpart; + + iterator ret = begin(); + std::advance(ret, index); + return ret; +} + +NodeList::iterator NodeList::before(Geom::PathTime const &pvp) +{ + iterator ret = begin(); + std::advance(ret, pvp.curve_index); + return ret; +} + +NodeList::iterator NodeList::insert(iterator pos, Node *x) +{ + ListNode *ins = pos._node; + x->ln_next = ins; + x->ln_prev = ins->ln_prev; + ins->ln_prev->ln_next = x; + ins->ln_prev = x; + x->ln_list = this; + return iterator(x); +} + +void NodeList::splice(iterator pos, NodeList &list) +{ + splice(pos, list, list.begin(), list.end()); +} + +void NodeList::splice(iterator pos, NodeList &list, iterator i) +{ + NodeList::iterator j = i; + ++j; + splice(pos, list, i, j); +} + +void NodeList::splice(iterator pos, NodeList &/*list*/, iterator first, iterator last) +{ + ListNode *ins_beg = first._node, *ins_end = last._node, *at = pos._node; + for (ListNode *ln = ins_beg; ln != ins_end; ln = ln->ln_next) { + ln->ln_list = this; + } + ins_beg->ln_prev->ln_next = ins_end; + ins_end->ln_prev->ln_next = at; + at->ln_prev->ln_next = ins_beg; + + ListNode *atprev = at->ln_prev; + at->ln_prev = ins_end->ln_prev; + ins_end->ln_prev = ins_beg->ln_prev; + ins_beg->ln_prev = atprev; +} + +void NodeList::shift(int n) +{ + // 1. make the list perfectly cyclic + ln_next->ln_prev = ln_prev; + ln_prev->ln_next = ln_next; + // 2. find new begin + ListNode *new_begin = ln_next; + if (n > 0) { + for (; n > 0; --n) new_begin = new_begin->ln_next; + } else { + for (; n < 0; ++n) new_begin = new_begin->ln_prev; + } + // 3. relink begin to list + ln_next = new_begin; + ln_prev = new_begin->ln_prev; + new_begin->ln_prev->ln_next = this; + new_begin->ln_prev = this; +} + +void NodeList::reverse() +{ + for (ListNode *ln = ln_next; ln != this; ln = ln->ln_prev) { + std::swap(ln->ln_next, ln->ln_prev); + Node *node = static_cast<Node*>(ln); + Geom::Point save_pos = node->front()->position(); + node->front()->setPosition(node->back()->position()); + node->back()->setPosition(save_pos); + } + std::swap(ln_next, ln_prev); +} + +void NodeList::clear() +{ + // ugly but more efficient clearing mechanism + std::vector<ControlPointSelection *> to_clear; + std::vector<std::pair<SelectableControlPoint *, long> > nodes; + long in = -1; + for (iterator i = begin(); i != end(); ++i) { + SelectableControlPoint *rm = static_cast<Node*>(i._node); + if (std::find(to_clear.begin(), to_clear.end(), &rm->_selection) == to_clear.end()) { + to_clear.push_back(&rm->_selection); + ++in; + } + nodes.emplace_back(rm, in); + } + for (size_t i = 0, e = nodes.size(); i != e; ++i) { + to_clear[nodes[i].second]->erase(nodes[i].first, false); + } + std::vector<std::vector<SelectableControlPoint *> > emission; + for (long i = 0, e = to_clear.size(); i != e; ++i) { + emission.emplace_back(); + for (size_t j = 0, f = nodes.size(); j != f; ++j) { + if (nodes[j].second != i) + break; + emission[i].push_back(nodes[j].first); + } + } + + for (size_t i = 0, e = emission.size(); i != e; ++i) { + to_clear[i]->signal_selection_changed.emit(emission[i], false); + } + + for (iterator i = begin(); i != end();) + erase (i++); +} + +NodeList::iterator NodeList::erase(iterator i) +{ + // some gymnastics are required to ensure that the node is valid when deleted; + // otherwise the code that updates handle visibility will break + Node *rm = static_cast<Node*>(i._node); + ListNode *rmnext = rm->ln_next, *rmprev = rm->ln_prev; + ++i; + delete rm; + rmprev->ln_next = rmnext; + rmnext->ln_prev = rmprev; + return i; +} + +// TODO this method is very ugly! +// converting SubpathList to an intrusive list might allow us to get rid of it +void NodeList::kill() +{ + for (SubpathList::iterator i = _list.begin(); i != _list.end(); ++i) { + if (i->get() == this) { + _list.erase(i); + return; + } + } +} + +NodeList &NodeList::get(Node *n) { + return n->nodeList(); +} +NodeList &NodeList::get(iterator const &i) { + return *(i._node->ln_list); +} + + +} // 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/node.h b/src/ui/tool/node.h new file mode 100644 index 0000000..0630e4f --- /dev/null +++ b/src/ui/tool/node.h @@ -0,0 +1,532 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Editable node and associated data structures. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_NODE_H +#define SEEN_UI_TOOL_NODE_H + +#include <iterator> +#include <iosfwd> +#include <stdexcept> +#include <cstddef> +#include <functional> + +#include "ui/tool/selectable-control-point.h" +#include "snapped-point.h" +#include "ui/tool/node-types.h" + +namespace Inkscape { +class CanvasItemGroup; +class CanvasItemCurve; + +namespace UI { +template <typename> class NodeIterator; +} +} + +namespace Inkscape { +namespace UI { + +class PathManipulator; +class MultiPathManipulator; + +class Node; +class Handle; +class NodeList; +class SubpathList; +template <typename> class NodeIterator; + +std::ostream &operator<<(std::ostream &, NodeType); + +/* +template <typename T> +struct ListMember { + T *next; + T *prev; +}; +struct SubpathMember : public ListMember<NodeListMember> { + Subpath *list; +}; +struct SubpathListMember : public ListMember<SubpathListMember> { + SubpathList *list; +}; +*/ + +struct ListNode { + ListNode *ln_next; + ListNode *ln_prev; + NodeList *ln_list; +}; + +struct NodeSharedData { + SPDesktop *desktop; + ControlPointSelection *selection; + Inkscape::CanvasItemGroup *node_group; + Inkscape::CanvasItemGroup *handle_group; + Inkscape::CanvasItemGroup *handle_line_group; +}; + +class Handle : public ControlPoint { +public: + + ~Handle() override; + inline Geom::Point relativePos() const; + inline double length() const; + bool isDegenerate() const { return _degenerate; } // True if the handle is retracted, i.e. has zero length. + + void setVisible(bool) override; + void move(Geom::Point const &p) override; + + void setPosition(Geom::Point const &p) override; + inline void setRelativePos(Geom::Point const &p); + void setLength(double len); + void retract(); + void setDirection(Geom::Point const &from, Geom::Point const &to); + void setDirection(Geom::Point const &dir); + Node *parent() { return _parent; } + Handle *other(); + Handle const *other() const; + + static char const *handle_type_to_localized_string(NodeType type); + +protected: + + Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent); + virtual void handle_2button_press(); + bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override; + void dragged(Geom::Point &new_pos, GdkEventMotion *event) override; + bool grabbed(GdkEventMotion *event) override; + void ungrabbed(GdkEventButton *event) override; + bool clicked(GdkEventButton *event) override; + + Glib::ustring _getTip(unsigned state) const override; + Glib::ustring _getDragTip(GdkEventMotion *event) const override; + bool _hasDragTips() const override { return true; } + +private: + + inline PathManipulator &_pm(); + inline PathManipulator &_pm() const; + Node *_parent; // the handle's lifetime does not extend beyond that of the parent node, + // so a naked pointer is OK and allows setting it during Node's construction + CanvasItemCurve *_handle_line; + bool _degenerate; // True if the handle is retracted, i.e. has zero length. This is used often internally so it makes sense to cache this + + /** + * Control point of a cubic Bezier curve in a path. + * + * Handle keeps the node type invariant only for the opposite handle of the same node. + * Keeping the invariant on node moves is left to the %Node class. + */ + static Geom::Point _saved_other_pos; + + static double _saved_length; + static bool _drag_out; + static ColorSet _handle_colors; + friend class Node; +}; + +class Node : ListNode, public SelectableControlPoint { +public: + + /** + * Curve endpoint in an editable path. + * + * The method move() keeps node type invariants during translations. + */ + Node(NodeSharedData const &data, Geom::Point const &pos); + + Node(Node const &) = delete; + + void move(Geom::Point const &p) override; + void transform(Geom::Affine const &m) override; + void fixNeighbors() override; + Geom::Rect bounds() const override; + + NodeType type() const { return _type; } + + /** + * Sets the node type and optionally restores the invariants associated with the given type. + * @param type The type to set. + * @param update_handles Whether to restore invariants associated with the given type. + * Passing false is useful e.g. when initially creating the path, + * and when making cusp nodes during some node algorithms. + * Pass true when used in response to an UI node type button. + */ + void setType(NodeType type, bool update_handles = true); + + void showHandles(bool v); + + void updateHandles(); + + + /** + * Pick the best type for this node, based on the position of its handles. + * This is what assigns types to nodes created using the pen tool. + */ + void pickBestType(); // automatically determine the type from handle positions + + bool isDegenerate() const { return _front.isDegenerate() && _back.isDegenerate(); } + bool isEndNode() const; + Handle *front() { return &_front; } + Handle *back() { return &_back; } + + /** + * Gets the handle that faces the given adjacent node. + * Will abort with error if the given node is not adjacent. + */ + Handle *handleToward(Node *to); + + /** + * Gets the node in the direction of the given handle. + * Will abort with error if the handle doesn't belong to this node. + */ + Node *nodeToward(Handle *h); + + /** + * Gets the handle that goes in the direction opposite to the given adjacent node. + * Will abort with error if the given node is not adjacent. + */ + Handle *handleAwayFrom(Node *to); + + /** + * Gets the node in the direction opposite to the given handle. + * Will abort with error if the handle doesn't belong to this node. + */ + Node *nodeAwayFrom(Handle *h); + + NodeList &nodeList() { return *(static_cast<ListNode*>(this)->ln_list); } + NodeList &nodeList() const { return *(static_cast<ListNode const*>(this)->ln_list); } + + /** + * Move the node to the bottom of its canvas group. + * Useful for node break, to ensure that the selected nodes are above the unselected ones. + */ + void sink(); + + static NodeType parse_nodetype(char x); + static char const *node_type_to_localized_string(NodeType type); + + // temporarily public + /** Customized event handler to catch scroll events needed for selection grow/shrink. */ + bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override; + + Inkscape::SnapCandidatePoint snapCandidatePoint(); + +protected: + + void dragged(Geom::Point &new_pos, GdkEventMotion *event) override; + bool grabbed(GdkEventMotion *event) override; + bool clicked(GdkEventButton *event) override; + + void _setState(State state) override; + Glib::ustring _getTip(unsigned state) const override; + Glib::ustring _getDragTip(GdkEventMotion *event) const override; + bool _hasDragTips() const override { return true; } + +private: + + void _updateAutoHandles(); + + /** + * Select or deselect a node in this node's subpath based on its path distance from this node. + * @param dir If negative, shrink selection by one node; if positive, grow by one node. + */ + void _linearGrow(int dir); + + Node *_next(); + Node const *_next() const; + Node *_prev(); + Node const *_prev() const; + Inkscape::SnapSourceType _snapSourceType() const; + Inkscape::SnapTargetType _snapTargetType() const; + inline PathManipulator &_pm(); + inline PathManipulator &_pm() const; + + /** Determine whether two nodes are joined by a linear segment. */ + static bool _is_line_segment(Node *first, Node *second); + + // Handles are always present, but are not visible if they coincide with the node + // (are degenerate). A segment that has both handles degenerate is always treated + // as a line segment + Handle _front; ///< Node handle in the backward direction of the path + Handle _back; ///< Node handle in the forward direction of the path + NodeType _type; ///< Type of node - cusp, smooth... + bool _handles_shown; + static ColorSet node_colors; + + // This is used by fixNeighbors to repair smooth nodes after all move + // operations have been completed. If this is empty, no fixing is needed. + std::optional<Geom::Point> _unfixed_pos; + + friend class Handle; + friend class NodeList; + friend class NodeIterator<Node>; + friend class NodeIterator<Node const>; +}; + +/// Iterator for editable nodes +/** Use this class for all operations that require some knowledge about the node's + * neighbors. It is a bidirectional iterator. + * + * Because paths can be cyclic, node iterators have two different ways to + * increment and decrement them. When using ++/--, the end iterator will eventually + * be returned. When using advance()/retreat(), the end iterator will only be returned + * when the path is open. If it's closed, calling advance() will cycle indefinitely. + * This is particularly useful for cases where the adjacency of nodes is more important + * than their sequence order. + * + * When @a i is a node iterator, then: + * - <code>++i</code> moves the iterator to the next node in sequence order; + * - <code>--i</code> moves the iterator to the previous node in sequence order; + * - <code>i.next()</code> returns the next node with wrap-around; + * - <code>i.prev()</code> returns the previous node with wrap-around; + * - <code>i.advance()</code> moves the iterator to the next node with wrap-around; + * - <code>i.retreat()</code> moves the iterator to the previous node with wrap-around. + * + * next() and prev() do not change their iterator. They can return the end iterator + * if the path is open. + * + * Unlike most other iterators, you can check whether you've reached the end of the list + * without having access to the iterator's container. + * Simply use <code>if (i) { ...</code> + * */ +template <typename N> +class NodeIterator + : public boost::bidirectional_iterator_helper<NodeIterator<N>, N, std::ptrdiff_t, + N *, N &> +{ +public: + typedef NodeIterator self; + NodeIterator() + : _node(nullptr) + {} + // default copy, default assign + + self &operator++() { + _node = (_node?_node->ln_next:nullptr); + return *this; + } + self &operator--() { + _node = (_node?_node->ln_prev:nullptr); + return *this; + } + bool operator==(self const &other) const { return _node == other._node; } + N &operator*() const { return *static_cast<N*>(_node); } + inline operator bool() const; // define after NodeList + /// Get a pointer to the underlying node. Equivalent to <code>&*i</code>. + N *get_pointer() const { return static_cast<N*>(_node); } + /// @see get_pointer() + N *ptr() const { return static_cast<N*>(_node); } + + self next() const { + self r(*this); + r.advance(); + return r; + } + self prev() const { + self r(*this); + r.retreat(); + return r; + } + self &advance(); + self &retreat(); +private: + NodeIterator(ListNode const *n) + : _node(const_cast<ListNode*>(n)) + {} + ListNode *_node; + friend class NodeList; +}; + +class NodeList : ListNode, boost::noncopyable { +public: + typedef std::size_t size_type; + typedef Node &reference; + typedef Node const &const_reference; + typedef Node *pointer; + typedef Node const *const_pointer; + typedef Node value_type; + typedef NodeIterator<value_type> iterator; + typedef NodeIterator<value_type const> const_iterator; + + // TODO Lame. Make this private and make SubpathList a factory + /** + * An editable list of nodes representing a subpath. + * + * It can optionally be cyclic to represent a closed path. + * The list has iterators that act like plain node iterators, but can also be used + * to obtain shared pointers to nodes. + */ + NodeList(SubpathList &_list); + + ~NodeList(); + + // no copy or assign + NodeList(NodeList const &) = delete; + void operator=(NodeList const &) = delete; + + // iterators + iterator begin() { return iterator(ln_next); } + iterator end() { return iterator(this); } + const_iterator begin() const { return const_iterator(ln_next); } + const_iterator end() const { return const_iterator(this); } + + // size + bool empty(); + size_type size(); + + // extra node-specific methods + bool closed(); + + /** + * A subpath is degenerate if it has no segments - either one node in an open path + * or no nodes in a closed path. + */ + bool degenerate(); + + void setClosed(bool c) { _closed = c; } + iterator before(double t, double *fracpart = nullptr); + iterator before(Geom::PathTime const &pvp); + const_iterator before(double t, double *fracpart = nullptr) const { + return const_cast<NodeList *>(this)->before(t, fracpart)._node; + } + const_iterator before(Geom::PathTime const &pvp) const { + return const_cast<NodeList *>(this)->before(pvp)._node; + } + + // list operations + + /** insert a node before pos. */ + iterator insert(iterator pos, Node *x); + + template <class InputIterator> + void insert(iterator pos, InputIterator first, InputIterator last) { + for (; first != last; ++first) insert(pos, *first); + } + void splice(iterator pos, NodeList &list); + void splice(iterator pos, NodeList &list, iterator i); + void splice(iterator pos, NodeList &list, iterator first, iterator last); + void reverse(); + void shift(int n); + void push_front(Node *x) { insert(begin(), x); } + void pop_front() { erase(begin()); } + void push_back(Node *x) { insert(end(), x); } + void pop_back() { erase(--end()); } + void clear(); + iterator erase(iterator pos); + iterator erase(iterator first, iterator last) { + NodeList::iterator ret = first; + while (first != last) ret = erase(first++); + return ret; + } + + // member access - undefined results when the list is empty + Node &front() { return *static_cast<Node*>(ln_next); } + Node &back() { return *static_cast<Node*>(ln_prev); } + + // HACK remove this subpath from its path. This will be removed later. + void kill(); + SubpathList &subpathList() { return _list; } + + static iterator get_iterator(Node *n) { return iterator(n); } + static const_iterator get_iterator(Node const *n) { return const_iterator(n); } + static NodeList &get(Node *n); + static NodeList &get(iterator const &i); +private: + + SubpathList &_list; + bool _closed; + + friend class Node; + friend class Handle; // required to access handle and handle line groups + friend class NodeIterator<Node>; + friend class NodeIterator<Node const>; +}; + +/** + * List of node lists. Represents an editable path. + * Editable path composed of one or more subpaths. + */ +class SubpathList : public std::list< std::shared_ptr<NodeList> > { +public: + typedef std::list< std::shared_ptr<NodeList> > list_type; + + SubpathList(PathManipulator &pm) : _path_manipulator(pm) {} + PathManipulator &pm() { return _path_manipulator; } + +private: + list_type _nodelists; + PathManipulator &_path_manipulator; + friend class NodeList; + friend class Node; + friend class Handle; +}; + + + +// define inline Handle funcs after definition of Node +inline Geom::Point Handle::relativePos() const { + return position() - _parent->position(); +} +inline void Handle::setRelativePos(Geom::Point const &p) { + setPosition(_parent->position() + p); +} +inline double Handle::length() const { + return relativePos().length(); +} +inline PathManipulator &Handle::_pm() { + return _parent->_pm(); +} +inline PathManipulator &Handle::_pm() const { + return _parent->_pm(); +} +inline PathManipulator &Node::_pm() { + return nodeList().subpathList().pm(); +} + +inline PathManipulator &Node::_pm() const { + return nodeList().subpathList().pm(); +} + +// definitions for node iterator +template <typename N> +NodeIterator<N>::operator bool() const { + return _node && static_cast<ListNode*>(_node->ln_list) != _node; +} +template <typename N> +NodeIterator<N> &NodeIterator<N>::advance() { + ++(*this); + if (G_UNLIKELY(!*this) && _node->ln_list->closed()) ++(*this); + return *this; +} +template <typename N> +NodeIterator<N> &NodeIterator<N>::retreat() { + --(*this); + if (G_UNLIKELY(!*this) && _node->ln_list->closed()) --(*this); + return *this; +} + +} // 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/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp new file mode 100644 index 0000000..7f30633 --- /dev/null +++ b/src/ui/tool/path-manipulator.cpp @@ -0,0 +1,1804 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Path manipulator - implementation. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/bezier-utils.h> +#include <2geom/path-sink.h> + +#include <utility> + +#include "display/curve.h" +#include "display/control/canvas-item-bpath.h" + +#include "helper/geom.h" + +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-slice.h" +#include "live_effects/lpe-bspline.h" +#include "live_effects/parameter/path.h" + +#include "object/sp-path.h" +#include "style.h" + +#include "ui/icon-names.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/curve-drag-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tools/node-tool.h" + +#include "xml/node-observer.h" + +namespace Inkscape { +namespace UI { + +namespace { +/// Types of path changes that we must react to. +enum PathChange { + PATH_CHANGE_D, + PATH_CHANGE_TRANSFORM +}; + +} // anonymous namespace +const double HANDLE_CUBIC_GAP = 0.001; +const double NO_POWER = 0.0; +const double DEFAULT_START_POWER = 1.0/3.0; + + +/** + * Notifies the path manipulator when something changes the path being edited + * (e.g. undo / redo) + */ +class PathManipulatorObserver : public Inkscape::XML::NodeObserver { +public: + PathManipulatorObserver(PathManipulator *p, Inkscape::XML::Node *node) + : _pm(p) + , _node(node) + , _blocked(false) + { + Inkscape::GC::anchor(_node); + _node->addObserver(*this); + } + + ~PathManipulatorObserver() override { + _node->removeObserver(*this); + Inkscape::GC::release(_node); + } + + void notifyAttributeChanged(Inkscape::XML::Node &/*node*/, GQuark attr, + Util::ptr_shared, Util::ptr_shared) override + { + // do nothing if blocked + if (_blocked) return; + + GQuark path_d = g_quark_from_static_string("d"); + GQuark path_transform = g_quark_from_static_string("transform"); + GQuark lpe_quark = _pm->_lpe_key.empty() ? 0 : g_quark_from_string(_pm->_lpe_key.data()); + + // only react to "d" (path data) and "transform" attribute changes + if (attr == lpe_quark || attr == path_d) { + _pm->_externalChange(PATH_CHANGE_D); + } else if (attr == path_transform) { + _pm->_externalChange(PATH_CHANGE_TRANSFORM); + } + } + + void block() { _blocked = true; } + void unblock() { _blocked = false; } +private: + PathManipulator *_pm; + Inkscape::XML::Node *_node; + bool _blocked; +}; + +void build_segment(Geom::PathBuilder &, Node *, Node *); +PathManipulator::PathManipulator(MultiPathManipulator &mpm, SPObject *path, + Geom::Affine const &et, guint32 outline_color, Glib::ustring lpe_key) + : PointManipulator(mpm._path_data.node_data.desktop, *mpm._path_data.node_data.selection) + , _subpaths(*this) + , _multi_path_manipulator(mpm) + , _path(path) + , _spcurve(new SPCurve()) + , _dragpoint(new CurveDragPoint(*this)) + , /* XML Tree being used here directly while it shouldn't be*/_observer(new PathManipulatorObserver(this, path->getRepr())) + , _edit_transform(et) + , _lpe_key(std::move(lpe_key)) +{ + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path); + SPPath *pathshadow = dynamic_cast<SPPath *>(_path); + if (!lpeobj) { + _i2d_transform = pathshadow->i2dt_affine(); + } else { + _i2d_transform = Geom::identity(); + } + _d2i_transform = _i2d_transform.inverse(); + _dragpoint->setVisible(false); + + _getGeometry(); + + _outline = new Inkscape::CanvasItemBpath(_multi_path_manipulator._path_data.outline_group); + _outline->hide(); + _outline->set_stroke(outline_color); + _outline->set_fill(0x0, SP_WIND_RULE_NONZERO); + + _selection.signal_update.connect( + sigc::bind(sigc::mem_fun(*this, &PathManipulator::update), false)); + _selection.signal_selection_changed.connect( + sigc::mem_fun(*this, &PathManipulator::_selectionChangedM)); + _desktop->signal_zoom_changed.connect( + sigc::hide( sigc::mem_fun(*this, &PathManipulator::_updateOutlineOnZoomChange))); + + _createControlPointsFromGeometry(); + //Define if the path is BSpline on construction + _recalculateIsBSpline(); +} + +PathManipulator::~PathManipulator() +{ + delete _dragpoint; + delete _observer; + delete _outline; + clear(); +} + +/** Handle motion events to update the position of the curve drag point. */ +bool PathManipulator::event(Inkscape::UI::Tools::ToolBase * /*event_context*/, GdkEvent *event) +{ + if (empty()) return false; + + switch (event->type) + { + case GDK_MOTION_NOTIFY: + _updateDragPoint(event_point(event->motion)); + break; + default: + break; + } + return false; +} + +/** Check whether the manipulator has any nodes. */ +bool PathManipulator::empty() { + return !_path || _subpaths.empty(); +} + +/** Update the display and the outline of the path. + * \param alert_LPE if true, alerts an applied LPE to what the path is going to be changed to, so it can adjust its parameters for nicer user interfacing + */ +void PathManipulator::update(bool alert_LPE) +{ + _createGeometryFromControlPoints(alert_LPE); +} + +/** Store the changes to the path in XML. */ +void PathManipulator::writeXML() +{ + if (!_live_outline) + _updateOutline(); + + _setGeometry(); + if (!_path) { + return; + } + + XML::Node *node = _getXMLNode(); + if (!node) { + return; + } + + _observer->block(); + if (!empty()) { + _path->updateRepr(); + node->setAttribute(_nodetypesKey(), _createTypeString()); + } else { + // this manipulator will have to be destroyed right after this call + node->removeObserver(*_observer); + _path->deleteObject(true, true); + _path = nullptr; + } + _observer->unblock(); +} + +/** Remove all nodes from the path. */ +void PathManipulator::clear() +{ + // no longer necessary since nodes remove themselves from selection on destruction + //_removeNodesFromSelection(); + _subpaths.clear(); +} + +/** Select all nodes in subpaths that have something selected. */ +void PathManipulator::selectSubpaths() +{ + for (auto & _subpath : _subpaths) { + NodeList::iterator sp_start = _subpath->begin(), sp_end = _subpath->end(); + for (NodeList::iterator j = sp_start; j != sp_end; ++j) { + if (j->selected()) { + // if at least one of the nodes from this subpath is selected, + // select all nodes from this subpath + for (NodeList::iterator ins = sp_start; ins != sp_end; ++ins) + _selection.insert(ins.ptr()); + continue; + } + } + } +} + +/** Invert selection in the selected subpaths. */ +void PathManipulator::invertSelectionInSubpaths() +{ + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if (j->selected()) { + // found selected node - invert selection in this subpath + for (NodeList::iterator k = _subpath->begin(); k != _subpath->end(); ++k) { + if (k->selected()) _selection.erase(k.ptr()); + else _selection.insert(k.ptr()); + } + // next subpath + break; + } + } + } +} + +/** Insert a new node in the middle of each selected segment. */ +void PathManipulator::insertNodes() +{ + if (_selection.size() < 2) return; + + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + NodeList::iterator k = j.next(); + if (k && j->selected() && k->selected()) { + j = subdivideSegment(j, 0.5); + _selection.insert(j.ptr()); + } + } + } +} + +void PathManipulator::insertNode(Geom::Point pt) +{ + Geom::Coord dist = _updateDragPoint(pt); + if (dist < 1e-5) { // 1e-6 is too small, as observed occasionally when inserting a node at a snapped intersection of paths + insertNode(_dragpoint->getIterator(), _dragpoint->getTimeValue(), true); + } +} + +void PathManipulator::insertNode(NodeList::iterator first, double t, bool take_selection) +{ + NodeList::iterator inserted = subdivideSegment(first, t); + if (take_selection) { + _selection.clear(); + } + _selection.insert(inserted.ptr()); + + update(true); + _commit(_("Add node")); +} + + +static void +add_or_replace_if_extremum(std::vector< std::pair<NodeList::iterator, double> > &vec, + double & extrvalue, double testvalue, NodeList::iterator const& node, double t) +{ + if (testvalue > extrvalue) { + // replace all extreme nodes with the new one + vec.clear(); + vec.emplace_back( node, t ); + extrvalue = testvalue; + } else if ( Geom::are_near(testvalue, extrvalue) ) { + // very rare but: extremum node at the same extreme value!!! so add it to the list + vec.emplace_back( node, t ); + } +} + +/** Insert a new node at the extremum of the selected segments. */ +void PathManipulator::insertNodeAtExtremum(ExtremumType extremum) +{ + if (_selection.size() < 2) return; + + double sign = (extremum == EXTR_MIN_X || extremum == EXTR_MIN_Y) ? -1. : 1.; + Geom::Dim2 dim = (extremum == EXTR_MIN_X || extremum == EXTR_MAX_X) ? Geom::X : Geom::Y; + + for (auto & _subpath : _subpaths) { + Geom::Coord extrvalue = - Geom::infinity(); + std::vector< std::pair<NodeList::iterator, double> > extremum_vector; + + for (NodeList::iterator first = _subpath->begin(); first != _subpath->end(); ++first) { + NodeList::iterator second = first.next(); + if (second && first->selected() && second->selected()) { + add_or_replace_if_extremum(extremum_vector, extrvalue, sign * first->position()[dim], first, 0.); + add_or_replace_if_extremum(extremum_vector, extrvalue, sign * second->position()[dim], first, 1.); + if (first->front()->isDegenerate() && second->back()->isDegenerate()) { + // a line segment has is extrema at the start and end, no node should be added + continue; + } else { + // build 1D cubic bezier curve + Geom::Bezier temp1d(first->position()[dim], first->front()->position()[dim], + second->back()->position()[dim], second->position()[dim]); + // and determine extremum + Geom::Bezier deriv1d = derivative(temp1d); + std::vector<double> rs = deriv1d.roots(); + for (double & r : rs) { + add_or_replace_if_extremum(extremum_vector, extrvalue, sign * temp1d.valueAt(r), first, r); + } + } + } + } + + for (auto & i : extremum_vector) { + // don't insert node at the start or end of a segment, i.e. round values for extr_t + double t = i.second; + if ( !Geom::are_near(t - std::floor(t+0.5),0.) ) // std::floor(t+0.5) is another way of writing round(t) + { + _selection.insert( subdivideSegment(i.first, t).ptr() ); + } + } + } +} + + +/** Insert new nodes exactly at the positions of selected nodes while preserving shape. + * This is equivalent to breaking, except that it doesn't split into subpaths. */ +void PathManipulator::duplicateNodes() +{ + if (_selection.empty()) return; + + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if (j->selected()) { + NodeList::iterator k = j.next(); + Node *n = new Node(_multi_path_manipulator._path_data.node_data, *j); + + if (k) { + // Move the new node to the bottom of the Z-order. This way you can drag all + // nodes that were selected before this operation without deselecting + // everything because there is a new node above. + n->sink(); + } + + n->front()->setPosition(*j->front()); + j->front()->retract(); + j->setType(NODE_CUSP, false); + _subpath->insert(k, n); + + if (k) { + // We need to manually call the selection change callback to refresh + // the handle display correctly. + // This call changes num_selected, but we call this once for a selected node + // and once for an unselected node, so in the end the number stays correct. + _selectionChanged(j.ptr(), true); + _selectionChanged(n, false); + } else { + // select the new end node instead of the node just before it + _selection.erase(j.ptr()); + _selection.insert(n); + break; // this was the end node, nothing more to do + } + } + } + } +} + +/** + * Copy the selected nodes using the PathBuilder + * + * @param builder[out] Selected nodes will be appended to this Path builder + * in pixel coordinates with all transforms applied. + */ +void PathManipulator::copySelectedPath(Geom::PathBuilder *builder) +{ + // Ignore LivePathEffect paths + if (!_path || dynamic_cast<LivePathEffectObject *>(_path)) + return; + // Rebuild the selected parts of each subpath + for (auto &subpath : _subpaths) { + Node *prev = nullptr; + bool is_last_node = false; + for (auto &node : *subpath) { + if (node.selected()) { + // The node positions are already transformed + if (!builder->inPath() || !prev) { + builder->moveTo(node.position()); + } else { + build_segment(*builder, prev, &node); + } + prev = &node; + is_last_node = true; + } else { + is_last_node = false; + } + } + + // Complete the path, especially for closed sub paths where the last node is selected + if (subpath->closed() && is_last_node) { + if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()) { + build_segment(*builder, prev, subpath->begin().ptr()); + } + // if that segment is linear, we just call closePath(). + builder->closePath(); + } + } + builder->flush(); +} + +/** Replace contiguous selections of nodes in each subpath with one node. */ +void PathManipulator::weldNodes(NodeList::iterator preserve_pos) +{ + if (_selection.size() < 2) return; + hideDragPoint(); + + bool pos_valid = preserve_pos; + for (auto sp : _subpaths) { + unsigned num_selected = 0, num_unselected = 0; + for (auto & j : *sp) { + if (j.selected()) ++num_selected; + else ++num_unselected; + } + if (num_selected < 2) continue; + if (num_unselected == 0) { + // if all nodes in a subpath are selected, the operation doesn't make much sense + continue; + } + + // Start from unselected node in closed paths, so that we don't start in the middle + // of a selection + NodeList::iterator sel_beg = sp->begin(), sel_end; + if (sp->closed()) { + while (sel_beg->selected()) ++sel_beg; + } + + // Work loop + while (num_selected > 0) { + // Find selected node + while (sel_beg && !sel_beg->selected()) sel_beg = sel_beg.next(); + if (!sel_beg) throw std::logic_error("Join nodes: end of open path reached, " + "but there are still nodes to process!"); + + // note: this is initialized to zero, because the loop below counts sel_beg as well + // the loop conditions are simpler that way + unsigned num_points = 0; + bool use_pos = false; + Geom::Point back_pos, front_pos; + back_pos = *sel_beg->back(); + + for (sel_end = sel_beg; sel_end && sel_end->selected(); sel_end = sel_end.next()) { + ++num_points; + front_pos = *sel_end->front(); + if (pos_valid && sel_end == preserve_pos) use_pos = true; + } + if (num_points > 1) { + Geom::Point joined_pos; + if (use_pos) { + joined_pos = preserve_pos->position(); + pos_valid = false; + } else { + joined_pos = Geom::middle_point(back_pos, front_pos); + } + sel_beg->setType(NODE_CUSP, false); + sel_beg->move(joined_pos); + // do not move handles if they aren't degenerate + if (!sel_beg->back()->isDegenerate()) { + sel_beg->back()->setPosition(back_pos); + } + if (!sel_end.prev()->front()->isDegenerate()) { + sel_beg->front()->setPosition(front_pos); + } + sel_beg = sel_beg.next(); + while (sel_beg != sel_end) { + NodeList::iterator next = sel_beg.next(); + sp->erase(sel_beg); + sel_beg = next; + --num_selected; + } + } + --num_selected; // for the joined node or single selected node + } + } +} + +/** Remove nodes in the middle of selected segments. */ +void PathManipulator::weldSegments() +{ + if (_selection.size() < 2) return; + hideDragPoint(); + + for (auto sp : _subpaths) { + unsigned num_selected = 0, num_unselected = 0; + for (auto & j : *sp) { + if (j.selected()) ++num_selected; + else ++num_unselected; + } + + // if 2 or fewer nodes are selected, there can't be any middle points to remove. + if (num_selected <= 2) continue; + + if (num_unselected == 0 && sp->closed()) { + // if all nodes in a closed subpath are selected, the operation doesn't make much sense + continue; + } + + // Start from unselected node in closed paths, so that we don't start in the middle + // of a selection + NodeList::iterator sel_beg = sp->begin(), sel_end; + if (sp->closed()) { + while (sel_beg->selected()) ++sel_beg; + } + + // Work loop + while (num_selected > 0) { + // Find selected node + while (sel_beg && !sel_beg->selected()) sel_beg = sel_beg.next(); + if (!sel_beg) throw std::logic_error("Join nodes: end of open path reached, " + "but there are still nodes to process!"); + + // note: this is initialized to zero, because the loop below counts sel_beg as well + // the loop conditions are simpler that way + unsigned num_points = 0; + + // find the end of selected segment + for (sel_end = sel_beg; sel_end && sel_end->selected(); sel_end = sel_end.next()) { + ++num_points; + } + if (num_points > 2) { + // remove nodes in the middle + // TODO: fit bezier to the former shape + sel_beg = sel_beg.next(); + while (sel_beg != sel_end.prev()) { + NodeList::iterator next = sel_beg.next(); + sp->erase(sel_beg); + sel_beg = next; + } + } + sel_beg = sel_end; + // decrease num_selected by the number of points processed + num_selected -= num_points; + } + } +} + +/** Break the subpath at selected nodes. It also works for single node closed paths. */ +void PathManipulator::breakNodes() +{ + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + SubpathPtr sp = *i; + NodeList::iterator cur = sp->begin(), end = sp->end(); + if (!sp->closed()) { + // Each open path must have at least two nodes so no checks are required. + // For 2-node open paths, cur == end + ++cur; + --end; + } + for (; cur != end; ++cur) { + if (!cur->selected()) continue; + SubpathPtr ins; + bool becomes_open = false; + + if (sp->closed()) { + // Move the node to break at to the beginning of path + if (cur != sp->begin()) + sp->splice(sp->begin(), *sp, cur, sp->end()); + sp->setClosed(false); + ins = sp; + becomes_open = true; + } else { + SubpathPtr new_sp(new NodeList(_subpaths)); + new_sp->splice(new_sp->end(), *sp, sp->begin(), cur); + _subpaths.insert(i, new_sp); + ins = new_sp; + } + + Node *n = new Node(_multi_path_manipulator._path_data.node_data, cur->position()); + ins->insert(ins->end(), n); + cur->setType(NODE_CUSP, false); + n->back()->setRelativePos(cur->back()->relativePos()); + cur->back()->retract(); + n->sink(); + + if (becomes_open) { + cur = sp->begin(); // this will be increased to ++sp->begin() + end = --sp->end(); + } + } + } +} + +/** Delete selected nodes in the path, optionally substituting deleted segments with bezier curves + * in a way that attempts to preserve the original shape of the curve. */ +void PathManipulator::deleteNodes(bool keep_shape) +{ + if (_selection.empty()) return; + hideDragPoint(); + + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) { + SubpathPtr sp = *i; + + // If there are less than 2 unselected nodes in an open subpath or no unselected nodes + // in a closed one, delete entire subpath. + unsigned num_unselected = 0, num_selected = 0; + for (auto & j : *sp) { + if (j.selected()) ++num_selected; + else ++num_unselected; + } + if (num_selected == 0) { + ++i; + continue; + } + if (sp->closed() ? (num_unselected < 1) : (num_unselected < 2)) { + _subpaths.erase(i++); + continue; + } + + // In closed paths, start from an unselected node - otherwise we might start in the middle + // of a selected stretch and the resulting bezier fit would be suboptimal + NodeList::iterator sel_beg = sp->begin(), sel_end; + if (sp->closed()) { + while (sel_beg->selected()) ++sel_beg; + } + sel_end = sel_beg; + + while (num_selected > 0) { + while (sel_beg && !sel_beg->selected()) { + sel_beg = sel_beg.next(); + } + sel_end = sel_beg; + + while (sel_end && sel_end->selected()) { + sel_end = sel_end.next(); + } + + num_selected -= _deleteStretch(sel_beg, sel_end, keep_shape); + sel_beg = sel_end; + } + ++i; + } +} + +/** + * Delete nodes between the two iterators. + * The given range can cross the beginning of the subpath in closed subpaths. + * @param start Beginning of the range to delete + * @param end End of the range + * @param keep_shape Whether to fit the handles at surrounding nodes to approximate + * the shape before deletion + * @return Number of deleted nodes + */ +unsigned PathManipulator::_deleteStretch(NodeList::iterator start, NodeList::iterator end, bool keep_shape) +{ + unsigned const samples_per_segment = 10; + double const t_step = 1.0 / samples_per_segment; + + unsigned del_len = 0; + for (NodeList::iterator i = start; i != end; i = i.next()) { + ++del_len; + } + if (del_len == 0) return 0; + + // set surrounding node types to cusp if: + // 1. keep_shape is on, or + // 2. we are deleting at the end or beginning of an open path + if ((keep_shape || !end) && start.prev()) start.prev()->setType(NODE_CUSP, false); + if ((keep_shape || !start.prev()) && end) end->setType(NODE_CUSP, false); + + if (keep_shape && start.prev() && end) { + unsigned num_samples = (del_len + 1) * samples_per_segment + 1; + Geom::Point *bezier_data = new Geom::Point[num_samples]; + Geom::Point result[4]; + unsigned seg = 0; + + for (NodeList::iterator cur = start.prev(); cur != end; cur = cur.next()) { + Geom::CubicBezier bc(*cur, *cur->front(), *cur.next(), *cur.next()->back()); + for (unsigned s = 0; s < samples_per_segment; ++s) { + bezier_data[seg * samples_per_segment + s] = bc.pointAt(t_step * s); + } + ++seg; + } + // Fill last point + bezier_data[num_samples - 1] = end->position(); + // Compute replacement bezier curve + // TODO the fitting algorithm sucks - rewrite it to be awesome + bezier_fit_cubic(result, bezier_data, num_samples, 0.5); + delete[] bezier_data; + + start.prev()->front()->setPosition(result[1]); + end->back()->setPosition(result[2]); + } + + // We can't use nl->erase(start, end), because it would break when the stretch + // crosses the beginning of a closed subpath + NodeList &nl = start->nodeList(); + while (start != end) { + NodeList::iterator next = start.next(); + nl.erase(start); + start = next; + } + // if we are removing, we readjust the handlers + if(_isBSpline()){ + if(start.prev()){ + double bspline_weight = _bsplineHandlePosition(start.prev()->back(), false); + start.prev()->front()->setPosition(_bsplineHandleReposition(start.prev()->front(), bspline_weight)); + } + if(end){ + double bspline_weight = _bsplineHandlePosition(end->front(), false); + end->back()->setPosition(_bsplineHandleReposition(end->back(),bspline_weight)); + } + } + + return del_len; +} + +/** Removes selected segments */ +void PathManipulator::deleteSegments() +{ + if (_selection.empty()) return; + hideDragPoint(); + + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) { + SubpathPtr sp = *i; + bool has_unselected = false; + unsigned num_selected = 0; + for (auto & j : *sp) { + if (j.selected()) { + ++num_selected; + } else { + has_unselected = true; + } + } + if (!has_unselected) { + _subpaths.erase(i++); + continue; + } + + NodeList::iterator sel_beg = sp->begin(); + if (sp->closed()) { + while (sel_beg && sel_beg->selected()) ++sel_beg; + } + while (num_selected > 0) { + if (!sel_beg->selected()) { + sel_beg = sel_beg.next(); + continue; + } + NodeList::iterator sel_end = sel_beg; + unsigned num_points = 0; + while (sel_end && sel_end->selected()) { + sel_end = sel_end.next(); + ++num_points; + } + if (num_points >= 2) { + // Retract end handles + sel_end.prev()->setType(NODE_CUSP, false); + sel_end.prev()->back()->retract(); + sel_beg->setType(NODE_CUSP, false); + sel_beg->front()->retract(); + if (sp->closed()) { + // In closed paths, relocate the beginning of the path to the last selected + // node and then unclose it. Remove the nodes from the first selected node + // to the new end of path. + if (sel_end.prev() != sp->begin()) + sp->splice(sp->begin(), *sp, sel_end.prev(), sp->end()); + sp->setClosed(false); + sp->erase(sel_beg.next(), sp->end()); + } else { + // for open paths: + // 1. At end or beginning, delete including the node on the end or beginning + // 2. In the middle, delete only inner nodes + if (sel_beg == sp->begin()) { + sp->erase(sp->begin(), sel_end.prev()); + } else if (sel_end == sp->end()) { + sp->erase(sel_beg.next(), sp->end()); + } else { + SubpathPtr new_sp(new NodeList(_subpaths)); + new_sp->splice(new_sp->end(), *sp, sp->begin(), sel_beg.next()); + _subpaths.insert(i, new_sp); + if (sel_end.prev()) + sp->erase(sp->begin(), sel_end.prev()); + } + } + } + sel_beg = sel_end; + num_selected -= num_points; + } + ++i; + } +} + +/** Reverse subpaths of the path. + * @param selected_only If true, only paths that have at least one selected node + * will be reversed. Otherwise all subpaths will be reversed. */ +void PathManipulator::reverseSubpaths(bool selected_only) +{ + for (auto & _subpath : _subpaths) { + if (selected_only) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if (j->selected()) { + _subpath->reverse(); + break; // continue with the next subpath + } + } + } else { + _subpath->reverse(); + } + } +} + +/** Make selected segments curves / lines. */ +void PathManipulator::setSegmentType(SegmentType type) +{ + if (_selection.empty()) return; + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + NodeList::iterator k = j.next(); + if (!(k && j->selected() && k->selected())) continue; + switch (type) { + case SEGMENT_STRAIGHT: + if (j->front()->isDegenerate() && k->back()->isDegenerate()) + break; + j->front()->move(*j); + k->back()->move(*k); + break; + case SEGMENT_CUBIC_BEZIER: + if (!j->front()->isDegenerate() || !k->back()->isDegenerate()) + break; + // move both handles to 1/3 of the line + j->front()->move(j->position() + (k->position() - j->position()) / 3); + k->back()->move(k->position() + (j->position() - k->position()) / 3); + break; + } + } + } +} + +void PathManipulator::scaleHandle(Node *n, int which, int dir, bool pixel) +{ + if (n->type() == NODE_SYMMETRIC || n->type() == NODE_AUTO) { + n->setType(NODE_SMOOTH); + } + Handle *h = _chooseHandle(n, which); + double length_change; + + if (pixel) { + 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; + } + + Geom::Point relpos; + if (h->isDegenerate()) { + if (dir < 0) return; + Node *nh = n->nodeToward(h); + if (!nh) return; + relpos = Geom::unit_vector(nh->position() - n->position()) * length_change; + } else { + relpos = h->relativePos(); + double rellen = relpos.length(); + relpos *= ((rellen + length_change) / rellen); + } + h->setRelativePos(relpos); + update(); + gchar const *key = which < 0 ? "handle:scale:left" : "handle:scale:right"; + _commit(_("Scale handle"), key); +} + +void PathManipulator::rotateHandle(Node *n, int which, int dir, bool pixel) +{ + if (n->type() != NODE_CUSP) { + n->setType(NODE_CUSP); + } + Handle *h = _chooseHandle(n, which); + if (h->isDegenerate()) return; + + double angle; + if (pixel) { + // Rotate by "one pixel" + angle = atan2(1.0 / _desktop->current_zoom(), h->length()) * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + angle = M_PI * dir / snaps; + } + + h->setRelativePos(h->relativePos() * Geom::Rotate(angle)); + update(); + gchar const *key = which < 0 ? "handle:rotate:left" : "handle:rotate:right"; + _commit(_("Rotate handle"), key); +} + +Handle *PathManipulator::_chooseHandle(Node *n, int which) +{ + NodeList::iterator i = NodeList::get_iterator(n); + Node *prev = i.prev().ptr(); + Node *next = i.next().ptr(); + + // on an endnode, the remaining handle automatically wins + if (!next) return n->back(); + if (!prev) return n->front(); + + // compare X coord offline segments + Geom::Point npos = next->position(); + Geom::Point ppos = prev->position(); + if (which < 0) { + // pick left handle. + // we just swap the handles and pick the right handle below. + std::swap(npos, ppos); + } + + if (npos[Geom::X] >= ppos[Geom::X]) { + return n->front(); + } else { + return n->back(); + } +} + +/** Set the visibility of handles. */ +void PathManipulator::showHandles(bool show) +{ + if (show == _show_handles) return; + if (show) { + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if (!j->selected()) continue; + j->showHandles(true); + if (j.prev()) j.prev()->showHandles(true); + if (j.next()) j.next()->showHandles(true); + } + } + } else { + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + j.showHandles(false); + } + } + } + _show_handles = show; +} + +/** Set the visibility of outline. */ +void PathManipulator::showOutline(bool show) +{ + if (show == _show_outline) return; + _show_outline = show; + _updateOutline(); +} + +void PathManipulator::showPathDirection(bool show) +{ + if (show == _show_path_direction) return; + _show_path_direction = show; + _updateOutline(); +} + +void PathManipulator::setLiveOutline(bool set) +{ + _live_outline = set; +} + +void PathManipulator::setLiveObjects(bool set) +{ + _live_objects = set; +} + +void PathManipulator::updateHandles() +{ + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + j.updateHandles(); + } + } +} + +void PathManipulator::setControlsTransform(Geom::Affine const &tnew) +{ + Geom::Affine delta = _i2d_transform.inverse() * _edit_transform.inverse() * tnew * _i2d_transform; + _edit_transform = tnew; + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + j.transform(delta); + } + } + _createGeometryFromControlPoints(); +} + +/** Hide the curve drag point until the next motion event. + * This should be called at the beginning of every method that can delete nodes. + * Otherwise the invalidated iterator in the dragpoint can cause crashes. */ +void PathManipulator::hideDragPoint() +{ + _dragpoint->setVisible(false); + _dragpoint->setIterator(NodeList::iterator()); +} + +/** Insert a node in the segment beginning with the supplied iterator, + * at the given time value */ +NodeList::iterator PathManipulator::subdivideSegment(NodeList::iterator first, double t) +{ + if (!first) throw std::invalid_argument("Subdivide after invalid iterator"); + NodeList &list = NodeList::get(first); + NodeList::iterator second = first.next(); + if (!second) throw std::invalid_argument("Subdivide after last node in open path"); + if (first->type() == NODE_SYMMETRIC) + first->setType(NODE_SMOOTH, false); + if (second->type() == NODE_SYMMETRIC) + second->setType(NODE_SMOOTH, false); + + // We need to insert the segment after 'first'. We can't simply use 'second' + // as the point of insertion, because when 'first' is the last node of closed path, + // the new node will be inserted as the first node instead. + NodeList::iterator insert_at = first; + ++insert_at; + + NodeList::iterator inserted; + if (first->front()->isDegenerate() && second->back()->isDegenerate()) { + // for a line segment, insert a cusp node + Node *n = new Node(_multi_path_manipulator._path_data.node_data, + Geom::lerp(t, first->position(), second->position())); + n->setType(NODE_CUSP, false); + inserted = list.insert(insert_at, n); + } else { + // build bezier curve and subdivide + Geom::CubicBezier temp(first->position(), first->front()->position(), + second->back()->position(), second->position()); + std::pair<Geom::CubicBezier, Geom::CubicBezier> div = temp.subdivide(t); + std::vector<Geom::Point> seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints(); + + // set new handle positions + Node *n = new Node(_multi_path_manipulator._path_data.node_data, seg2[0]); + if(!_isBSpline()){ + n->back()->setPosition(seg1[2]); + n->front()->setPosition(seg2[1]); + n->setType(NODE_SMOOTH, false); + } else { + Geom::D2< Geom::SBasis > sbasis_inside_nodes; + std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve()); + if(second->back()->isDegenerate()){ + line_inside_nodes->moveto(n->position()); + line_inside_nodes->lineto(second->position()); + sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis(); + Geom::Point next = sbasis_inside_nodes.valueAt(DEFAULT_START_POWER); + next = Geom::Point(next[Geom::X] + HANDLE_CUBIC_GAP,next[Geom::Y] + HANDLE_CUBIC_GAP); + line_inside_nodes->reset(); + n->front()->setPosition(next); + }else{ + n->front()->setPosition(seg2[1]); + } + if(first->front()->isDegenerate()){ + line_inside_nodes->moveto(n->position()); + line_inside_nodes->lineto(first->position()); + sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis(); + Geom::Point previous = sbasis_inside_nodes.valueAt(DEFAULT_START_POWER); + previous = Geom::Point(previous[Geom::X] + HANDLE_CUBIC_GAP,previous[Geom::Y] + HANDLE_CUBIC_GAP); + n->back()->setPosition(previous); + }else{ + n->back()->setPosition(seg1[2]); + } + n->setType(NODE_CUSP, false); + } + inserted = list.insert(insert_at, n); + + first->front()->move(seg1[1]); + second->back()->move(seg2[2]); + } + return inserted; +} + +/** Find the node that is closest/farthest from the origin + * @param origin Point of reference + * @param search_selected Consider selected nodes + * @param search_unselected Consider unselected nodes + * @param closest If true, return closest node, if false, return farthest + * @return The matching node, or an empty iterator if none found + */ +NodeList::iterator PathManipulator::extremeNode(NodeList::iterator origin, bool search_selected, + bool search_unselected, bool closest) +{ + NodeList::iterator match; + double extr_dist = closest ? HUGE_VAL : -HUGE_VAL; + if (_selection.empty() && !search_unselected) return match; + + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if(j->selected()) { + if (!search_selected) continue; + } else { + if (!search_unselected) continue; + } + double dist = Geom::distance(*j, *origin); + bool cond = closest ? (dist < extr_dist) : (dist > extr_dist); + if (cond) { + match = j; + extr_dist = dist; + } + } + } + return match; +} + +/* Called when a process updates the path in-situe */ +void PathManipulator::updatePath() +{ + _externalChange(PATH_CHANGE_D); +} + +/** Called by the XML observer when something else than us modifies the path. */ +void PathManipulator::_externalChange(unsigned type) +{ + hideDragPoint(); + + switch (type) { + case PATH_CHANGE_D: { + _getGeometry(); + + // ugly: stored offsets of selected nodes in a vector + // vector<bool> should be specialized so that it takes only 1 bit per value + std::vector<bool> selpos; + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + selpos.push_back(j.selected()); + } + } + unsigned size = selpos.size(), curpos = 0; + + _createControlPointsFromGeometry(); + + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if (curpos >= size) goto end_restore; + if (selpos[curpos]) _selection.insert(j.ptr()); + ++curpos; + } + } + end_restore: + + _updateOutline(); + } break; + case PATH_CHANGE_TRANSFORM: { + SPPath *path = dynamic_cast<SPPath *>(_path); + if (path) { + Geom::Affine i2d_change = _d2i_transform; + _i2d_transform = path->i2dt_affine(); + _d2i_transform = _i2d_transform.inverse(); + i2d_change *= _i2d_transform; + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + j.transform(i2d_change); + } + } + _updateOutline(); + } + } break; + default: break; + } +} + +/** Create nodes and handles based on the XML of the edited path. */ +void PathManipulator::_createControlPointsFromGeometry() +{ + clear(); + + // sanitize pathvector and store it in SPCurve, + // so that _updateDragPoint doesn't crash on paths with naked movetos + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(_spcurve->get_pathvector()); + for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) { + // NOTE: this utilizes the fact that Geom::PathVector is an std::vector. + // When we erase an element, the next one slides into position, + // so we do not increment the iterator even though it is theoretically invalidated. + if (i->empty()) { + i = pathv.erase(i); + } else { + ++i; + } + } + if (pathv.empty()) { + return; + } + _spcurve->set_pathvector(pathv); + + pathv *= (_edit_transform * _i2d_transform); + + // in this loop, we know that there are no zero-segment subpaths + for (auto & pit : pathv) { + // prepare new subpath + SubpathPtr subpath(new NodeList(_subpaths)); + _subpaths.push_back(subpath); + + Node *previous_node = new Node(_multi_path_manipulator._path_data.node_data, pit.initialPoint()); + subpath->push_back(previous_node); + + bool closed = pit.closed(); + + for (Geom::Path::iterator cit = pit.begin(); cit != pit.end(); ++cit) { + Geom::Point pos = cit->finalPoint(); + Node *current_node; + // if the closing segment is degenerate and the path is closed, we need to move + // the handle of the first node instead of creating a new one + if (closed && cit == --(pit.end())) { + current_node = subpath->begin().get_pointer(); + } else { + /* regardless of segment type, create a new node at the end + * of this segment (unless this is the last segment of a closed path + * with a degenerate closing segment */ + current_node = new Node(_multi_path_manipulator._path_data.node_data, pos); + subpath->push_back(current_node); + } + // if this is a bezier segment, move handles appropriately + // TODO: I don't know why the dynamic cast below doesn't want to work + // when I replace BezierCurve with CubicBezier. Might be a bug + // somewhere in pathv_to_linear_and_cubic_beziers + Geom::BezierCurve const *bezier = dynamic_cast<Geom::BezierCurve const*>(&*cit); + if (bezier && bezier->order() == 3) + { + previous_node->front()->setPosition((*bezier)[1]); + current_node ->back() ->setPosition((*bezier)[2]); + } + previous_node = current_node; + } + // If the path is closed, make the list cyclic + if (pit.closed()) subpath->setClosed(true); + } + + // we need to set the nodetypes after all the handles are in place, + // so that pickBestType works correctly + // TODO maybe migrate to inkscape:node-types? + // TODO move this into SPPath - do not manipulate directly + + //XML Tree being used here directly while it shouldn't be. + gchar const *nts_raw = _path ? _path->getRepr()->attribute(_nodetypesKey().data()) : nullptr; + /* Calculate the needed length of the nodetype string. + * For closed paths, the entry is duplicated for the starting node, + * so we can just use the count of segments including the closing one + * to include the extra end node. */ + /* pad the string to required length with a bogus value. + * 'b' and any other letter not recognized by the parser causes the best fit to be set + * as the node type */ + auto const *tsi = nts_raw ? nts_raw : ""; + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + char nodetype = (*tsi) ? (*tsi++) : 'b'; + j.setType(Node::parse_nodetype(nodetype), false); + } + if (_subpath->closed() && *tsi) { + // STUPIDITY ALERT: it seems we need to use the duplicate type symbol instead of + // the first one to remain backward compatible. + _subpath->begin()->setType(Node::parse_nodetype(*tsi++), false); + } + } +} + +//determines if the trace has a bspline effect and the number of steps that it takes +int PathManipulator::_bsplineGetSteps() const { + + LivePathEffect::LPEBSpline const *lpe_bsp = nullptr; + + SPLPEItem * path = dynamic_cast<SPLPEItem *>(_path); + if (path){ + if(path->hasPathEffect()){ + Inkscape::LivePathEffect::Effect const *this_effect = + path->getFirstPathEffectOfType(Inkscape::LivePathEffect::BSPLINE); + if(this_effect){ + lpe_bsp = dynamic_cast<LivePathEffect::LPEBSpline const*>(this_effect->getLPEObj()->get_lpe()); + } + } + } + int steps = 0; + if(lpe_bsp){ + steps = lpe_bsp->steps+1; + } + return steps; +} + +// determines if the trace has bspline effect +void PathManipulator::_recalculateIsBSpline(){ + SPPath *path = dynamic_cast<SPPath *>(_path); + if (path && path->hasPathEffect()) { + Inkscape::LivePathEffect::Effect const *this_effect = + path->getFirstPathEffectOfType(Inkscape::LivePathEffect::BSPLINE); + if(this_effect){ + _is_bspline = true; + return; + } + } + _is_bspline = false; +} + +bool PathManipulator::_isBSpline() const { + return _is_bspline; +} + +// returns the corresponding strength to the position of the handlers +double PathManipulator::_bsplineHandlePosition(Handle *h, bool check_other) +{ + using Geom::X; + using Geom::Y; + double pos = NO_POWER; + Node *n = h->parent(); + Node * next_node = nullptr; + next_node = n->nodeToward(h); + if(next_node){ + std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve()); + line_inside_nodes->moveto(n->position()); + line_inside_nodes->lineto(next_node->position()); + if(!are_near(h->position(), n->position())){ + pos = Geom::nearest_time(Geom::Point(h->position()[X] - HANDLE_CUBIC_GAP, h->position()[Y] - HANDLE_CUBIC_GAP), *line_inside_nodes->first_segment()); + } + } + if (pos == NO_POWER && check_other){ + return _bsplineHandlePosition(h->other(), false); + } + return pos; +} + +// give the location for the handler in the corresponding position +Geom::Point PathManipulator::_bsplineHandleReposition(Handle *h, bool check_other) +{ + double pos = this->_bsplineHandlePosition(h, check_other); + return _bsplineHandleReposition(h,pos); +} + +// give the location for the handler to the specified position +Geom::Point PathManipulator::_bsplineHandleReposition(Handle *h,double pos){ + using Geom::X; + using Geom::Y; + Geom::Point ret = h->position(); + Node *n = h->parent(); + Geom::D2< Geom::SBasis > sbasis_inside_nodes; + std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve()); + Node * next_node = nullptr; + next_node = n->nodeToward(h); + if(next_node && pos != NO_POWER){ + line_inside_nodes->moveto(n->position()); + line_inside_nodes->lineto(next_node->position()); + sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis(); + ret = sbasis_inside_nodes.valueAt(pos); + ret = Geom::Point(ret[X] + HANDLE_CUBIC_GAP, ret[Y] + HANDLE_CUBIC_GAP); + }else{ + if(pos == NO_POWER){ + ret = n->position(); + } + } + return ret; +} + +/** Construct the geometric representation of nodes and handles, update the outline + * and display + * \param alert_LPE if true, first the LPE is warned what the new path is going to be before updating it + */ +void PathManipulator::_createGeometryFromControlPoints(bool alert_LPE) +{ + Geom::PathBuilder builder; + //Refresh if is bspline some times -think on path change selection, this value get lost + _recalculateIsBSpline(); + for (std::list<SubpathPtr>::iterator spi = _subpaths.begin(); spi != _subpaths.end(); ) { + SubpathPtr subpath = *spi; + if (subpath->empty()) { + _subpaths.erase(spi++); + continue; + } + NodeList::iterator prev = subpath->begin(); + builder.moveTo(prev->position()); + for (NodeList::iterator i = ++subpath->begin(); i != subpath->end(); ++i) { + build_segment(builder, prev.ptr(), i.ptr()); + prev = i; + } + if (subpath->closed()) { + // Here we link the last and first node if the path is closed. + // If the last segment is Bezier, we add it. + if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()) { + build_segment(builder, prev.ptr(), subpath->begin().ptr()); + } + // if that segment is linear, we just call closePath(). + builder.closePath(); + } + ++spi; + } + builder.flush(); + Geom::PathVector pathv = builder.peek() * (_edit_transform * _i2d_transform).inverse(); + for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) { + // NOTE: this utilizes the fact that Geom::PathVector is an std::vector. + // When we erase an element, the next one slides into position, + // so we do not increment the iterator even though it is theoretically invalidated. + if (i->empty()) { + i = pathv.erase(i); + } else { + ++i; + } + } + if (pathv.empty()) { + return; + } + + if (_spcurve->get_pathvector() == pathv) { + return; + } + _spcurve->set_pathvector(pathv); + if (alert_LPE) { + /// \todo note that _path can be an Inkscape::LivePathEffect::Effect* too, kind of confusing, rework member naming? + SPPath *path = dynamic_cast<SPPath *>(_path); + if (path && path->hasPathEffect()) { + Inkscape::LivePathEffect::Effect *this_effect = + path->getFirstPathEffectOfType(Inkscape::LivePathEffect::POWERSTROKE); + if(this_effect){ + LivePathEffect::LPEPowerStroke *lpe_pwr = dynamic_cast<LivePathEffect::LPEPowerStroke*>(this_effect->getLPEObj()->get_lpe()); + if (lpe_pwr) { + lpe_pwr->adjustForNewPath(pathv); + } + } + } + } + if (_live_outline) { + _updateOutline(); + } + if (_live_objects) { + _setGeometry(); + } +} + +/** Build one segment of the geometric representation. + * @relates PathManipulator */ +void build_segment(Geom::PathBuilder &builder, Node *prev_node, Node *cur_node) +{ + if (cur_node->back()->isDegenerate() && prev_node->front()->isDegenerate()) + { + // NOTE: It seems like the renderer cannot correctly handle vline / hline segments, + // and trying to display a path using them results in funny artifacts. + builder.lineTo(cur_node->position()); + } else { + // this is a bezier segment + builder.curveTo( + prev_node->front()->position(), + cur_node->back()->position(), + cur_node->position()); + } +} + +/** Construct a node type string to store in the sodipodi:nodetypes attribute. */ +std::string PathManipulator::_createTypeString() +{ + // precondition: no single-node subpaths + std::stringstream tstr; + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + tstr << j.type(); + } + // nodestring format peculiarity: first node is counted twice for closed paths + if (_subpath->closed()) tstr << _subpath->begin()->type(); + } + return tstr.str(); +} + +/** Update the path outline. */ +void PathManipulator::_updateOutline() +{ + if (!_show_outline) { + _outline->hide(); + return; + } + + Geom::PathVector pv = _spcurve->get_pathvector(); + pv *= (_edit_transform * _i2d_transform); + // This SPCurve thing has to be killed with extreme prejudice + auto _hc = std::make_unique<SPCurve>(); + if (_show_path_direction) { + // To show the direction, we append additional subpaths which consist of a single + // linear segment that starts at the time value of 0.5 and extends for 10 pixels + // at an angle 150 degrees from the unit tangent. This creates the appearance + // of little 'harpoons' that show the direction of the subpaths. + auto rot_scale_w2d = Geom::Rotate(210.0 / 180.0 * M_PI) * Geom::Scale(10.0) * _desktop->w2d(); + Geom::PathVector arrows; + for (auto & path : pv) { + for (Geom::Path::iterator j = path.begin(); j != path.end_default(); ++j) { + Geom::Point at = j->pointAt(0.5); + Geom::Point ut = j->unitTangentAt(0.5); + Geom::Point arrow_end = at + (Geom::unit_vector(_desktop->d2w(ut)) * rot_scale_w2d); + + Geom::Path arrow(at); + arrow.appendNew<Geom::LineSegment>(arrow_end); + arrows.push_back(arrow); + } + } + pv.insert(pv.end(), arrows.begin(), arrows.end()); + } + _hc->set_pathvector(pv); + _outline->set_bpath(_hc.get()); + _outline->show(); +} + +/** Retrieve the geometry of the edited object from the object tree */ +void PathManipulator::_getGeometry() +{ + using namespace Inkscape::LivePathEffect; + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path); + SPPath *path = dynamic_cast<SPPath *>(_path); + if (lpeobj) { + Effect *lpe = lpeobj->get_lpe(); + if (lpe) { + PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data())); + _spcurve.reset(new SPCurve(pathparam->get_pathvector())); + } + } else if (path) { + _spcurve = SPCurve::copy(path->curveForEdit()); + // never allow NULL to sneak in here! + if (_spcurve == nullptr) { + _spcurve.reset(new SPCurve()); + } + } +} + +/** Set the geometry of the edited object in the object tree, but do not commit to XML */ +void PathManipulator::_setGeometry() +{ + using namespace Inkscape::LivePathEffect; + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path); + SPPath *path = dynamic_cast<SPPath *>(_path); + if (lpeobj) { + // copied from nodepath.cpp + // NOTE: if we are editing an LPE param, _path is not actually an SPPath, it is + // a LivePathEffectObject. (mad laughter) + Effect *lpe = lpeobj->get_lpe(); + if (lpe) { + PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data())); + if (pathparam->get_pathvector() == _spcurve->get_pathvector()) { + return; //False we dont update LPE + } + pathparam->set_new_value(_spcurve->get_pathvector(), false); + lpeobj->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } else if (path) { + // return true to leave the decision on empty to the caller. + // Maybe the path become empty and we want to update to empty + if (empty()) return; + if (path->curveBeforeLPE()) { + path->setCurveBeforeLPE(_spcurve.get()); + if (!path->hasPathEffectOfTypeRecursive(Inkscape::LivePathEffect::SLICE)) { + sp_lpe_item_update_patheffect(path, true, false); + } else { + path->setCurve(_spcurve.get()); + } + } else { + path->setCurve(_spcurve.get()); + } + } +} + +/** Figure out in what attribute to store the nodetype string. */ +Glib::ustring PathManipulator::_nodetypesKey() +{ + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path); + if (!lpeobj) { + return "sodipodi:nodetypes"; + } else { + return _lpe_key + "-nodetypes"; + } +} + +/** Return the XML node we are editing. + * This method is wrong but necessary at the moment. */ +Inkscape::XML::Node *PathManipulator::_getXMLNode() +{ + //XML Tree being used here directly while it shouldn't be. + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path); + if (!lpeobj) + return _path->getRepr(); + //XML Tree being used here directly while it shouldn't be. + return lpeobj->getRepr(); +} + +bool PathManipulator::_nodeClicked(Node *n, GdkEventButton *event) +{ + if (event->button != 1) return false; + if (held_alt(*event) && held_control(*event)) { + // Ctrl+Alt+click: delete nodes + hideDragPoint(); + NodeList::iterator iter = NodeList::get_iterator(n); + NodeList &nl = iter->nodeList(); + + if (nl.size() <= 1 || (nl.size() <= 2 && !nl.closed())) { + // Removing last node of closed path - delete it + nl.kill(); + } else { + // In other cases, delete the node under cursor + _deleteStretch(iter, iter.next(), true); + } + + if (!empty()) { + update(true); + } + + // We need to call MPM's method because it could have been our last node + _multi_path_manipulator._doneWithCleanup(_("Delete node")); + + return true; + } else if (held_control(*event)) { + // Ctrl+click: cycle between node types + if (!n->isEndNode()) { + n->setType(static_cast<NodeType>((n->type() + 1) % NODE_LAST_REAL_TYPE)); + update(); + _commit(_("Cycle node type")); + } + return true; + } + return false; +} + +void PathManipulator::_handleGrabbed() +{ + _selection.hideTransformHandles(); +} + +void PathManipulator::_handleUngrabbed() +{ + _selection.restoreTransformHandles(); + _commit(_("Drag handle")); +} + +bool PathManipulator::_handleClicked(Handle *h, GdkEventButton *event) +{ + // retracting by Ctrl+click + if (event->button == 1 && held_control(*event)) { + h->move(h->parent()->position()); + update(); + _commit(_("Retract handle")); + return true; + } + return false; +} + +void PathManipulator::_selectionChangedM(std::vector<SelectableControlPoint *> pvec, bool selected) { + for (auto & n : pvec) { + _selectionChanged(n, selected); + } +} + +void PathManipulator::_selectionChanged(SelectableControlPoint *p, bool selected) +{ + // don't do anything if we do not show handles + if (!_show_handles) return; + + // only do something if a node changed selection state + Node *node = dynamic_cast<Node*>(p); + if (!node) return; + + // update handle display + NodeList::iterator iters[5]; + iters[2] = NodeList::get_iterator(node); + iters[1] = iters[2].prev(); + iters[3] = iters[2].next(); + if (selected) { + // selection - show handles on this node and adjacent ones + node->showHandles(true); + if (iters[1]) iters[1]->showHandles(true); + if (iters[3]) iters[3]->showHandles(true); + } else { + /* Deselection is more complex. + * The change might affect 3 nodes - this one and two adjacent. + * If the node and both its neighbors are deselected, hide handles. + * Otherwise, leave as is. */ + if (iters[1]) iters[0] = iters[1].prev(); + if (iters[3]) iters[4] = iters[3].next(); + bool nodesel[5]; + for (int i = 0; i < 5; ++i) { + nodesel[i] = iters[i] && iters[i]->selected(); + } + for (int i = 1; i < 4; ++i) { + if (iters[i] && !nodesel[i-1] && !nodesel[i] && !nodesel[i+1]) { + iters[i]->showHandles(false); + } + } + } +} + +/** Removes all nodes belonging to this manipulator from the control point selection */ +void PathManipulator::_removeNodesFromSelection() +{ + // remove this manipulator's nodes from selection + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + _selection.erase(j.get_pointer()); + } + } +} + +/** Update the XML representation and put the specified annotation on the undo stack */ +void PathManipulator::_commit(Glib::ustring const &annotation) +{ + writeXML(); + if (_desktop) { + DocumentUndo::done(_desktop->getDocument(), annotation.data(), INKSCAPE_ICON("tool-node-editor")); + } +} + +void PathManipulator::_commit(Glib::ustring const &annotation, gchar const *key) +{ + writeXML(); + DocumentUndo::maybeDone(_desktop->getDocument(), key, annotation.data(), INKSCAPE_ICON("tool-node-editor")); +} + +/** Update the position of the curve drag point such that it is over the nearest + * point of the path. */ +Geom::Coord PathManipulator::_updateDragPoint(Geom::Point const &evp) +{ + Geom::Coord dist = HUGE_VAL; + + Geom::Affine to_desktop = _edit_transform * _i2d_transform; + Geom::PathVector pv = _spcurve->get_pathvector(); + std::optional<Geom::PathVectorTime> pvp = + pv.nearestTime(_desktop->w2d(evp) * to_desktop.inverse()); + if (!pvp) return dist; + Geom::Point nearest_pt = _desktop->d2w(pv.pointAt(*pvp) * to_desktop); + + double fracpart = pvp->t; + std::list<SubpathPtr>::iterator spi = _subpaths.begin(); + for (unsigned i = 0; i < pvp->path_index; ++i, ++spi) {} + NodeList::iterator first = (*spi)->before(pvp->asPathTime()); + + dist = Geom::distance(evp, nearest_pt); + + double stroke_tolerance = _getStrokeTolerance(); + if (first && first.next() && + fracpart != 0.0 && + fracpart != 1.0 && + dist < stroke_tolerance) + { + // stroke_tolerance is at least two. + int tolerance = std::max(2, (int)stroke_tolerance); + _dragpoint->setVisible(true); + _dragpoint->setPosition(_desktop->w2d(nearest_pt)); + _dragpoint->setSize(2 * tolerance - 1); + _dragpoint->setTimeValue(fracpart); + _dragpoint->setIterator(first); + } else { + _dragpoint->setVisible(false); + } + + return dist; +} + +/// This is called on zoom change to update the direction arrows +void PathManipulator::_updateOutlineOnZoomChange() +{ + if (_show_path_direction) _updateOutline(); +} + +/** Compute the radius from the edge of the path where clicks should initiate a curve drag + * or segment selection, in window coordinates. */ +double PathManipulator::_getStrokeTolerance() +{ + /* Stroke event tolerance is equal to half the stroke's width plus the global + * drag tolerance setting. */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double ret = prefs->getIntLimited("/options/dragtolerance/value", 2, 0, 100); + if (_path && _path->style && !_path->style->stroke.isNone()) { + ret += _path->style->stroke_width.computed * 0.5 + * (_edit_transform * _i2d_transform).descrim() // scale to desktop coords + * _desktop->current_zoom(); // == _d2w.descrim() - scale to window coords + } + return ret; +} + +} // 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/path-manipulator.h b/src/ui/tool/path-manipulator.h new file mode 100644 index 0000000..ca861c6 --- /dev/null +++ b/src/ui/tool/path-manipulator.h @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Path manipulator - a component that edits a single path on-canvas + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_PATH_MANIPULATOR_H +#define SEEN_UI_TOOL_PATH_MANIPULATOR_H + +#include <string> +#include <memory> +#include <2geom/pathvector.h> +#include <2geom/path-sink.h> +#include <2geom/affine.h> +#include "ui/tool/node.h" +#include "ui/tool/manipulator.h" +#include "live_effects/lpe-bspline.h" + +struct SPCanvasItem; +class SPCurve; +class SPPath; + +namespace Inkscape { + +class CanvasItemBpath; + +namespace XML { class Node; } + +namespace UI { + +class PathManipulator; +class ControlPointSelection; +class PathManipulatorObserver; +class CurveDragPoint; +class PathCanvasGroups; +class MultiPathManipulator; +class Node; +class Handle; + +struct PathSharedData { + NodeSharedData node_data; + Inkscape::CanvasItemGroup *outline_group; + Inkscape::CanvasItemGroup *dragpoint_group; +}; + +/** + * Manipulator that edits a single path using nodes with handles. + * Currently only cubic bezier and linear segments are supported, but this might change + * some time in the future. + */ +class PathManipulator : public PointManipulator { +public: + typedef SPPath *ItemType; + + PathManipulator(MultiPathManipulator &mpm, SPObject *path, Geom::Affine const &edit_trans, + guint32 outline_color, Glib::ustring lpe_key); + ~PathManipulator() override; + bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override; + + bool empty(); + void writeXML(); + void update(bool alert_LPE = false); // update display, but don't commit + void clear(); // remove all nodes from manipulator + SPObject *item() { return _path; } + + void selectSubpaths(); + void invertSelectionInSubpaths(); + + void insertNodeAtExtremum(ExtremumType extremum); + void insertNodes(); + void insertNode(Geom::Point); + void insertNode(NodeList::iterator first, double t, bool take_selection); + void duplicateNodes(); + void copySelectedPath(Geom::PathBuilder *builder); + void weldNodes(NodeList::iterator preserve_pos = NodeList::iterator()); + void weldSegments(); + void breakNodes(); + void deleteNodes(bool keep_shape = true); + void deleteSegments(); + void reverseSubpaths(bool selected_only); + void setSegmentType(SegmentType); + + void scaleHandle(Node *n, int which, int dir, bool pixel); + void rotateHandle(Node *n, int which, int dir, bool pixel); + + void showOutline(bool show); + void showHandles(bool show); + void showPathDirection(bool show); + void setLiveOutline(bool set); + void setLiveObjects(bool set); + void updateHandles(); + void updatePath(); + void setControlsTransform(Geom::Affine const &); + void hideDragPoint(); + MultiPathManipulator &mpm() { return _multi_path_manipulator; } + + NodeList::iterator subdivideSegment(NodeList::iterator after, double t); + NodeList::iterator extremeNode(NodeList::iterator origin, bool search_selected, + bool search_unselected, bool closest); + + int _bsplineGetSteps() const; + // this is necessary for Tab-selection in MultiPathManipulator + SubpathList &subpathList() { return _subpaths; } + + static bool is_item_type(void *item); +private: + typedef NodeList Subpath; + typedef std::shared_ptr<NodeList> SubpathPtr; + + void _createControlPointsFromGeometry(); + + void _recalculateIsBSpline(); + bool _isBSpline() const; + double _bsplineHandlePosition(Handle *h, bool check_other = true); + Geom::Point _bsplineHandleReposition(Handle *h, bool check_other = true); + Geom::Point _bsplineHandleReposition(Handle *h, double pos); + void _createGeometryFromControlPoints(bool alert_LPE = false); + unsigned _deleteStretch(NodeList::iterator first, NodeList::iterator last, bool keep_shape); + std::string _createTypeString(); + void _updateOutline(); + //void _setOutline(Geom::PathVector const &); + void _getGeometry(); + void _setGeometry(); + Glib::ustring _nodetypesKey(); + Inkscape::XML::Node *_getXMLNode(); + + void _selectionChangedM(std::vector<SelectableControlPoint *> pvec, bool selected); + void _selectionChanged(SelectableControlPoint * p, bool selected); + bool _nodeClicked(Node *, GdkEventButton *); + void _handleGrabbed(); + bool _handleClicked(Handle *, GdkEventButton *); + void _handleUngrabbed(); + + void _externalChange(unsigned type); + void _removeNodesFromSelection(); + void _commit(Glib::ustring const &annotation); + void _commit(Glib::ustring const &annotation, gchar const *key); + Geom::Coord _updateDragPoint(Geom::Point const &); + void _updateOutlineOnZoomChange(); + double _getStrokeTolerance(); + Handle *_chooseHandle(Node *n, int which); + + SubpathList _subpaths; + MultiPathManipulator &_multi_path_manipulator; + SPObject *_path; ///< can be an SPPath or an Inkscape::LivePathEffect::Effect !!! + std::unique_ptr<SPCurve> _spcurve; // in item coordinates + Inkscape::CanvasItemBpath *_outline = nullptr; + CurveDragPoint *_dragpoint; // an invisible control point hovering over curve + PathManipulatorObserver *_observer; + Geom::Affine _d2i_transform; ///< desktop-to-item transform + Geom::Affine _i2d_transform; ///< item-to-desktop transform, inverse of _d2i_transform + Geom::Affine _edit_transform; ///< additional transform to apply to editing controls + bool _show_handles = true; + bool _show_outline = false; + bool _show_path_direction = false; + bool _live_outline = true; + bool _live_objects = true; + bool _is_bspline = false; + Glib::ustring _lpe_key; + + friend class PathManipulatorObserver; + friend class CurveDragPoint; + friend class Node; + friend class Handle; +}; + +} // 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/selectable-control-point.cpp b/src/ui/tool/selectable-control-point.cpp new file mode 100644 index 0000000..5e5d0b2 --- /dev/null +++ b/src/ui/tool/selectable-control-point.cpp @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tool/selectable-control-point.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" + +namespace Inkscape { +namespace UI { + +ControlPoint::ColorSet SelectableControlPoint::_default_scp_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 +}; + +SelectableControlPoint::SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Inkscape::CanvasItemCtrlType type, + ControlPointSelection &sel, + ColorSet const &cset, + Inkscape::CanvasItemGroup *group) + : ControlPoint(d, initial_pos, anchor, type, cset, group) + , _selection(sel) +{ + _canvas_item_ctrl->set_name("CanvasItemCtrl:SelectableControlPoint"); + _selection.allPoints().insert(this); +} + +SelectableControlPoint::SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Glib::RefPtr<Gdk::Pixbuf> pixbuf, + ControlPointSelection &sel, + ColorSet const &cset, + Inkscape::CanvasItemGroup *group) + : ControlPoint(d, initial_pos, anchor, pixbuf, cset, group) + , _selection (sel) +{ + _selection.allPoints().insert(this); +} + +SelectableControlPoint::~SelectableControlPoint() +{ + _selection.erase(this); + _selection.allPoints().erase(this); +} + +bool SelectableControlPoint::grabbed(GdkEventMotion *) +{ + // if a point is dragged while not selected, it should select itself + if (!selected()) { + _takeSelection(); + } + _selection._pointGrabbed(this); + return false; +} + +void SelectableControlPoint::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + _selection._pointDragged(new_pos, event); +} + +void SelectableControlPoint::ungrabbed(GdkEventButton *) +{ + _selection._pointUngrabbed(); +} + +bool SelectableControlPoint::clicked(GdkEventButton *event) +{ + if (_selection._pointClicked(this, event)) + return true; + + if (event->button != 1) return false; + if (held_shift(*event)) { + if (selected()) { + _selection.erase(this); + } else { + _selection.insert(this); + } + } else { + _takeSelection(); + } + return true; +} + +void SelectableControlPoint::select(bool toselect) +{ + if (toselect) { + _selection.insert(this); + } else { + _selection.erase(this); + } +} + +void SelectableControlPoint::_takeSelection() +{ + _selection.clear(); + _selection.insert(this); +} + +bool SelectableControlPoint::selected() const +{ + SelectableControlPoint *p = const_cast<SelectableControlPoint*>(this); + return _selection.find(p) != _selection.end(); +} + +void SelectableControlPoint::_setState(State state) +{ + if (!selected()) { + ControlPoint::_setState(state); + } else { + ColorEntry current = {0, 0}; + ColorSet const &activeCset = (_isLurking()) ? invisible_cset : _cset; + switch (state) { + case STATE_NORMAL: + current = activeCset.selected_normal; + break; + case STATE_MOUSEOVER: + current = activeCset.selected_mouseover; + break; + case STATE_CLICKED: + current = activeCset.selected_clicked; + break; + } + _setColors(current); + _state = 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/selectable-control-point.h b/src/ui/tool/selectable-control-point.h new file mode 100644 index 0000000..d57dd50 --- /dev/null +++ b/src/ui/tool/selectable-control-point.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_SELECTABLE_CONTROL_POINT_H +#define SEEN_UI_TOOL_SELECTABLE_CONTROL_POINT_H + +#include "ui/tool/control-point.h" + +namespace Inkscape { +namespace UI { + +class ControlPointSelection; + +/** + * Desktop-bound selectable control object. + */ +class SelectableControlPoint : public ControlPoint { +public: + + ~SelectableControlPoint() override; + bool selected() const; + void updateState() { _setState(_state); } + virtual Geom::Rect bounds() const { + return Geom::Rect(position(), position()); + } + virtual void select(bool toselect); + friend class NodeList; + + +protected: + + SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Inkscape::CanvasItemCtrlType type, + ControlPointSelection &sel, + ColorSet const &cset = _default_scp_color_set, + Inkscape::CanvasItemGroup *group = nullptr); + + SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Glib::RefPtr<Gdk::Pixbuf> pixbuf, + ControlPointSelection &sel, + ColorSet const &cset = _default_scp_color_set, + Inkscape::CanvasItemGroup *group = nullptr); + + void _setState(State state) override; + + void dragged(Geom::Point &new_pos, GdkEventMotion *event) override; + bool grabbed(GdkEventMotion *event) override; + void ungrabbed(GdkEventButton *event) override; + bool clicked(GdkEventButton *event) override; + + ControlPointSelection &_selection; + +private: + + void _takeSelection(); + + static ColorSet _default_scp_color_set; +}; + +} // 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/selector.cpp b/src/ui/tool/selector.cpp new file mode 100644 index 0000000..5b2fd45 --- /dev/null +++ b/src/ui/tool/selector.cpp @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Selector component (click and rubberband) + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdkkeysyms.h> + +#include "control-point.h" +#include "desktop.h" + +#include "display/control/canvas-item-rect.h" + +#include "ui/tools/tool-base.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/selector.h" + +namespace Inkscape { +namespace UI { + +/** A hidden control point used for rubberbanding and selection. + * It uses a clever hack: the canvas item is hidden and only receives events when they + * are passed to it using Selector's event() function. When left mouse button + * is pressed, it grabs events and handles drags and clicks in the usual way. */ +class SelectorPoint : public ControlPoint { +public: + SelectorPoint(SPDesktop *d, Inkscape::CanvasItemGroup *group, Selector *s) : + ControlPoint(d, Geom::Point(0,0), SP_ANCHOR_CENTER, + Inkscape::CANVAS_ITEM_CTRL_TYPE_INVISIPOINT, + invisible_cset, group), + _selector(s), + _cancel(false) + { + _canvas_item_ctrl->set_name("CanvasItemCtrl:SelectorPoint"); + setVisible(false); + _rubber = new Inkscape::CanvasItemRect(_desktop->getCanvasControls()); + _rubber->set_name("CanavasItemRect:SelectorPoint:Rubberband"); + _rubber->set_stroke(0x8080ffff); + _rubber->set_inverted(true); + _rubber->hide(); + } + + ~SelectorPoint() override { + delete _rubber; + } + + SPDesktop *desktop() { return _desktop; } + + bool event(Inkscape::UI::Tools::ToolBase *ec, GdkEvent *e) { + return _eventHandler(ec, e); + } + +protected: + bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override { + if (event->type == GDK_KEY_PRESS && + shortcut_key(event->key) == GDK_KEY_Escape && + _rubber->is_visible() ) + { + _cancel = true; + _rubber->hide(); + return true; + } + return ControlPoint::_eventHandler(event_context, event); + } + +private: + bool grabbed(GdkEventMotion *) override { + _cancel = false; + _start = position(); + _rubber->show(); + return false; + } + + void dragged(Geom::Point &new_pos, GdkEventMotion *) override { + if (_cancel) return; + Geom::Rect sel(_start, new_pos); + _rubber->set_rect(sel); + } + + void ungrabbed(GdkEventButton *event) override { + if (_cancel) return; + _rubber->hide(); + Geom::Rect sel(_start, position()); + _selector->signal_area.emit(sel, event); + } + + bool clicked(GdkEventButton *event) override { + if (event->button != 1) return false; + _selector->signal_point.emit(position(), event); + return true; + } + + Inkscape::CanvasItemRect *_rubber; + Selector *_selector; + Geom::Point _start; + bool _cancel; +}; + + +Selector::Selector(SPDesktop *desktop) + : Manipulator(desktop) + , _dragger(new SelectorPoint(desktop, desktop->getCanvasControls(), this)) +{ + _dragger->setVisible(false); +} + +Selector::~Selector() +{ + delete _dragger; +} + +bool Selector::event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + // The hidden control point will capture all events after it obtains the grab, + // but it relies on this function to initiate it. If we pass only first button + // press events here, it won't interfere with any other event handling. + switch (event->type) { + case GDK_BUTTON_PRESS: + // Do not pass button presses other than left button to the control point. + // This way middle click and right click can be handled in ToolBase. + if (event->button.button == 1 && !event_context->is_space_panning()) { + _dragger->setPosition(_desktop->w2d(event_point(event->motion))); + return _dragger->event(event_context, event); + } + break; + default: break; + } + return false; +} + +bool Selector::doubleClicked() { + return _dragger->doubleClicked(); +} + +} // 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/selector.h b/src/ui/tool/selector.h new file mode 100644 index 0000000..7ad0c3b --- /dev/null +++ b/src/ui/tool/selector.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Selector component (click and rubberband) + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_SELECTOR_H +#define SEEN_UI_TOOL_SELECTOR_H + +#include <memory> +#include <gdk/gdk.h> +#include <2geom/rect.h> +#include "ui/tool/manipulator.h" + +class SPDesktop; + +namespace Inkscape { + +class CanvasItemRect; + +namespace UI { + +class SelectorPoint; + +class Selector : public Manipulator { +public: + Selector(SPDesktop *d); + ~Selector() override; + bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override; + virtual bool doubleClicked(); + + sigc::signal<void, Geom::Rect const &, GdkEventButton*> signal_area; + sigc::signal<void, Geom::Point const &, GdkEventButton*> signal_point; +private: + SelectorPoint *_dragger; + Geom::Point _start; + friend class SelectorPoint; +}; + +} // 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/shape-record.h b/src/ui/tool/shape-record.h new file mode 100644 index 0000000..1f29453 --- /dev/null +++ b/src/ui/tool/shape-record.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Structures that store data needed for shape editing which are not contained + * directly in the XML node + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_SHAPE_RECORD_H +#define SEEN_UI_TOOL_SHAPE_RECORD_H + +#include <glibmm/ustring.h> +#include <boost/operators.hpp> +#include <2geom/affine.h> + +class SPItem; +class SPObject; +namespace Inkscape { +namespace UI { + +/** Role of the shape in the drawing - affects outline display and color */ +enum ShapeRole { + SHAPE_ROLE_NORMAL, + SHAPE_ROLE_CLIPPING_PATH, + SHAPE_ROLE_MASK, + SHAPE_ROLE_LPE_PARAM // implies edit_original set to true in ShapeRecord +}; + +struct ShapeRecord : + public boost::totally_ordered<ShapeRecord> +{ + SPObject *object; // SP node for the edited shape could be a lpeoject invisible so we use a spobject + ShapeRole role; + Glib::ustring lpe_key; // name of LPE shape param being edited + + Geom::Affine edit_transform; // how to transform controls - used for clipping paths, masks, and markers + double edit_rotation; // how to transform controls - used for markers + + inline bool operator==(ShapeRecord const &o) const { + return object == o.object && lpe_key == o.lpe_key; + } + inline bool operator<(ShapeRecord const &o) const { + return object == o.object ? (lpe_key < o.lpe_key) : (object < o.object); + } +}; + +} // 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/transform-handle-set.cpp b/src/ui/tool/transform-handle-set.cpp new file mode 100644 index 0000000..875429a --- /dev/null +++ b/src/ui/tool/transform-handle-set.cpp @@ -0,0 +1,827 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Affine transform handles component + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include <algorithm> + +#include <glib/gi18n.h> + +#include <2geom/transforms.h> + +#include "control-point.h" +#include "desktop.h" +#include "pure-transform.h" +#include "seltrans.h" +#include "snap.h" + +#include "display/control/canvas-item-rect.h" + +#include "object/sp-namedview.h" + +#include "ui/tool/commit-events.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/node.h" +#include "ui/tool/selectable-control-point.h" +#include "ui/tool/transform-handle-set.h" +#include "ui/tools/node-tool.h" + + +GType sp_select_context_get_type(); + +namespace Inkscape { +namespace UI { + +namespace { + +SPAnchorType corner_to_anchor(unsigned c) { + switch (c % 4) { + case 0: return SP_ANCHOR_NE; + case 1: return SP_ANCHOR_NW; + case 2: return SP_ANCHOR_SW; + default: return SP_ANCHOR_SE; + } +} + +SPAnchorType side_to_anchor(unsigned s) { + switch (s % 4) { + case 0: return SP_ANCHOR_N; + case 1: return SP_ANCHOR_W; + case 2: return SP_ANCHOR_S; + default: return SP_ANCHOR_E; + } +} + +// TODO move those two functions into a common place +double snap_angle(double a) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + double unit_angle = M_PI / snaps; + return CLAMP(unit_angle * round(a / unit_angle), -M_PI, M_PI); +} + +double snap_increment_degrees() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + return 180.0 / snaps; +} + +} // anonymous namespace + +ControlPoint::ColorSet TransformHandle::thandle_cset = { + {0x000000ff, 0x000000ff}, // normal fill, stroke + {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke + {0x00ff66ff, 0x000000ff}, // clicked fill, stroke + // + {0x000000ff, 0x000000ff}, // normal fill, stroke when selected + {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke when selected + {0x00ff66ff, 0x000000ff} // clicked fill, stroke when selected +}; + +TransformHandle::TransformHandle(TransformHandleSet &th, SPAnchorType anchor, Inkscape::CanvasItemCtrlType type) + : ControlPoint(th._desktop, Geom::Point(), anchor, type, thandle_cset, th._transform_handle_group) + , _th(th) +{ + _canvas_item_ctrl->set_name("CanvasItemCtrl:TransformHandle"); + setVisible(false); +} + +// TODO: This code is duplicated in seltrans.cpp; fix this! +void TransformHandle::getNextClosestPoint(bool reverse) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/snapclosestonly/value", false)) { + if (!_all_snap_sources_sorted.empty()) { + if (reverse) { // Shift-tab will find a closer point + if (_all_snap_sources_iter == _all_snap_sources_sorted.begin()) { + _all_snap_sources_iter = _all_snap_sources_sorted.end(); + } + --_all_snap_sources_iter; + } else { // Tab will find a point further away + ++_all_snap_sources_iter; + if (_all_snap_sources_iter == _all_snap_sources_sorted.end()) { + _all_snap_sources_iter = _all_snap_sources_sorted.begin(); + } + } + + _snap_points.clear(); + _snap_points.push_back(*_all_snap_sources_iter); + + // Show the updated snap source now; otherwise it won't be shown until the selection is being moved again + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.displaySnapsource(*_all_snap_sources_iter); + m.unSetup(); + } + } +} + +bool TransformHandle::grabbed(GdkEventMotion *) +{ + _origin = position(); + _last_transform.setIdentity(); + startTransform(); + + _th._setActiveHandle(this); + _setLurking(true); + _setState(_state); + + // Collect the snap-candidates, one for each selected node. These will be stored in the _snap_points vector. + Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(_th._desktop->event_context); + //ControlPointSelection *selection = nt->_selected_nodes.get(); + ControlPointSelection* selection = nt->_selected_nodes; + + selection->setOriginalPoints(); + selection->getOriginalPoints(_snap_points); + selection->getUnselectedPoints(_unselected_points); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/snapclosestonly/value", false)) { + // Find the closest snap source candidate + _all_snap_sources_sorted = _snap_points; + + // Calculate and store the distance to the reference point for each snap candidate point + for(auto & i : _all_snap_sources_sorted) { + i.setDistance(Geom::L2(i.getPoint() - _origin)); + } + + // Sort them ascending, using the distance calculated above as the single criteria + std::sort(_all_snap_sources_sorted.begin(), _all_snap_sources_sorted.end()); + + // Now get the closest snap source + _snap_points.clear(); + if (!_all_snap_sources_sorted.empty()) { + _all_snap_sources_iter = _all_snap_sources_sorted.begin(); + _snap_points.push_back(_all_snap_sources_sorted.front()); + } + } + + return false; +} + +void TransformHandle::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + Geom::Affine t = computeTransform(new_pos, event); + // protect against degeneracies + if (t.isSingular()) return; + Geom::Affine incr = _last_transform.inverse() * t; + if (incr.isSingular()) return; + _th.signal_transform.emit(incr); + _last_transform = t; +} + +void TransformHandle::ungrabbed(GdkEventButton *) +{ + _snap_points.clear(); + _th._clearActiveHandle(); + _setLurking(false); + _setState(_state); + endTransform(); + _th.signal_commit.emit(getCommitEvent()); + + //updates the positions of the nodes + Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(_th._desktop->event_context); + ControlPointSelection* selection = nt->_selected_nodes; + selection->setOriginalPoints(); +} + + +class ScaleHandle : public TransformHandle { +public: + ScaleHandle(TransformHandleSet &th, SPAnchorType anchor, Inkscape::CanvasItemCtrlType type) + : TransformHandle(th, anchor, type) + {} +protected: + Glib::ustring _getTip(unsigned state) const override { + if (state_held_control(state)) { + if (state_held_shift(state)) { + return C_("Transform handle tip", + "<b>Shift+Ctrl</b>: scale uniformly about the rotation center"); + } + return C_("Transform handle tip", "<b>Ctrl:</b> scale uniformly"); + } + if (state_held_shift(state)) { + if (state_held_alt(state)) { + return C_("Transform handle tip", + "<b>Shift+Alt</b>: scale using an integer ratio about the rotation center"); + } + return C_("Transform handle tip", "<b>Shift</b>: scale from the rotation center"); + } + if (state_held_alt(state)) { + return C_("Transform handle tip", "<b>Alt</b>: scale using an integer ratio"); + } + return C_("Transform handle tip", "<b>Scale handle</b>: drag to scale the selection"); + } + + Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override { + return format_tip(C_("Transform handle tip", + "Scale by %.2f%% x %.2f%%"), _last_scale_x * 100, _last_scale_y * 100); + } + + bool _hasDragTips() const override { return true; } + + static double _last_scale_x, _last_scale_y; +}; +double ScaleHandle::_last_scale_x = 1.0; +double ScaleHandle::_last_scale_y = 1.0; + +/** + * Corner scaling handle for node transforms. + */ +class ScaleCornerHandle : public ScaleHandle { +public: + + ScaleCornerHandle(TransformHandleSet &th, unsigned corner, unsigned d_corner) : + ScaleHandle(th, corner_to_anchor(d_corner), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE), + _corner(corner) + {} + +protected: + void startTransform() override { + _sc_center = _th.rotationCenter(); + _sc_opposite = _th.bounds().corner(_corner + 2); + _last_scale_x = _last_scale_y = 1.0; + } + + Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override { + Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite; + Geom::Point vold = _origin - scc, vnew = new_pos - scc; + // avoid exploding the selection + if (Geom::are_near(vold[Geom::X], 0) || Geom::are_near(vold[Geom::Y], 0)) + return Geom::identity(); + + Geom::Scale scale = Geom::Scale(vnew[Geom::X] / vold[Geom::X], vnew[Geom::Y] / vold[Geom::Y]); + + if (held_alt(*event)) { + for (unsigned i = 0; i < 2; ++i) { + if (fabs(scale[i]) >= 1.0) { + scale[i] = round(scale[i]); + } else { + scale[i] = 1.0 / round(1.0 / MIN(scale[i],10)); + } + } + } else { + SnapManager &m = _th._desktop->namedview->snap_manager; + m.setupIgnoreSelection(_th._desktop, true, &_unselected_points); + + Inkscape::PureScale *ptr; + if (held_control(*event)) { + scale[0] = scale[1] = std::min(scale[0], scale[1]); + ptr = new Inkscape::PureScaleConstrained(Geom::Scale(scale[0], scale[1]), scc); + } else { + ptr = new Inkscape::PureScale(Geom::Scale(scale[0], scale[1]), scc, false); + } + m.snapTransformed(_snap_points, _origin, (*ptr)); + m.unSetup(); + if (ptr->best_snapped_point.getSnapped()) { + scale = ptr->getScaleSnapped(); + } + + delete ptr; + } + + _last_scale_x = scale[0]; + _last_scale_y = scale[1]; + Geom::Affine t = Geom::Translate(-scc) + * Geom::Scale(scale[0], scale[1]) + * Geom::Translate(scc); + return t; + } + + CommitEvent getCommitEvent() override { + return _last_transform.isUniformScale() + ? COMMIT_MOUSE_SCALE_UNIFORM + : COMMIT_MOUSE_SCALE; + } + +private: + + Geom::Point _sc_center; + Geom::Point _sc_opposite; + unsigned _corner; +}; + +/** + * Side scaling handle for node transforms. + */ +class ScaleSideHandle : public ScaleHandle { +public: + ScaleSideHandle(TransformHandleSet &th, unsigned side, unsigned d_side) + : ScaleHandle(th, side_to_anchor(d_side), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE) + , _side(side) + {} +protected: + void startTransform() override { + _sc_center = _th.rotationCenter(); + Geom::Rect b = _th.bounds(); + _sc_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3)); + _last_scale_x = _last_scale_y = 1.0; + } + Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override { + Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite; + Geom::Point vs; + Geom::Dim2 d1 = static_cast<Geom::Dim2>((_side + 1) % 2); + Geom::Dim2 d2 = static_cast<Geom::Dim2>(_side % 2); + + // avoid exploding the selection + if (Geom::are_near(scc[d1], _origin[d1])) + return Geom::identity(); + + vs[d1] = (new_pos - scc)[d1] / (_origin - scc)[d1]; + if (held_alt(*event)) { + if (fabs(vs[d1]) >= 1.0) { + vs[d1] = round(vs[d1]); + } else { + vs[d1] = 1.0 / round(1.0 / MIN(vs[d1],10)); + } + vs[d2] = 1.0; + } else { + SnapManager &m = _th._desktop->namedview->snap_manager; + m.setupIgnoreSelection(_th._desktop, true, &_unselected_points); + + bool uniform = held_control(*event); + Inkscape::PureStretchConstrained psc = Inkscape::PureStretchConstrained(vs[d1], scc, d1, uniform); + m.snapTransformed(_snap_points, _origin, psc); + m.unSetup(); + + if (psc.best_snapped_point.getSnapped()) { + Geom::Point result = psc.getStretchSnapped().vector(); //best_snapped_point.getTransformation(); + vs[d1] = result[d1]; + vs[d2] = result[d2]; + } else { + // on ctrl, apply uniform scaling instead of stretching + // Preserve aspect ratio, but never flip in the dimension not being edited (by using fabs()) + vs[d2] = uniform ? fabs(vs[d1]) : 1.0; + } + } + + _last_scale_x = vs[Geom::X]; + _last_scale_y = vs[Geom::Y]; + Geom::Affine t = Geom::Translate(-scc) + * Geom::Scale(vs) + * Geom::Translate(scc); + return t; + } + CommitEvent getCommitEvent() override { + return _last_transform.isUniformScale() + ? COMMIT_MOUSE_SCALE_UNIFORM + : COMMIT_MOUSE_SCALE; + } + +private: + + Geom::Point _sc_center; + Geom::Point _sc_opposite; + unsigned _side; +}; + +/** + * Rotation handle for node transforms. + */ +class RotateHandle : public TransformHandle { +public: + RotateHandle(TransformHandleSet &th, unsigned corner, unsigned d_corner) + : TransformHandle(th, corner_to_anchor(d_corner), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE) + , _corner(corner) + {} +protected: + + void startTransform() override { + _rot_center = _th.rotationCenter(); + _rot_opposite = _th.bounds().corner(_corner + 2); + _last_angle = 0; + } + + Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override + { + Geom::Point rotc = held_shift(*event) ? _rot_opposite : _rot_center; + double angle = Geom::angle_between(_origin - rotc, new_pos - rotc); + if (held_control(*event)) { + angle = snap_angle(angle); + } else { + SnapManager &m = _th._desktop->namedview->snap_manager; + m.setupIgnoreSelection(_th._desktop, true, &_unselected_points); + Inkscape::PureRotateConstrained prc = Inkscape::PureRotateConstrained(angle, rotc); + m.snapTransformed(_snap_points, _origin, prc); + m.unSetup(); + + if (prc.best_snapped_point.getSnapped()) { + angle = prc.getAngleSnapped(); //best_snapped_point.getTransformation()[0]; + } + } + + _last_angle = angle; + Geom::Affine t = Geom::Translate(-rotc) + * Geom::Rotate(angle) + * Geom::Translate(rotc); + return t; + } + + CommitEvent getCommitEvent() override { return COMMIT_MOUSE_ROTATE; } + + Glib::ustring _getTip(unsigned state) const override { + if (state_held_shift(state)) { + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Shift+Ctrl</b>: rotate around the opposite corner and snap " + "angle to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", "<b>Shift</b>: rotate around the opposite corner"); + } + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Ctrl</b>: snap angle to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", "<b>Rotation handle</b>: drag to rotate " + "the selection around the rotation center"); + } + + Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override { + return format_tip(C_("Transform handle tip", "Rotate by %.2f°"), + _last_angle * 180.0 / M_PI); + } + + bool _hasDragTips() const override { return true; } + +private: + Geom::Point _rot_center; + Geom::Point _rot_opposite; + unsigned _corner; + static double _last_angle; +}; +double RotateHandle::_last_angle = 0; + +class SkewHandle : public TransformHandle { +public: + SkewHandle(TransformHandleSet &th, unsigned side, unsigned d_side) + : TransformHandle(th, side_to_anchor(d_side), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW) + , _side(side) + {} + +protected: + + void startTransform() override { + _skew_center = _th.rotationCenter(); + Geom::Rect b = _th.bounds(); + _skew_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3)); + _last_angle = 0; + _last_horizontal = _side % 2; + } + + Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override + { + Geom::Point scc = held_shift(*event) ? _skew_center : _skew_opposite; + Geom::Dim2 d1 = static_cast<Geom::Dim2>((_side + 1) % 2); + Geom::Dim2 d2 = static_cast<Geom::Dim2>(_side % 2); + + Geom::Point const initial_delta = _origin - scc; + + if (fabs(initial_delta[d1]) < 1e-15) { + return Geom::Affine(); + } + + // Calculate the scale factors, which can be either visual or geometric + // depending on which type of bbox is currently being used (see preferences -> selector tool) + Geom::Scale scale = calcScaleFactors(_origin, new_pos, scc, false); + Geom::Scale skew = calcScaleFactors(_origin, new_pos, scc, true); + scale[d2] = 1; + skew[d2] = 1; + + // Skew handles allow scaling up to integer multiples of the original size + // in the second direction; prevent explosions + + if (fabs(scale[d1]) < 1) { + // Prevent shrinking of the selected object, while allowing mirroring + scale[d1] = copysign(1.0, scale[d1]); + } else { + // Allow expanding of the selected object by integer multiples + scale[d1] = floor(scale[d1] + 0.5); + } + + double angle = atan(skew[d1] / scale[d1]); + + if (held_control(*event)) { + angle = snap_angle(angle); + skew[d1] = tan(angle) * scale[d1]; + } else { + SnapManager &m = _th._desktop->namedview->snap_manager; + m.setupIgnoreSelection(_th._desktop, true, &_unselected_points); + + Inkscape::PureSkewConstrained psc = Inkscape::PureSkewConstrained(skew[d1], scale[d1], scc, d2); + m.snapTransformed(_snap_points, _origin, psc); + m.unSetup(); + + if (psc.best_snapped_point.getSnapped()) { + skew[d1] = psc.getSkewSnapped(); //best_snapped_point.getTransformation()[0]; + } + } + + _last_angle = angle; + + // Update the handle position + Geom::Point new_new_pos; + new_new_pos[d2] = initial_delta[d1] * skew[d1] + _origin[d2]; + new_new_pos[d1] = initial_delta[d1] * scale[d1] + scc[d1]; + + // Calculate the relative affine + Geom::Affine relative_affine = Geom::identity(); + relative_affine[2*d1 + d1] = (new_new_pos[d1] - scc[d1]) / initial_delta[d1]; + relative_affine[2*d1 + (d2)] = (new_new_pos[d2] - _origin[d2]) / initial_delta[d1]; + relative_affine[2*(d2) + (d1)] = 0; + relative_affine[2*(d2) + (d2)] = 1; + + for (int i = 0; i < 2; i++) { + if (fabs(relative_affine[3*i]) < 1e-15) { + relative_affine[3*i] = 1e-15; + } + } + + Geom::Affine t = Geom::Translate(-scc) + * relative_affine + * Geom::Translate(scc); + + return t; + } + + CommitEvent getCommitEvent() override { + return (_side % 2) + ? COMMIT_MOUSE_SKEW_Y + : COMMIT_MOUSE_SKEW_X; + } + + Glib::ustring _getTip(unsigned state) const override { + if (state_held_shift(state)) { + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Shift+Ctrl</b>: skew about the rotation center with snapping " + "to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", "<b>Shift</b>: skew about the rotation center"); + } + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Ctrl</b>: snap skew angle to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", + "<b>Skew handle</b>: drag to skew (shear) selection about " + "the opposite handle"); + } + + Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override { + if (_last_horizontal) { + return format_tip(C_("Transform handle tip", "Skew horizontally by %.2f°"), + _last_angle * 360.0); + } else { + return format_tip(C_("Transform handle tip", "Skew vertically by %.2f°"), + _last_angle * 360.0); + } + } + + bool _hasDragTips() const override { return true; } + +private: + + Geom::Point _skew_center; + Geom::Point _skew_opposite; + unsigned _side; + static bool _last_horizontal; + static double _last_angle; +}; +bool SkewHandle::_last_horizontal = false; +double SkewHandle::_last_angle = 0; + +class RotationCenter : public ControlPoint { + +public: + RotationCenter(TransformHandleSet &th) : + ControlPoint(th._desktop, Geom::Point(), SP_ANCHOR_CENTER, + Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER, + _center_cset, th._transform_handle_group), + _th(th) + { + setVisible(false); + } + +protected: + void dragged(Geom::Point &new_pos, GdkEventMotion *event) override { + SnapManager &sm = _th._desktop->namedview->snap_manager; + sm.setup(_th._desktop); + bool snap = !held_shift(*event) && sm.someSnapperMightSnap(); + if (held_control(*event)) { + // constrain to axes + Geom::Point origin = _last_drag_origin(); + std::vector<Inkscape::Snapper::SnapConstraint> constraints; + constraints.emplace_back(origin, Geom::Point(1, 0)); + constraints.emplace_back(origin, Geom::Point(0, 1)); + new_pos = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, + SNAPSOURCE_ROTATION_CENTER), constraints, held_shift(*event)).getPoint(); + } else if (snap) { + sm.freeSnapReturnByRef(new_pos, SNAPSOURCE_ROTATION_CENTER); + } + sm.unSetup(); + } + Glib::ustring _getTip(unsigned /*state*/) const override { + return C_("Transform handle tip", + "<b>Rotation center</b>: drag to change the origin of transforms"); + } + +private: + + static ColorSet _center_cset; + TransformHandleSet &_th; +}; + +ControlPoint::ColorSet RotationCenter::_center_cset = { + {0x000000ff, 0x000000ff}, // normal fill, stroke + {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke + {0x00ff66ff, 0x000000ff}, // clicked fill, stroke + // + {0x000000ff, 0x000000ff}, // normal fill, stroke when selected + {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke when selected + {0x00ff66ff, 0x000000ff} // clicked fill, stroke when selected +}; + + +TransformHandleSet::TransformHandleSet(SPDesktop *d, Inkscape::CanvasItemGroup *th_group) + : Manipulator(d) + , _active(nullptr) + , _transform_handle_group(th_group) + , _mode(MODE_SCALE) + , _in_transform(false) + , _visible(true) +{ + _trans_outline = new Inkscape::CanvasItemRect(_desktop->getCanvasControls()); + _trans_outline->set_name("CanvasItemRect:Transform"); + _trans_outline->hide(); + _trans_outline->set_dashed(true); + + bool y_inverted = !d->is_yaxisdown(); + for (unsigned i = 0; i < 4; ++i) { + unsigned d_c = y_inverted ? i : 3 - i; + unsigned d_s = y_inverted ? i : 6 - i; + _scale_corners[i] = new ScaleCornerHandle(*this, i, d_c); + _scale_sides[i] = new ScaleSideHandle(*this, i, d_s); + _rot_corners[i] = new RotateHandle(*this, i, d_c); + _skew_sides[i] = new SkewHandle(*this, i, d_s); + } + _center = new RotationCenter(*this); + // when transforming, update rotation center position + signal_transform.connect(sigc::mem_fun(*_center, &RotationCenter::transform)); +} + +TransformHandleSet::~TransformHandleSet() +{ + for (auto & _handle : _handles) { + delete _handle; + } +} + +void TransformHandleSet::setMode(Mode m) +{ + _mode = m; + _updateVisibility(_visible); +} + +Geom::Rect TransformHandleSet::bounds() const +{ + return Geom::Rect(*_scale_corners[0], *_scale_corners[2]); +} + +ControlPoint const &TransformHandleSet::rotationCenter() const +{ + return *_center; +} + +ControlPoint &TransformHandleSet::rotationCenter() +{ + return *_center; +} + +void TransformHandleSet::setVisible(bool v) +{ + if (_visible != v) { + _visible = v; + _updateVisibility(_visible); + } +} + +void TransformHandleSet::setBounds(Geom::Rect const &r, bool preserve_center) +{ + if (_in_transform) { + _trans_outline->set_rect(r); + } else { + for (unsigned i = 0; i < 4; ++i) { + _scale_corners[i]->move(r.corner(i)); + _scale_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1))); + _rot_corners[i]->move(r.corner(i)); + _skew_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1))); + } + if (!preserve_center) _center->move(r.midpoint()); + if (_visible) _updateVisibility(true); + } +} + +bool TransformHandleSet::event(Inkscape::UI::Tools::ToolBase *, GdkEvent*) +{ + return false; +} + +void TransformHandleSet::_emitTransform(Geom::Affine const &t) +{ + signal_transform.emit(t); + _center->transform(t); +} + +void TransformHandleSet::_setActiveHandle(ControlPoint *th) +{ + _active = th; + if (_in_transform) + throw std::logic_error("Transform initiated when another transform in progress"); + _in_transform = true; + // hide all handles except the active one + _updateVisibility(false); + _trans_outline->show(); +} + +void TransformHandleSet::_clearActiveHandle() +{ + // This can only be called from handles, so they had to be visible before _setActiveHandle + _trans_outline->hide(); + _active = nullptr; + _in_transform = false; + _updateVisibility(_visible); +} + +void TransformHandleSet::_updateVisibility(bool v) +{ + if (v) { + Geom::Rect b = bounds(); + + // Roughly estimate handle size. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int handle_index = prefs->getIntLimited("/options/grabsize/value", 3, 1, 15); + int handle_size = handle_index * 2 + 1; // Handle pixmaps are actually larger but that's to allow space when handle is rotated. + + Geom::Point bp = b.dimensions(); + + // do not scale when the bounding rectangle has zero width or height + bool show_scale = (_mode == MODE_SCALE) && !Geom::are_near(b.minExtent(), 0); + // do not rotate if the bounding rectangle is degenerate + bool show_rotate = (_mode == MODE_ROTATE_SKEW) && !Geom::are_near(b.maxExtent(), 0); + bool show_scale_side[2], show_skew[2]; + + // show sides if: + // a) there is enough space between corner handles, or + // b) corner handles are not shown, but side handles make sense + // this affects horizontal and vertical scale handles; skew handles never + // make sense if rotate handles are not shown + for (unsigned i = 0; i < 2; ++i) { + Geom::Dim2 d = static_cast<Geom::Dim2>(i); + Geom::Dim2 otherd = static_cast<Geom::Dim2>((i+1)%2); + show_scale_side[i] = (_mode == MODE_SCALE); + show_scale_side[i] &= (show_scale ? bp[d] >= handle_size + : !Geom::are_near(bp[otherd], 0)); + show_skew[i] = (show_rotate && bp[d] >= handle_size + && !Geom::are_near(bp[otherd], 0)); + } + + for (unsigned i = 0; i < 4; ++i) { + _scale_corners[i]->setVisible(show_scale); + _rot_corners[i]->setVisible(show_rotate); + _scale_sides[i]->setVisible(show_scale_side[i%2]); + _skew_sides[i]->setVisible(show_skew[i%2]); + } + + // show rotation center + _center->setVisible(show_rotate); + } else { + for (auto & _handle : _handles) { + if (_handle != _active) + _handle->setVisible(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/transform-handle-set.h b/src/ui/tool/transform-handle-set.h new file mode 100644 index 0000000..4b6877c --- /dev/null +++ b/src/ui/tool/transform-handle-set.h @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Affine transform handles component + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_TRANSFORM_HANDLE_SET_H +#define SEEN_UI_TOOL_TRANSFORM_HANDLE_SET_H + +#include <memory> +#include <gdk/gdk.h> +#include <2geom/forward.h> +#include "ui/tool/commit-events.h" +#include "ui/tool/manipulator.h" +#include "ui/tool/control-point.h" +#include "enums.h" +#include "snap-candidate.h" + +class SPDesktop; + +namespace Inkscape { + +class CanvasItemGroup; +class CanvasItemRect; + +namespace UI { + +class RotateHandle; +class SkewHandle; +class ScaleCornerHandle; +class ScaleSideHandle; +class RotationCenter; + +class TransformHandleSet : public Manipulator { +public: + + enum Mode { + MODE_SCALE, + MODE_ROTATE_SKEW + }; + + TransformHandleSet(SPDesktop *d, Inkscape::CanvasItemGroup *th_group); + ~TransformHandleSet() override; + bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override; + + bool visible() const { return _visible; } + Mode mode() const { return _mode; } + Geom::Rect bounds() const; + void setVisible(bool v); + + /** Sets the mode of transform handles (scale or rotate). */ + void setMode(Mode m); + + void setBounds(Geom::Rect const &, bool preserve_center = false); + + bool transforming() { return _in_transform; } + + ControlPoint const &rotationCenter() const; + ControlPoint &rotationCenter(); + + sigc::signal<void, Geom::Affine const &> signal_transform; + sigc::signal<void, CommitEvent> signal_commit; + +private: + + void _emitTransform(Geom::Affine const &); + void _setActiveHandle(ControlPoint *h); + void _clearActiveHandle(); + + /** Update the visibility of transformation handles according to settings and the dimensions + * of the bounding box. It hides the handles that would have no effect or lead to + * discontinuities. Additionally, side handles for which there is no space are not shown. + */ + void _updateVisibility(bool v); + + // TODO unions must GO AWAY: + union { + ControlPoint *_handles[17]; + struct { + ScaleCornerHandle *_scale_corners[4]; + ScaleSideHandle *_scale_sides[4]; + RotateHandle *_rot_corners[4]; + SkewHandle *_skew_sides[4]; + RotationCenter *_center; + }; + }; + + ControlPoint *_active; + Inkscape::CanvasItemGroup *_transform_handle_group; + Inkscape::CanvasItemRect *_trans_outline; + Mode _mode; + bool _in_transform; + bool _visible; + friend class TransformHandle; + friend class RotationCenter; +}; + +/** Base class for node transform handles to simplify implementation. */ +class TransformHandle : public ControlPoint +{ +public: + TransformHandle(TransformHandleSet &th, SPAnchorType anchor, Inkscape::CanvasItemCtrlType type); + void getNextClosestPoint(bool reverse); + +protected: + virtual void startTransform() {} + virtual void endTransform() {} + virtual Geom::Affine computeTransform(Geom::Point const &pos, GdkEventMotion *event) = 0; + virtual CommitEvent getCommitEvent() = 0; + + Geom::Affine _last_transform; + Geom::Point _origin; + TransformHandleSet &_th; + std::vector<Inkscape::SnapCandidatePoint> _snap_points; + std::vector<Inkscape::SnapCandidatePoint> _unselected_points; + std::vector<Inkscape::SnapCandidatePoint> _all_snap_sources_sorted; + std::vector<Inkscape::SnapCandidatePoint>::iterator _all_snap_sources_iter; + +private: + bool grabbed(GdkEventMotion *) override; + void dragged(Geom::Point &new_pos, GdkEventMotion *event) override; + void ungrabbed(GdkEventButton *) override; + + static ColorSet thandle_cset; +}; + +} // 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/toolbar/arc-toolbar.cpp b/src/ui/toolbar/arc-toolbar.cpp new file mode 100644 index 0000000..b3493e8 --- /dev/null +++ b/src/ui/toolbar/arc-toolbar.cpp @@ -0,0 +1,559 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Arc aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "arc-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "mod360.h" +#include "selection.h" + +#include "object/sp-ellipse.h" +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/tools/arc-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +#include "widgets/widget-sizes.h" + +#include "xml/node-event-vector.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::DocumentUndo; +using Inkscape::Util::Quantity; +using Inkscape::Util::unit_table; + + +static Inkscape::XML::NodeEventVector arc_tb_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::ArcToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +ArcToolbar::ArcToolbar(SPDesktop *desktop) : + Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)), + _freeze(false), + _repr(nullptr) +{ + auto init_units = desktop->getNamedView()->display_units; + _tracker->setActiveUnit(init_units); + auto prefs = Inkscape::Preferences::get(); + + { + _mode_item = Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>"))); + _mode_item->set_use_markup(true); + add(*_mode_item); + } + + /* Radius X */ + { + std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + auto rx_val = prefs->getDouble("/tools/shapes/arc/rx", 0); + rx_val = Quantity::convert(rx_val, "px", init_units); + + _rx_adj = Gtk::Adjustment::create(rx_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _rx_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-rx", _("Rx:"), _rx_adj)); + _rx_item->set_tooltip_text(_("Horizontal radius of the circle, ellipse, or arc")); + _rx_item->set_custom_numeric_menu_data(values); + _tracker->addAdjustment(_rx_adj->gobj()); + _rx_item->get_spin_button()->addUnitTracker(_tracker); + _rx_item->set_focus_widget(desktop->canvas); + _rx_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::value_changed), + _rx_adj, "rx")); + _rx_item->set_sensitive(false); + add(*_rx_item); + } + + /* Radius Y */ + { + std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + auto ry_val = prefs->getDouble("/tools/shapes/arc/ry", 0); + ry_val = Quantity::convert(ry_val, "px", init_units); + + _ry_adj = Gtk::Adjustment::create(ry_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _ry_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-ry", _("Ry:"), _ry_adj)); + _ry_item->set_tooltip_text(_("Vertical radius of the circle, ellipse, or arc")); + _ry_item->set_custom_numeric_menu_data(values); + _tracker->addAdjustment(_ry_adj->gobj()); + _ry_item->get_spin_button()->addUnitTracker(_tracker); + _ry_item->set_focus_widget(desktop->canvas); + _ry_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::value_changed), + _ry_adj, "ry")); + _ry_item->set_sensitive(false); + add(*_ry_item); + } + + // add the units menu + { + auto unit_menu = _tracker->create_tool_item(_("Units"), ("") ); + add(*unit_menu); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Start */ + { + auto start_val = prefs->getDouble("/tools/shapes/arc/start", 0.0); + _start_adj = Gtk::Adjustment::create(start_val, -360.0, 360.0, 1.0, 10.0); + auto eact = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-start", _("Start:"), _start_adj)); + eact->set_tooltip_text(_("The angle (in degrees) from the horizontal to the arc's start point")); + eact->set_focus_widget(desktop->canvas); + add(*eact); + } + + /* End */ + { + auto end_val = prefs->getDouble("/tools/shapes/arc/end", 0.0); + _end_adj = Gtk::Adjustment::create(end_val, -360.0, 360.0, 1.0, 10.0); + auto eact = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-end", _("End:"), _end_adj)); + eact->set_tooltip_text(_("The angle (in degrees) from the horizontal to the arc's end point")); + eact->set_focus_widget(desktop->canvas); + add(*eact); + } + _start_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::startend_value_changed), + _start_adj, "start", _end_adj)); + _end_adj->signal_value_changed().connect( sigc::bind(sigc::mem_fun(*this, &ArcToolbar::startend_value_changed), + _end_adj, "end", _start_adj)); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Arc: Slice, Arc, Chord */ + { + Gtk::RadioToolButton::Group type_group; + + auto slice_btn = Gtk::manage(new Gtk::RadioToolButton(_("Slice"))); + slice_btn->set_tooltip_text(_("Switch to slice (closed shape with two radii)")); + slice_btn->set_icon_name(INKSCAPE_ICON("draw-ellipse-segment")); + _type_buttons.push_back(slice_btn); + + auto arc_btn = Gtk::manage(new Gtk::RadioToolButton(_("Arc (Open)"))); + arc_btn->set_tooltip_text(_("Switch to arc (unclosed shape)")); + arc_btn->set_icon_name(INKSCAPE_ICON("draw-ellipse-arc")); + _type_buttons.push_back(arc_btn); + + auto chord_btn = Gtk::manage(new Gtk::RadioToolButton(_("Chord"))); + chord_btn->set_tooltip_text(_("Switch to chord (closed shape)")); + chord_btn->set_icon_name(INKSCAPE_ICON("draw-ellipse-chord")); + _type_buttons.push_back(chord_btn); + + slice_btn->set_group(type_group); + arc_btn->set_group(type_group); + chord_btn->set_group(type_group); + + gint type = prefs->getInt("/tools/shapes/arc/arc_type", 0); + _type_buttons[type]->set_active(); + + int btn_index = 0; + for (auto btn : _type_buttons) + { + btn->set_sensitive(); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::type_changed), btn_index++)); + add(*btn); + } + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Make Whole */ + { + _make_whole = Gtk::manage(new Gtk::ToolButton(_("Make whole"))); + _make_whole->set_tooltip_text(_("Make the shape a whole ellipse, not arc or segment")); + _make_whole->set_icon_name(INKSCAPE_ICON("draw-ellipse-whole")); + _make_whole->signal_clicked().connect(sigc::mem_fun(*this, &ArcToolbar::defaults)); + add(*_make_whole); + _make_whole->set_sensitive(true); + } + + _single = true; + // sensitivize make whole and open checkbox + { + sensitivize( _start_adj->get_value(), _end_adj->get_value() ); + } + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &ArcToolbar::check_ec)); + + show_all(); +} + +ArcToolbar::~ArcToolbar() +{ + if(_repr) { + _repr->removeListenerByData(this); + GC::release(_repr); + _repr = nullptr; + } +} + +GtkWidget * +ArcToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new ArcToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +ArcToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name) +{ + // Per SVG spec "a [radius] value of zero disables rendering of the element". + // However our implementation does not allow a setting of zero in the UI (not even in the XML editor) + // and ugly things happen if it's forced here, so better leave the properties untouched. + if (!adj->get_value()) { + return; + } + + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + SPDocument* document = _desktop->getDocument(); + + if (DocumentUndo::getUndoSensitive(document)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/shapes/arc/") + value_name, + Quantity::convert(adj->get_value(), unit, "px")); + } + + // quit if run by the attr_changed listener + if (_freeze || _tracker->isUpdating()) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_GENERICELLIPSE(item)) { + + SPGenericEllipse *ge = SP_GENERICELLIPSE(item); + + if (!strcmp(value_name, "rx")) { + ge->setVisibleRx(Quantity::convert(adj->get_value(), unit, "px")); + } else { + ge->setVisibleRy(Quantity::convert(adj->get_value(), unit, "px")); + } + + ge->normalize(); + ge->updateRepr(); + ge->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + + modmade = true; + } + } + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), _("Ellipse: Change radius"), INKSCAPE_ICON("draw-ellipse")); + } + + _freeze = false; +} + +void +ArcToolbar::startend_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name, + Glib::RefPtr<Gtk::Adjustment>& other_adj) +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/shapes/arc/") + value_name, adj->get_value()); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + gchar* namespaced_name = g_strconcat("sodipodi:", value_name, nullptr); + + bool modmade = false; + auto itemlist= _desktop->getSelection()->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_GENERICELLIPSE(item)) { + + SPGenericEllipse *ge = SP_GENERICELLIPSE(item); + + if (!strcmp(value_name, "start")) { + ge->start = (adj->get_value() * M_PI)/ 180; + } else { + ge->end = (adj->get_value() * M_PI)/ 180; + } + + ge->normalize(); + ge->updateRepr(); + ge->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + + modmade = true; + } + } + + g_free(namespaced_name); + + sensitivize( adj->get_value(), other_adj->get_value() ); + + if (modmade) { + DocumentUndo::maybeDone(_desktop->getDocument(), value_name, _("Arc: Change start/end"), INKSCAPE_ICON("draw-ellipse")); + } + + _freeze = false; +} + +void +ArcToolbar::type_changed( int type ) +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/shapes/arc/arc_type", type); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + Glib::ustring arc_type = "slice"; + bool open = false; + switch (type) { + case 0: + arc_type = "slice"; + open = false; + break; + case 1: + arc_type = "arc"; + open = true; + break; + case 2: + arc_type = "chord"; + open = true; // For backward compat, not truly open but chord most like arc. + break; + default: + std::cerr << "sp_arctb_type_changed: bad arc type: " << type << std::endl; + } + + bool modmade = false; + auto itemlist= _desktop->getSelection()->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_GENERICELLIPSE(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + repr->setAttribute("sodipodi:open", (open?"true":nullptr) ); + repr->setAttribute("sodipodi:arc-type", arc_type); + item->updateRepr(); + modmade = true; + } + } + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), _("Arc: Changed arc type"), INKSCAPE_ICON("draw-ellipse")); + } + + _freeze = false; +} + +void +ArcToolbar::defaults() +{ + _start_adj->set_value(0.0); + _end_adj->set_value(0.0); + + if(_desktop->canvas) _desktop->canvas->grab_focus(); +} + +void +ArcToolbar::sensitivize( double v1, double v2 ) +{ + if (v1 == 0 && v2 == 0) { + if (_single) { // only for a single selected ellipse (for now) + for (auto btn : _type_buttons) btn->set_sensitive(false); + _make_whole->set_sensitive(false); + } + } else { + for (auto btn : _type_buttons) btn->set_sensitive(true); + _make_whole->set_sensitive(true); + } +} + +void +ArcToolbar::check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (SP_IS_ARC_CONTEXT(ec)) { + _changed = _desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &ArcToolbar::selection_changed)); + selection_changed(desktop->getSelection()); + } else { + if (_changed) { + _changed.disconnect(); + if(_repr) { + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + } + } +} + +void +ArcToolbar::selection_changed(Inkscape::Selection *selection) +{ + int n_selected = 0; + Inkscape::XML::Node *repr = nullptr; + + if ( _repr ) { + _item = nullptr; + _repr->removeListenerByData(this); + GC::release(_repr); + _repr = nullptr; + } + + SPItem *item = nullptr; + + for(auto i : selection->items()){ + if (SP_IS_GENERICELLIPSE(i)) { + n_selected++; + item = i; + repr = item->getRepr(); + } + } + + _single = false; + if (n_selected == 0) { + _mode_item->set_markup(_("<b>New:</b>")); + } else if (n_selected == 1) { + _single = true; + _mode_item->set_markup(_("<b>Change:</b>")); + _rx_item->set_sensitive(true); + _ry_item->set_sensitive(true); + + if (repr) { + _repr = repr; + _item = item; + Inkscape::GC::anchor(_repr); + _repr->addListener(&arc_tb_repr_events, this); + _repr->synthesizeEvents(&arc_tb_repr_events, this); + } + } else { + // FIXME: implement averaging of all parameters for multiple selected + //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>")); + _mode_item->set_markup(_("<b>Change:</b>")); + sensitivize( 1, 0 ); + } +} + +void +ArcToolbar::event_attr_changed(Inkscape::XML::Node *repr, gchar const * /*name*/, + gchar const * /*old_value*/, gchar const * /*new_value*/, + bool /*is_interactive*/, gpointer data) +{ + auto toolbar = reinterpret_cast<ArcToolbar *>(data); + + // quit if run by the _changed callbacks + if (toolbar->_freeze) { + return; + } + + // in turn, prevent callbacks from responding + toolbar->_freeze = true; + + if (toolbar->_item && SP_IS_GENERICELLIPSE(toolbar->_item)) { + SPGenericEllipse *ge = SP_GENERICELLIPSE(toolbar->_item); + + Unit const *unit = toolbar->_tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + gdouble rx = ge->getVisibleRx(); + gdouble ry = ge->getVisibleRy(); + toolbar->_rx_adj->set_value(Quantity::convert(rx, "px", unit)); + toolbar->_ry_adj->set_value(Quantity::convert(ry, "px", unit)); + } + + gdouble start = repr->getAttributeDouble("sodipodi:start", 0.0);; + gdouble end = repr->getAttributeDouble("sodipodi:end", 0.0); + + toolbar->_start_adj->set_value(mod360((start * 180)/M_PI)); + toolbar->_end_adj->set_value(mod360((end * 180)/M_PI)); + + toolbar->sensitivize(toolbar->_start_adj->get_value(), toolbar->_end_adj->get_value()); + + char const *arctypestr = nullptr; + arctypestr = repr->attribute("sodipodi:arc-type"); + if (!arctypestr) { // For old files. + char const *openstr = nullptr; + openstr = repr->attribute("sodipodi:open"); + arctypestr = (openstr ? "arc" : "slice"); + } + + if (!strcmp(arctypestr,"slice")) { + toolbar->_type_buttons[0]->set_active(); + } else if (!strcmp(arctypestr,"arc")) { + toolbar->_type_buttons[1]->set_active(); + } else { + toolbar->_type_buttons[2]->set_active(); + } + + toolbar->_freeze = false; +} + +} +} +} + + +/* + 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/toolbar/arc-toolbar.h b/src/ui/toolbar/arc-toolbar.h new file mode 100644 index 0000000..b0b0450 --- /dev/null +++ b/src/ui/toolbar/arc-toolbar.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_ARC_TOOLBAR_H +#define SEEN_ARC_TOOLBAR_H + +/** + * @file + * 3d box aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; +class SPItem; + +namespace Gtk { +class RadioToolButton; +class ToolButton; +} + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class LabelToolItem; +class SpinButtonToolItem; +class UnitTracker; +} + +namespace Toolbar { +class ArcToolbar : public Toolbar { +private: + UI::Widget::UnitTracker *_tracker; + + UI::Widget::SpinButtonToolItem *_rx_item; + UI::Widget::SpinButtonToolItem *_ry_item; + + UI::Widget::LabelToolItem *_mode_item; + + std::vector<Gtk::RadioToolButton *> _type_buttons; + Gtk::ToolButton *_make_whole; + + Glib::RefPtr<Gtk::Adjustment> _rx_adj; + Glib::RefPtr<Gtk::Adjustment> _ry_adj; + Glib::RefPtr<Gtk::Adjustment> _start_adj; + Glib::RefPtr<Gtk::Adjustment> _end_adj; + + bool _freeze; + bool _single; + + XML::Node *_repr; + SPItem *_item; + + void value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name); + void startend_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name, + Glib::RefPtr<Gtk::Adjustment>& other_adj); + void type_changed( int type ); + void defaults(); + void sensitivize( double v1, double v2 ); + void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_changed(Inkscape::Selection *selection); + + sigc::connection _changed; + +protected: + ArcToolbar(SPDesktop *desktop); + ~ArcToolbar() override; + +public: + static GtkWidget * create(SPDesktop *desktop); + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const *old_value, + gchar const *new_value, + bool is_interactive, + gpointer data); +}; + +} +} +} + +#endif /* !SEEN_ARC_TOOLBAR_H */ diff --git a/src/ui/toolbar/box3d-toolbar.cpp b/src/ui/toolbar/box3d-toolbar.cpp new file mode 100644 index 0000000..e594240 --- /dev/null +++ b/src/ui/toolbar/box3d-toolbar.cpp @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * 3d box aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "box3d-toolbar.h" + +#include <glibmm/i18n.h> +#include <gtkmm/adjustment.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "selection.h" + +#include "object/box3d.h" +#include "object/persp3d.h" + +#include "ui/icon-names.h" +#include "ui/tools/box3d-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +static Inkscape::XML::NodeEventVector box3d_persp_tb_repr_events = +{ + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::Box3DToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +Box3DToolbar::Box3DToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _repr(nullptr), + _freeze(false) +{ + auto prefs = Inkscape::Preferences::get(); + auto document = desktop->getDocument(); + auto persp_impl = document->getCurrentPersp3DImpl(); + + /* Angle X */ + { + std::vector<double> values = {-90, -60, -30, 0, 30, 60, 90}; + auto angle_x_val = prefs->getDouble("/tools/shapes/3dbox/box3d_angle_x", 30); + _angle_x_adj = Gtk::Adjustment::create(angle_x_val, -360.0, 360.0, 1.0, 10.0); + _angle_x_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("box3d-angle-x", _("Angle X:"), _angle_x_adj)); + // TRANSLATORS: PL is short for 'perspective line' + _angle_x_item->set_tooltip_text(_("Angle of PLs in X direction")); + _angle_x_item->set_custom_numeric_menu_data(values); + _angle_x_item->set_focus_widget(desktop->canvas); + _angle_x_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::angle_value_changed), + _angle_x_adj, Proj::X)); + add(*_angle_x_item); + } + + if (!persp_impl || !Persp3D::VP_is_finite(persp_impl, Proj::X)) { + _angle_x_item->set_sensitive(true); + } else { + _angle_x_item->set_sensitive(false); + } + + /* VP X state */ + { + // TRANSLATORS: VP is short for 'vanishing point' + _vp_x_state_item = add_toggle_button(_("State of VP in X direction"), + _("Toggle VP in X direction between 'finite' and 'infinite' (=parallel)")); + _vp_x_state_item->set_icon_name(INKSCAPE_ICON("perspective-parallel")); + _vp_x_state_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::vp_state_changed), Proj::X)); + _angle_x_item->set_sensitive( !prefs->getBool("/tools/shapes/3dbox/vp_x_state", true) ); + _vp_x_state_item->set_active( prefs->getBool("/tools/shapes/3dbox/vp_x_state", true) ); + } + + /* Angle Y */ + { + auto angle_y_val = prefs->getDouble("/tools/shapes/3dbox/box3d_angle_y", 30); + _angle_y_adj = Gtk::Adjustment::create(angle_y_val, -360.0, 360.0, 1.0, 10.0); + _angle_y_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("box3d-angle-y", _("Angle Y:"), _angle_y_adj)); + // TRANSLATORS: PL is short for 'perspective line' + _angle_y_item->set_tooltip_text(_("Angle of PLs in Y direction")); + std::vector<double> values = {-90, -60, -30, 0, 30, 60, 90}; + _angle_y_item->set_custom_numeric_menu_data(values); + _angle_y_item->set_focus_widget(desktop->canvas); + _angle_y_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::angle_value_changed), + _angle_y_adj, Proj::Y)); + add(*_angle_y_item); + } + + if (!persp_impl || !Persp3D::VP_is_finite(persp_impl, Proj::Y)) { + _angle_y_item->set_sensitive(true); + } else { + _angle_y_item->set_sensitive(false); + } + + /* VP Y state */ + { + // TRANSLATORS: VP is short for 'vanishing point' + _vp_y_state_item = add_toggle_button(_("State of VP in Y direction"), + _("Toggle VP in Y direction between 'finite' and 'infinite' (=parallel)")); + _vp_y_state_item->set_icon_name(INKSCAPE_ICON("perspective-parallel")); + _vp_y_state_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::vp_state_changed), Proj::Y)); + _angle_y_item->set_sensitive( !prefs->getBool("/tools/shapes/3dbox/vp_y_state", true) ); + _vp_y_state_item->set_active( prefs->getBool("/tools/shapes/3dbox/vp_y_state", true) ); + } + + /* Angle Z */ + { + auto angle_z_val = prefs->getDouble("/tools/shapes/3dbox/box3d_angle_z", 30); + _angle_z_adj = Gtk::Adjustment::create(angle_z_val, -360.0, 360.0, 1.0, 10.0); + _angle_z_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("box3d-angle-z", _("Angle Z:"), _angle_z_adj)); + // TRANSLATORS: PL is short for 'perspective line' + _angle_z_item->set_tooltip_text(_("Angle of PLs in Z direction")); + std::vector<double> values = {-90, -60, -30, 0, 30, 60, 90}; + _angle_z_item->set_custom_numeric_menu_data(values); + _angle_z_item->set_focus_widget(desktop->canvas); + _angle_z_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::angle_value_changed), + _angle_z_adj, Proj::Z)); + add(*_angle_z_item); + } + + if (!persp_impl || !Persp3D::VP_is_finite(persp_impl, Proj::Z)) { + _angle_z_item->set_sensitive(true); + } else { + _angle_z_item->set_sensitive(false); + } + + /* VP Z state */ + { + // TRANSLATORS: VP is short for 'vanishing point' + _vp_z_state_item = add_toggle_button(_("State of VP in Z direction"), + _("Toggle VP in Z direction between 'finite' and 'infinite' (=parallel)")); + _vp_z_state_item->set_icon_name(INKSCAPE_ICON("perspective-parallel")); + _vp_z_state_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::vp_state_changed), Proj::Z)); + _angle_z_item->set_sensitive(!prefs->getBool("/tools/shapes/3dbox/vp_z_state", true)); + _vp_z_state_item->set_active( prefs->getBool("/tools/shapes/3dbox/vp_z_state", true) ); + } + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &Box3DToolbar::check_ec)); + + show_all(); +} + +GtkWidget * +Box3DToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new Box3DToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +Box3DToolbar::angle_value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, + Proj::Axis axis) +{ + SPDocument *document = _desktop->getDocument(); + + // quit if run by the attr_changed or selection changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + std::list<Persp3D *> sel_persps = _desktop->getSelection()->perspList(); + if (sel_persps.empty()) { + // this can happen when the document is created; we silently ignore it + return; + } + Persp3D *persp = sel_persps.front(); + + persp->perspective_impl->tmat.set_infinite_direction (axis, + adj->get_value()); + persp->updateRepr(); + + // TODO: use the correct axis here, too + DocumentUndo::maybeDone(document, "perspangle", _("3D Box: Change perspective (angle of infinite axis)"), INKSCAPE_ICON("draw-cuboid")); + + _freeze = false; +} + +void +Box3DToolbar::vp_state_changed(Proj::Axis axis) +{ + // TODO: Take all selected perspectives into account + auto sel_persps = _desktop->getSelection()->perspList(); + if (sel_persps.empty()) { + // this can happen when the document is created; we silently ignore it + return; + } + Persp3D *persp = sel_persps.front(); + + Gtk::ToggleToolButton *btn = nullptr; + + switch(axis) { + case Proj::X: + btn = _vp_x_state_item; + break; + case Proj::Y: + btn = _vp_y_state_item; + break; + case Proj::Z: + btn = _vp_z_state_item; + break; + default: + return; + } + + bool set_infinite = btn->get_active(); + persp->set_VP_state (axis, set_infinite ? Proj::VP_INFINITE : Proj::VP_FINITE); +} + +void +Box3DToolbar::check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (SP_IS_BOX3D_CONTEXT(ec)) { + _changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &Box3DToolbar::selection_changed)); + selection_changed(desktop->getSelection()); + } else { + if (_changed) + _changed.disconnect(); + + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + } +} + +Box3DToolbar::~Box3DToolbar() +{ + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } +} + +/** + * \param selection Should not be NULL. + */ +// FIXME: This should rather be put into persp3d-reference.cpp or something similar so that it reacts upon each +// Change of the perspective, and not of the current selection (but how to refer to the toolbar then?) +void +Box3DToolbar::selection_changed(Inkscape::Selection *selection) +{ + // Here the following should be done: If all selected boxes have finite VPs in a certain direction, + // disable the angle entry fields for this direction (otherwise entering a value in them should only + // update the perspectives with infinite VPs and leave the other ones untouched). + + Inkscape::XML::Node *persp_repr = nullptr; + + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + + SPItem *item = selection->singleItem(); + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + // FIXME: Also deal with multiple selected boxes + Persp3D *persp = box->get_perspective(); + if (!persp) { + g_warning("Box has no perspective set!"); + return; + } + persp_repr = persp->getRepr(); + if (persp_repr) { + _repr = persp_repr; + Inkscape::GC::anchor(_repr); + _repr->addListener(&box3d_persp_tb_repr_events, this); + _repr->synthesizeEvents(&box3d_persp_tb_repr_events, this); + + selection->document()->setCurrentPersp3D(Persp3D::get_from_repr(_repr)); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/tools/shapes/3dbox/persp", _repr->attribute("id")); + + _freeze = true; + resync_toolbar(_repr); + _freeze = false; + } + } +} + +void +Box3DToolbar::resync_toolbar(Inkscape::XML::Node *persp_repr) +{ + if (!persp_repr) { + g_print ("No perspective given to box3d_resync_toolbar().\n"); + return; + } + + Persp3D *persp = Persp3D::get_from_repr(persp_repr); + if (!persp) { + // Hmm, is it an error if this happens? + return; + } + set_button_and_adjustment(persp, Proj::X, + _angle_x_adj, + _angle_x_item, + _vp_x_state_item); + set_button_and_adjustment(persp, Proj::Y, + _angle_y_adj, + _angle_y_item, + _vp_y_state_item); + set_button_and_adjustment(persp, Proj::Z, + _angle_z_adj, + _angle_z_item, + _vp_z_state_item); +} + +void +Box3DToolbar::set_button_and_adjustment(Persp3D *persp, + Proj::Axis axis, + Glib::RefPtr<Gtk::Adjustment>& adj, + UI::Widget::SpinButtonToolItem *spin_btn, + Gtk::ToggleToolButton *toggle_btn) +{ + // TODO: Take all selected perspectives into account but don't touch the state button if not all of them + // have the same state (otherwise a call to box3d_vp_z_state_changed() is triggered and the states + // are reset). + bool is_infinite = !Persp3D::VP_is_finite(persp->perspective_impl, axis); + + if (is_infinite) { + toggle_btn->set_active(true); + spin_btn->set_sensitive(true); + + double angle = persp->get_infinite_angle(axis); + if (angle != Geom::infinity()) { // FIXME: We should catch this error earlier (don't show the spinbutton at all) + adj->set_value(normalize_angle(angle)); + } + } else { + toggle_btn->set_active(false); + spin_btn->set_sensitive(false); + } +} + +void +Box3DToolbar::event_attr_changed(Inkscape::XML::Node *repr, + gchar const * /*name*/, + gchar const * /*old_value*/, + gchar const * /*new_value*/, + bool /*is_interactive*/, + gpointer data) +{ + auto toolbar = reinterpret_cast<Box3DToolbar*>(data); + + // quit if run by the attr_changed or selection changed listener + if (toolbar->_freeze) { + return; + } + + // set freeze so that it can be caught in box3d_angle_z_value_changed() (to avoid calling + // SPDocumentUndo::maybeDone() when the document is undo insensitive) + toolbar->_freeze = true; + + // TODO: Only update the appropriate part of the toolbar +// if (!strcmp(name, "inkscape:vp_z")) { + toolbar->resync_toolbar(repr); +// } + + Persp3D *persp = Persp3D::get_from_repr(repr); + if (persp) { + persp->update_box_reprs(); + } + + toolbar->_freeze = false; +} + +/** + * \brief normalize angle so that it lies in the interval [0,360] + * + * TODO: Isn't there something in 2Geom or cmath that does this? + */ +double +Box3DToolbar::normalize_angle(double a) { + double angle = a + ((int) (a/360.0))*360; + if (angle < 0) { + angle += 360.0; + } + return angle; +} + +} +} +} + + +/* + 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/toolbar/box3d-toolbar.h b/src/ui/toolbar/box3d-toolbar.h new file mode 100644 index 0000000..8ba7872 --- /dev/null +++ b/src/ui/toolbar/box3d-toolbar.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_BOX3D_TOOLBAR_H +#define SEEN_BOX3D_TOOLBAR_H + +/** + * @file + * 3d box aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include "axis-manip.h" + +namespace Gtk { +class Adjustment; +} + +class Persp3D; +class SPDesktop; + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Widget { +class SpinButtonToolItem; +} + +namespace Tools { +class ToolBase; +} + +namespace Toolbar { +class Box3DToolbar : public Toolbar { +private: + UI::Widget::SpinButtonToolItem *_angle_x_item; + UI::Widget::SpinButtonToolItem *_angle_y_item; + UI::Widget::SpinButtonToolItem *_angle_z_item; + + Glib::RefPtr<Gtk::Adjustment> _angle_x_adj; + Glib::RefPtr<Gtk::Adjustment> _angle_y_adj; + Glib::RefPtr<Gtk::Adjustment> _angle_z_adj; + + Gtk::ToggleToolButton *_vp_x_state_item; + Gtk::ToggleToolButton *_vp_y_state_item; + Gtk::ToggleToolButton *_vp_z_state_item; + + XML::Node *_repr; + bool _freeze; + + void angle_value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, + Proj::Axis axis); + void vp_state_changed(Proj::Axis axis); + void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_changed(Inkscape::Selection *selection); + void resync_toolbar(Inkscape::XML::Node *persp_repr); + void set_button_and_adjustment(Persp3D *persp, + Proj::Axis axis, + Glib::RefPtr<Gtk::Adjustment>& adj, + UI::Widget::SpinButtonToolItem *spin_btn, + Gtk::ToggleToolButton *toggle_btn); + double normalize_angle(double a); + + sigc::connection _changed; + +protected: + Box3DToolbar(SPDesktop *desktop); + ~Box3DToolbar() override; + +public: + static GtkWidget * create(SPDesktop *desktop); + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const *old_value, + gchar const *new_value, + bool is_interactive, + gpointer data); + +}; +} +} +} +#endif /* !SEEN_BOX3D_TOOLBAR_H */ diff --git a/src/ui/toolbar/calligraphy-toolbar.cpp b/src/ui/toolbar/calligraphy-toolbar.cpp new file mode 100644 index 0000000..05af968 --- /dev/null +++ b/src/ui/toolbar/calligraphy-toolbar.cpp @@ -0,0 +1,628 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Calligraphy aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "calligraphy-toolbar.h" + +#include <glibmm/i18n.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" + +#include "ui/dialog/calligraphic-profile-rename.h" +#include "ui/icon-names.h" +#include "ui/simple-pref-pusher.h" +#include "ui/widget/canvas.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::Util::Quantity; +using Inkscape::Util::Unit; +using Inkscape::Util::unit_table; + +std::vector<Glib::ustring> get_presets_list() { + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + std::vector<Glib::ustring> presets = prefs->getAllDirs("/tools/calligraphic/preset"); + + return presets; +} + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +CalligraphyToolbar::CalligraphyToolbar(SPDesktop *desktop) + : Toolbar(desktop) + , _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)) + , _presets_blocked(false) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _tracker->prependUnit(unit_table.getUnit("px")); + _tracker->changeLabel("%", 0, true); + if (prefs->getBool("/tools/calligraphic/abs_width")) { + _tracker->setActiveUnitByLabel(prefs->getString("/tools/calligraphic/unit")); + } + + /*calligraphic profile */ + { + _profile_selector_combo = Gtk::manage(new Gtk::ComboBoxText()); + _profile_selector_combo->set_tooltip_text(_("Choose a preset")); + + build_presets_list(); + + auto profile_selector_ti = Gtk::manage(new Gtk::ToolItem()); + profile_selector_ti->add(*_profile_selector_combo); + add(*profile_selector_ti); + + _profile_selector_combo->signal_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::change_profile)); + } + + /*calligraphic profile editor */ + { + auto profile_edit_item = Gtk::manage(new Gtk::ToolButton(_("Add/Edit Profile"))); + profile_edit_item->set_tooltip_text(_("Add or edit calligraphic profile")); + profile_edit_item->set_icon_name(INKSCAPE_ICON("document-properties")); + profile_edit_item->signal_clicked().connect(sigc::mem_fun(*this, &CalligraphyToolbar::edit_profile)); + add(*profile_edit_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Width */ + std::vector<Glib::ustring> labels = {_("(hairline)"), "", "", "", _("(default)"), "", "", "", "", _("(broad stroke)")}; + std::vector<double> values = { 1, 3, 5, 10, 15, 20, 30, 50, 75, 100}; + auto width_val = prefs->getDouble("/tools/calligraphic/width", 15.118); + Unit const *unit = unit_table.getUnit(prefs->getString("/tools/calligraphic/unit")); + _width_adj = Gtk::Adjustment::create(Quantity::convert(width_val, "px", unit), 0.001, 100, 1.0, 10.0); + auto width_item = + Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-width", _("Width:"), _width_adj, 0.001, 3)); + width_item->set_tooltip_text(_("The width of the calligraphic pen (relative to the visible canvas area)")); + width_item->set_custom_numeric_menu_data(values, labels); + width_item->set_focus_widget(desktop->canvas); + _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::width_value_changed)); + _widget_map["width"] = G_OBJECT(_width_adj->gobj()); + add(*width_item); + _tracker->addAdjustment(_width_adj->gobj()); + width_item->set_sensitive(true); + } + + /* Unit Menu */ + { + auto unit_menu_ti = _tracker->create_tool_item(_("Units"), ""); + add(*unit_menu_ti); + unit_menu_ti->signal_changed_after().connect(sigc::mem_fun(*this, &CalligraphyToolbar::unit_changed)); + } + + /* Use Pressure button */ + { + _usepressure = add_toggle_button(_("Pressure"), + _("Use the pressure of the input device to alter the width of the pen")); + _usepressure->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _widget_map["usepressure"] = G_OBJECT(_usepressure->gobj()); + _usepressure_pusher.reset(new SimplePrefPusher(_usepressure, "/tools/calligraphic/usepressure")); + _usepressure->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CalligraphyToolbar::on_pref_toggled), + _usepressure, + "/tools/calligraphic/usepressure")); + } + + /* Trace Background button */ + { + _tracebackground = add_toggle_button(_("Trace Background"), + _("Trace the lightness of the background by the width of the pen (white - minimum width, black - maximum width)")); + _tracebackground->set_icon_name(INKSCAPE_ICON("draw-trace-background")); + _widget_map["tracebackground"] = G_OBJECT(_tracebackground->gobj()); + _tracebackground_pusher.reset(new SimplePrefPusher(_tracebackground, "/tools/calligraphic/tracebackground")); + _tracebackground->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CalligraphyToolbar::on_pref_toggled), + _tracebackground, + "/tools/calligraphic/tracebackground")); + } + + { + /* Thinning */ + std::vector<Glib::ustring> labels = {_("(speed blows up stroke)"), "", "", _("(slight widening)"), _("(constant width)"), _("(slight thinning, default)"), "", "", _("(speed deflates stroke)")}; + std::vector<double> values = { -100, -40, -20, -10, 0, 10, 20, 40, 100}; + auto thinning_val = prefs->getDouble("/tools/calligraphic/thinning", 10); + _thinning_adj = Gtk::Adjustment::create(thinning_val, -100, 100, 1, 10.0); + auto thinning_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-thinning", _("Thinning:"), _thinning_adj, 1, 0)); + thinning_item->set_tooltip_text(("How much velocity thins the stroke (> 0 makes fast strokes thinner, < 0 makes them broader, 0 makes width independent of velocity)")); + thinning_item->set_custom_numeric_menu_data(values, labels); + thinning_item->set_focus_widget(desktop->canvas); + _thinning_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::velthin_value_changed)); + _widget_map["thinning"] = G_OBJECT(_thinning_adj->gobj()); + add(*thinning_item); + thinning_item->set_sensitive(true); + } + + { + /* Mass */ + std::vector<Glib::ustring> labels = {_("(no inertia)"), _("(slight smoothing, default)"), _("(noticeable lagging)"), "", "", _("(maximum inertia)")}; + std::vector<double> values = { 0.0, 2, 10, 20, 50, 100}; + auto mass_val = prefs->getDouble("/tools/calligraphic/mass", 2.0); + _mass_adj = Gtk::Adjustment::create(mass_val, 0.0, 100, 1, 10.0); + auto mass_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-mass", _("Mass:"), _mass_adj, 1, 0)); + mass_item->set_tooltip_text(_("Increase to make the pen drag behind, as if slowed by inertia")); + mass_item->set_custom_numeric_menu_data(values, labels); + mass_item->set_focus_widget(desktop->canvas); + _mass_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::mass_value_changed)); + _widget_map["mass"] = G_OBJECT(_mass_adj->gobj()); + add(*mass_item); + mass_item->set_sensitive(true); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Angle */ + std::vector<Glib::ustring> labels = {_("(left edge up)"), "", "", _("(horizontal)"), _("(default)"), "", _("(right edge up)")}; + std::vector<double> values = { -90, -60, -30, 0, 30, 60, 90}; + auto angle_val = prefs->getDouble("/tools/calligraphic/angle", 30); + _angle_adj = Gtk::Adjustment::create(angle_val, -90.0, 90.0, 1.0, 10.0); + _angle_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-angle", _("Angle:"), _angle_adj, 1, 0)); + _angle_item->set_tooltip_text(_("The angle of the pen's nib (in degrees; 0 = horizontal; has no effect if fixation = 0)")); + _angle_item->set_custom_numeric_menu_data(values, labels); + _angle_item->set_focus_widget(desktop->canvas); + _angle_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::angle_value_changed)); + _widget_map["angle"] = G_OBJECT(_angle_adj->gobj()); + add(*_angle_item); + _angle_item->set_sensitive(true); + } + + /* Use Tilt button */ + { + _usetilt = add_toggle_button(_("Tilt"), + _("Use the tilt of the input device to alter the angle of the pen's nib")); + _usetilt->set_icon_name(INKSCAPE_ICON("draw-use-tilt")); + _widget_map["usetilt"] = G_OBJECT(_usetilt->gobj()); + _usetilt_pusher.reset(new SimplePrefPusher(_usetilt, "/tools/calligraphic/usetilt")); + _usetilt->signal_toggled().connect(sigc::mem_fun(*this, &CalligraphyToolbar::tilt_state_changed)); + _angle_item->set_sensitive(!prefs->getBool("/tools/calligraphic/usetilt", true)); + _usetilt->set_active(prefs->getBool("/tools/calligraphic/usetilt", true)); + } + + { + /* Fixation */ + std::vector<Glib::ustring> labels = {_("(perpendicular to stroke, \"brush\")"), "", "", "", _("(almost fixed, default)"), _("(fixed by Angle, \"pen\")")}; + std::vector<double> values = { 0, 20, 40, 60, 90, 100}; + auto flatness_val = prefs->getDouble("/tools/calligraphic/flatness", -90); + _fixation_adj = Gtk::Adjustment::create(flatness_val, -100.0, 100.0, 1.0, 10.0); + auto flatness_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-fixation", _("Fixation:"), _fixation_adj, 1, 0)); + flatness_item->set_tooltip_text(_("Angle behavior (0 = nib always perpendicular to stroke direction, 100 = fixed angle, -100 = fixed angle in opposite direction)")); + flatness_item->set_custom_numeric_menu_data(values, labels); + flatness_item->set_focus_widget(desktop->canvas); + _fixation_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::flatness_value_changed)); + _widget_map["flatness"] = G_OBJECT(_fixation_adj->gobj()); + add(*flatness_item); + flatness_item->set_sensitive(true); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Cap Rounding */ + std::vector<Glib::ustring> labels = {_("(blunt caps, default)"), _("(slightly bulging)"), "", "", _("(approximately round)"), _("(long protruding caps)")}; + std::vector<double> values = { 0, 0.3, 0.5, 1.0, 1.4, 5.0}; + auto cap_rounding_val = prefs->getDouble("/tools/calligraphic/cap_rounding", 0.0); + _cap_rounding_adj = Gtk::Adjustment::create(cap_rounding_val, 0.0, 5.0, 0.01, 0.1); + auto cap_rounding_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-cap-rounding", _("Caps:"), _cap_rounding_adj, 0.01, 2)); + + // TRANSLATORS: "cap" means "end" (both start and finish) here + cap_rounding_item->set_tooltip_text(_("Increase to make caps at the ends of strokes protrude more (0 = no caps, 1 = round caps)")); + cap_rounding_item->set_custom_numeric_menu_data(values, labels); + cap_rounding_item->set_focus_widget(desktop->canvas); + _cap_rounding_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::cap_rounding_value_changed)); + _widget_map["cap_rounding"] = G_OBJECT(_cap_rounding_adj->gobj()); + add(*cap_rounding_item); + cap_rounding_item->set_sensitive(true); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Tremor */ + std::vector<Glib::ustring> labels = {_("(smooth line)"), _("(slight tremor)"), _("(noticeable tremor)"), "", "", _("(maximum tremor)")}; + std::vector<double> values = { 0, 10, 20, 40, 60, 100}; + auto tremor_val = prefs->getDouble("/tools/calligraphic/tremor", 0.0); + _tremor_adj = Gtk::Adjustment::create(tremor_val, 0.0, 100, 1, 10.0); + auto tremor_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-tremor", _("Tremor:"), _tremor_adj, 1, 0)); + tremor_item->set_tooltip_text(_("Increase to make strokes rugged and trembling")); + tremor_item->set_custom_numeric_menu_data(values, labels); + tremor_item->set_focus_widget(desktop->canvas); + _tremor_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::tremor_value_changed)); + _widget_map["tremor"] = G_OBJECT(_tremor_adj->gobj()); + add(*tremor_item); + tremor_item->set_sensitive(true); + } + + { + /* Wiggle */ + std::vector<Glib::ustring> labels = {_("(no wiggle)"), _("(slight deviation)"), "", "", _("(wild waves and curls)")}; + std::vector<double> values = { 0, 20, 40, 60, 100}; + auto wiggle_val = prefs->getDouble("/tools/calligraphic/wiggle", 0.0); + _wiggle_adj = Gtk::Adjustment::create(wiggle_val, 0.0, 100, 1, 10.0); + auto wiggle_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-wiggle", _("Wiggle:"), _wiggle_adj, 1, 0)); + wiggle_item->set_tooltip_text(_("Increase to make the pen waver and wiggle")); + wiggle_item->set_custom_numeric_menu_data(values, labels); + wiggle_item->set_focus_widget(desktop->canvas); + _wiggle_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::wiggle_value_changed)); + _widget_map["wiggle"] = G_OBJECT(_wiggle_adj->gobj()); + add(*wiggle_item); + wiggle_item->set_sensitive(true); + } + + show_all(); +} + +GtkWidget * +CalligraphyToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new CalligraphyToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +CalligraphyToolbar::width_value_changed() +{ + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/calligraphic/abs_width", _tracker->getCurrentLabel() != "%"); + prefs->setDouble("/tools/calligraphic/width", Quantity::convert(_width_adj->get_value(), unit, "px")); + update_presets_list(); +} + +void +CalligraphyToolbar::velthin_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/calligraphic/thinning", _thinning_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::angle_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/angle", _angle_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::flatness_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/flatness", _fixation_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::cap_rounding_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/cap_rounding", _cap_rounding_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::tremor_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/tremor", _tremor_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::wiggle_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/wiggle", _wiggle_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::mass_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/mass", _mass_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::on_pref_toggled(Gtk::ToggleToolButton *item, + const Glib::ustring& path) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(path, item->get_active()); + update_presets_list(); +} + +void +CalligraphyToolbar::update_presets_list() +{ + if (_presets_blocked) { + return; + } + + auto prefs = Inkscape::Preferences::get(); + auto presets = get_presets_list(); + + int index = 1; // 0 is for no preset. + for (auto i = presets.begin(); i != presets.end(); ++i, ++index) { + bool match = true; + + auto preset = prefs->getAllEntries(*i); + for (auto & j : preset) { + Glib::ustring entry_name = j.getEntryName(); + if (entry_name == "id" || entry_name == "name") { + continue; + } + + void *widget = _widget_map[entry_name.data()]; + if (widget) { + if (GTK_IS_ADJUSTMENT(widget)) { + double v = j.getDouble(); + GtkAdjustment* adj = static_cast<GtkAdjustment *>(widget); + //std::cout << "compared adj " << attr_name << gtk_adjustment_get_value(adj) << " to " << v << "\n"; + if (fabs(gtk_adjustment_get_value(adj) - v) > 1e-6) { + match = false; + break; + } + } else if (GTK_IS_TOGGLE_TOOL_BUTTON(widget)) { + bool v = j.getBool(); + auto toggle = GTK_TOGGLE_TOOL_BUTTON(widget); + //std::cout << "compared toggle " << attr_name << gtk_toggle_action_get_active(toggle) << " to " << v << "\n"; + if ( static_cast<bool>(gtk_toggle_tool_button_get_active(toggle)) != v ) { + match = false; + break; + } + } + } + } + + if (match) { + // newly added item is at the same index as the + // save command, so we need to change twice for it to take effect + _profile_selector_combo->set_active(0); + _profile_selector_combo->set_active(index); + return; + } + } + + // no match found + _profile_selector_combo->set_active(0); +} + +void +CalligraphyToolbar::tilt_state_changed() +{ + _angle_item->set_sensitive(!_usetilt->get_active()); + on_pref_toggled(_usetilt, "/tools/calligraphic/usetilt"); +} + +void +CalligraphyToolbar::build_presets_list() +{ + _presets_blocked = true; + + _profile_selector_combo->remove_all(); + _profile_selector_combo->append(_("No preset")); + + // iterate over all presets to populate the list + auto prefs = Inkscape::Preferences::get(); + auto presets = get_presets_list(); + + for (auto & preset : presets) { + Glib::ustring preset_name = prefs->getString(preset + "/name"); + + if (!preset_name.empty()) { + _profile_selector_combo->append(_(preset_name.data())); + } + } + + _presets_blocked = false; + + update_presets_list(); +} + +void +CalligraphyToolbar::change_profile() +{ + auto mode = _profile_selector_combo->get_active_row_number(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (_presets_blocked) { + return; + } + + // mode is one-based so we subtract 1 + std::vector<Glib::ustring> presets = get_presets_list(); + + Glib::ustring preset_path = ""; + if (mode - 1 < presets.size()) { + preset_path = presets.at(mode - 1); + } + + if (!preset_path.empty()) { + _presets_blocked = true; //temporarily block the selector so no one will updadte it while we're reading it + + std::vector<Inkscape::Preferences::Entry> preset = prefs->getAllEntries(preset_path); + + // Shouldn't this be std::map? + for (auto & i : preset) { + Glib::ustring entry_name = i.getEntryName(); + if (entry_name == "id" || entry_name == "name") { + continue; + } + void *widget = _widget_map[entry_name.data()]; + if (widget) { + if (GTK_IS_ADJUSTMENT(widget)) { + GtkAdjustment* adj = static_cast<GtkAdjustment *>(widget); + gtk_adjustment_set_value(adj, i.getDouble()); + //std::cout << "set adj " << attr_name << " to " << v << "\n"; + } else if (GTK_IS_TOGGLE_TOOL_BUTTON(widget)) { + auto toggle = GTK_TOGGLE_TOOL_BUTTON(widget); + gtk_toggle_tool_button_set_active(toggle, i.getBool()); + //std::cout << "set toggle " << attr_name << " to " << v << "\n"; + } else { + g_warning("Unknown widget type for preset: %s\n", entry_name.data()); + } + } else { + g_warning("Bad key found in a preset record: %s\n", entry_name.data()); + } + } + _presets_blocked = false; + } +} + +void +CalligraphyToolbar::edit_profile() +{ + save_profile(nullptr); +} + +void CalligraphyToolbar::unit_changed(int /* NotUsed */) +{ + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/calligraphic/abs_width", _tracker->getCurrentLabel() != "%"); + prefs->setDouble("/tools/calligraphic/width", + CLAMP(prefs->getDouble("/tools/calligraphic/width"), Quantity::convert(0.001, unit, "px"), + Quantity::convert(100, unit, "px"))); + prefs->setString("/tools/calligraphic/unit", unit->abbr); +} + +void CalligraphyToolbar::save_profile(GtkWidget * /*widget*/) +{ + using Inkscape::UI::Dialog::CalligraphicProfileRename; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (! _desktop) { + return; + } + + if (_presets_blocked) { + return; + } + + Glib::ustring current_profile_name = _profile_selector_combo->get_active_text(); + + if (current_profile_name == _("No preset")) { + current_profile_name = ""; + } + + CalligraphicProfileRename::show(_desktop, current_profile_name); + if ( !CalligraphicProfileRename::applied()) { + // dialog cancelled + update_presets_list(); + return; + } + Glib::ustring new_profile_name = CalligraphicProfileRename::getProfileName(); + + if (new_profile_name.empty()) { + // empty name entered + update_presets_list (); + return; + } + + _presets_blocked = true; + + // If there's a preset with the given name, find it and set save_path appropriately + auto presets = get_presets_list(); + int total_presets = presets.size(); + int new_index = -1; + Glib::ustring save_path; // profile pref path without a trailing slash + + int temp_index = 0; + for (std::vector<Glib::ustring>::iterator i = presets.begin(); i != presets.end(); ++i, ++temp_index) { + Glib::ustring name = prefs->getString(*i + "/name"); + if (!name.empty() && (new_profile_name == name || current_profile_name == name)) { + new_index = temp_index; + save_path = *i; + break; + } + } + + if ( CalligraphicProfileRename::deleted() && new_index != -1) { + prefs->remove(save_path); + _presets_blocked = false; + build_presets_list(); + return; + } + + if (new_index == -1) { + // no preset with this name, create + new_index = total_presets + 1; + gchar *profile_id = g_strdup_printf("/dcc%d", new_index); + save_path = Glib::ustring("/tools/calligraphic/preset") + profile_id; + g_free(profile_id); + } + + for (auto map_item : _widget_map) { + auto widget_name = map_item.first; + auto widget = map_item.second; + + if (widget) { + if (GTK_IS_ADJUSTMENT(widget)) { + GtkAdjustment* adj = GTK_ADJUSTMENT(widget); + prefs->setDouble(save_path + "/" + widget_name, gtk_adjustment_get_value(adj)); + //std::cout << "wrote adj " << widget_name << ": " << v << "\n"; + } else if (GTK_IS_TOGGLE_TOOL_BUTTON(widget)) { + auto toggle = GTK_TOGGLE_TOOL_BUTTON(widget); + prefs->setBool(save_path + "/" + widget_name, gtk_toggle_tool_button_get_active(toggle)); + //std::cout << "wrote tog " << widget_name << ": " << v << "\n"; + } else { + g_warning("Unknown widget type for preset: %s\n", widget_name.c_str()); + } + } else { + g_warning("Bad key when writing preset: %s\n", widget_name.c_str()); + } + } + prefs->setString(save_path + "/name", new_profile_name); + + _presets_blocked = true; + build_presets_list(); +} + +} +} +} + + +/* + 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/toolbar/calligraphy-toolbar.h b/src/ui/toolbar/calligraphy-toolbar.h new file mode 100644 index 0000000..88f22ad --- /dev/null +++ b/src/ui/toolbar/calligraphy-toolbar.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CALLIGRAPHY_TOOLBAR_H +#define SEEN_CALLIGRAPHY_TOOLBAR_H + +/** + * @file + * Calligraphy aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class ComboBoxText; +} + +namespace Inkscape { +namespace UI { +class SimplePrefPusher; + +namespace Widget { +class SpinButtonToolItem; +class UnitTracker; +} + +namespace Toolbar { +class CalligraphyToolbar : public Toolbar { +private: + UI::Widget::UnitTracker *_tracker; + bool _presets_blocked; + + UI::Widget::SpinButtonToolItem *_angle_item; + Gtk::ComboBoxText *_profile_selector_combo; + + std::map<Glib::ustring, GObject *> _widget_map; + + Glib::RefPtr<Gtk::Adjustment> _width_adj; + Glib::RefPtr<Gtk::Adjustment> _mass_adj; + Glib::RefPtr<Gtk::Adjustment> _wiggle_adj; + Glib::RefPtr<Gtk::Adjustment> _angle_adj; + Glib::RefPtr<Gtk::Adjustment> _thinning_adj; + Glib::RefPtr<Gtk::Adjustment> _tremor_adj; + Glib::RefPtr<Gtk::Adjustment> _fixation_adj; + Glib::RefPtr<Gtk::Adjustment> _cap_rounding_adj; + Gtk::ToggleToolButton *_usepressure; + Gtk::ToggleToolButton *_tracebackground; + Gtk::ToggleToolButton *_usetilt; + + std::unique_ptr<SimplePrefPusher> _tracebackground_pusher; + std::unique_ptr<SimplePrefPusher> _usepressure_pusher; + std::unique_ptr<SimplePrefPusher> _usetilt_pusher; + + void width_value_changed(); + void velthin_value_changed(); + void angle_value_changed(); + void flatness_value_changed(); + void cap_rounding_value_changed(); + void tremor_value_changed(); + void wiggle_value_changed(); + void mass_value_changed(); + void build_presets_list(); + void change_profile(); + void save_profile(GtkWidget *widget); + void edit_profile(); + void update_presets_list(); + void tilt_state_changed(); + void unit_changed(int not_used); + void on_pref_toggled(Gtk::ToggleToolButton *item, + const Glib::ustring& path); + +protected: + CalligraphyToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_CALLIGRAPHY_TOOLBAR_H */ diff --git a/src/ui/toolbar/connector-toolbar.cpp b/src/ui/toolbar/connector-toolbar.cpp new file mode 100644 index 0000000..305698b --- /dev/null +++ b/src/ui/toolbar/connector-toolbar.cpp @@ -0,0 +1,431 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Connector aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "connector-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/separatortoolitem.h> + +#include "conn-avoid-ref.h" + +#include "desktop.h" +#include "document-undo.h" +#include "enums.h" +#include "layer-manager.h" +#include "selection.h" + +#include "object/algorithms/graphlayout.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" + +#include "ui/icon-names.h" +#include "ui/tools/connector-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +static Inkscape::XML::NodeEventVector connector_tb_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::ConnectorToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +ConnectorToolbar::ConnectorToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _freeze(false), + _repr(nullptr) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + { + auto avoid_item = Gtk::manage(new Gtk::ToolButton(_("Avoid"))); + avoid_item->set_tooltip_text(_("Make connectors avoid selected objects")); + avoid_item->set_icon_name(INKSCAPE_ICON("connector-avoid")); + avoid_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::path_set_avoid)); + add(*avoid_item); + } + + { + auto ignore_item = Gtk::manage(new Gtk::ToolButton(_("Ignore"))); + ignore_item->set_tooltip_text(_("Make connectors ignore selected objects")); + ignore_item->set_icon_name(INKSCAPE_ICON("connector-ignore")); + ignore_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::path_set_ignore)); + add(*ignore_item); + } + + // Orthogonal connectors toggle button + { + _orthogonal = add_toggle_button(_("Orthogonal"), + _("Make connector orthogonal or polyline")); + _orthogonal->set_icon_name(INKSCAPE_ICON("connector-orthogonal")); + + bool tbuttonstate = prefs->getBool("/tools/connector/orthogonal"); + _orthogonal->set_active(( tbuttonstate ? TRUE : FALSE )); + _orthogonal->signal_toggled().connect(sigc::mem_fun(*this, &ConnectorToolbar::orthogonal_toggled)); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + // Curvature spinbox + auto curvature_val = prefs->getDouble("/tools/connector/curvature", defaultConnCurvature); + _curvature_adj = Gtk::Adjustment::create(curvature_val, 0, 100, 1.0, 10.0); + auto curvature_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:connector-curvature", _("Curvature:"), _curvature_adj, 1, 0)); + curvature_item->set_tooltip_text(_("The amount of connectors curvature")); + curvature_item->set_focus_widget(desktop->canvas); + _curvature_adj->signal_value_changed().connect(sigc::mem_fun(*this, &ConnectorToolbar::curvature_changed)); + add(*curvature_item); + + // Spacing spinbox + auto spacing_val = prefs->getDouble("/tools/connector/spacing", defaultConnSpacing); + _spacing_adj = Gtk::Adjustment::create(spacing_val, 0, 100, 1.0, 10.0); + auto spacing_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:connector-spacing", _("Spacing:"), _spacing_adj, 1, 0)); + spacing_item->set_tooltip_text(_("The amount of space left around objects by auto-routing connectors")); + spacing_item->set_focus_widget(desktop->canvas); + _spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &ConnectorToolbar::spacing_changed)); + add(*spacing_item); + + // Graph (connector network) layout + { + auto graph_item = Gtk::manage(new Gtk::ToolButton(_("Graph"))); + graph_item->set_tooltip_text(_("Nicely arrange selected connector network")); + graph_item->set_icon_name(INKSCAPE_ICON("distribute-graph")); + graph_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::graph_layout)); + add(*graph_item); + } + + // Default connector length spinbox + auto length_val = prefs->getDouble("/tools/connector/length", 100); + _length_adj = Gtk::Adjustment::create(length_val, 10, 1000, 10.0, 100.0); + auto length_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:connector-length", _("Length:"), _length_adj, 1, 0)); + length_item->set_tooltip_text(_("Ideal length for connectors when layout is applied")); + length_item->set_focus_widget(desktop->canvas); + _length_adj->signal_value_changed().connect(sigc::mem_fun(*this, &ConnectorToolbar::length_changed)); + add(*length_item); + + // Directed edges toggle button + { + _directed_item = add_toggle_button(_("Downwards"), + _("Make connectors with end-markers (arrows) point downwards")); + _directed_item->set_icon_name(INKSCAPE_ICON("distribute-graph-directed")); + + bool tbuttonstate = prefs->getBool("/tools/connector/directedlayout"); + _directed_item->set_active(tbuttonstate ? TRUE : FALSE); + + _directed_item->signal_toggled().connect(sigc::mem_fun(*this, &ConnectorToolbar::directed_graph_layout_toggled)); + desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &ConnectorToolbar::selection_changed)); + } + + // Avoid overlaps toggle button + { + _overlap_item = add_toggle_button(_("Remove overlaps"), + _("Do not allow overlapping shapes")); + _overlap_item->set_icon_name(INKSCAPE_ICON("distribute-remove-overlaps")); + + bool tbuttonstate = prefs->getBool("/tools/connector/avoidoverlaplayout"); + _overlap_item->set_active(tbuttonstate ? TRUE : FALSE); + + _overlap_item->signal_toggled().connect(sigc::mem_fun(*this, &ConnectorToolbar::nooverlaps_graph_layout_toggled)); + } + + // Code to watch for changes to the connector-spacing attribute in + // the XML. + Inkscape::XML::Node *repr = desktop->namedview->getRepr(); + g_assert(repr != nullptr); + + if(_repr) { + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + + if (repr) { + _repr = repr; + Inkscape::GC::anchor(_repr); + _repr->addListener(&connector_tb_repr_events, this); + _repr->synthesizeEvents(&connector_tb_repr_events, this); + } + + show_all(); +} + +GtkWidget * +ConnectorToolbar::create( SPDesktop *desktop) +{ + auto toolbar = new ConnectorToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} // end of ConnectorToolbar::prep() + +void +ConnectorToolbar::path_set_avoid() +{ + Inkscape::UI::Tools::cc_selection_set_avoid(_desktop, true); +} + +void +ConnectorToolbar::path_set_ignore() +{ + Inkscape::UI::Tools::cc_selection_set_avoid(_desktop, false); +} + +void +ConnectorToolbar::orthogonal_toggled() +{ + auto doc = _desktop->getDocument(); + + if (!DocumentUndo::getUndoSensitive(doc)) { + return; + } + + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + + // in turn, prevent callbacks from responding + _freeze = true; + + bool is_orthog = _orthogonal->get_active(); + gchar orthog_str[] = "orthogonal"; + gchar polyline_str[] = "polyline"; + gchar *value = is_orthog ? orthog_str : polyline_str ; + + bool modmade = false; + auto itemlist= _desktop->getSelection()->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + + if (Inkscape::UI::Tools::cc_item_is_connector(item)) { + item->setAttribute( "inkscape:connector-type", value); + item->getAvoidRef().handleSettingChange(); + modmade = true; + } + } + + if (!modmade) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/connector/orthogonal", is_orthog); + } else { + + DocumentUndo::done(doc, is_orthog ? _("Set connector type: orthogonal"): _("Set connector type: polyline"), INKSCAPE_ICON("draw-connector")); + } + + _freeze = false; +} + +void +ConnectorToolbar::curvature_changed() +{ + SPDocument *doc = _desktop->getDocument(); + + if (!DocumentUndo::getUndoSensitive(doc)) { + return; + } + + + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + + // in turn, prevent callbacks from responding + _freeze = true; + + auto newValue = _curvature_adj->get_value(); + gchar value[G_ASCII_DTOSTR_BUF_SIZE]; + g_ascii_dtostr(value, G_ASCII_DTOSTR_BUF_SIZE, newValue); + + bool modmade = false; + auto itemlist= _desktop->getSelection()->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + + if (Inkscape::UI::Tools::cc_item_is_connector(item)) { + item->setAttribute( "inkscape:connector-curvature", value); + item->getAvoidRef().handleSettingChange(); + modmade = true; + } + } + + if (!modmade) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/connector/curvature"), newValue); + } + else { + DocumentUndo::done(doc, _("Change connector curvature"), INKSCAPE_ICON("draw-connector")); + } + + _freeze = false; +} + +void +ConnectorToolbar::spacing_changed() +{ + SPDocument *doc = _desktop->getDocument(); + + if (!DocumentUndo::getUndoSensitive(doc)) { + return; + } + + Inkscape::XML::Node *repr = _desktop->namedview->getRepr(); + + if ( !repr->attribute("inkscape:connector-spacing") && + ( _spacing_adj->get_value() == defaultConnSpacing )) { + // Don't need to update the repr if the attribute doesn't + // exist and it is being set to the default value -- as will + // happen at startup. + return; + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + repr->setAttributeCssDouble("inkscape:connector-spacing", _spacing_adj->get_value()); + _desktop->namedview->updateRepr(); + bool modmade = false; + + std::vector<SPItem *> items; + items = get_avoided_items(items, _desktop->layerManager().currentRoot(), _desktop); + for (auto item : items) { + Geom::Affine m = Geom::identity(); + avoid_item_move(&m, item); + modmade = true; + } + + if(modmade) { + DocumentUndo::done(doc, _("Change connector spacing"), INKSCAPE_ICON("draw-connector")); + } + _freeze = false; +} + +void +ConnectorToolbar::graph_layout() +{ + assert(_desktop); + if (!_desktop) { + return; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // hack for clones, see comment in align-and-distribute.cpp + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + auto tmp = _desktop->getSelection()->items(); + std::vector<SPItem *> vec(tmp.begin(), tmp.end()); + graphlayout(vec); + + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_desktop->getDocument(), _("Arrange connector network"), INKSCAPE_ICON("dialog-align-and-distribute")); +} + +void +ConnectorToolbar::length_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/connector/length", _length_adj->get_value()); +} + +void +ConnectorToolbar::directed_graph_layout_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/connector/directedlayout", _directed_item->get_active()); +} + +void +ConnectorToolbar::selection_changed(Inkscape::Selection *selection) +{ + SPItem *item = selection->singleItem(); + if (SP_IS_PATH(item)) + { + gdouble curvature = SP_PATH(item)->connEndPair.getCurvature(); + bool is_orthog = SP_PATH(item)->connEndPair.isOrthogonal(); + _orthogonal->set_active(is_orthog); + _curvature_adj->set_value(curvature); + } + +} + +void +ConnectorToolbar::nooverlaps_graph_layout_toggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/connector/avoidoverlaplayout", + _overlap_item->get_active()); +} + +void +ConnectorToolbar::event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const * /*old_value*/, + gchar const * /*new_value*/, + bool /*is_interactive*/, + gpointer data) +{ + auto toolbar = reinterpret_cast<ConnectorToolbar *>(data); + + if ( !toolbar->_freeze + && (strcmp(name, "inkscape:connector-spacing") == 0) ) { + gdouble spacing = repr->getAttributeDouble("inkscape:connector-spacing", defaultConnSpacing); + + toolbar->_spacing_adj->set_value(spacing); + + if (toolbar->_desktop->canvas) { + toolbar->_desktop->canvas->grab_focus(); + } + } +} + +} +} +} + +/* + 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/toolbar/connector-toolbar.h b/src/ui/toolbar/connector-toolbar.h new file mode 100644 index 0000000..66df79e --- /dev/null +++ b/src/ui/toolbar/connector-toolbar.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CONNECTOR_TOOLBAR_H +#define SEEN_CONNECTOR_TOOLBAR_H + +/** + * @file + * Connector aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class ToolButton; +} + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Toolbar { +class ConnectorToolbar : public Toolbar { +private: + Gtk::ToggleToolButton *_orthogonal; + Gtk::ToggleToolButton *_directed_item; + Gtk::ToggleToolButton *_overlap_item; + + Glib::RefPtr<Gtk::Adjustment> _curvature_adj; + Glib::RefPtr<Gtk::Adjustment> _spacing_adj; + Glib::RefPtr<Gtk::Adjustment> _length_adj; + + bool _freeze; + + Inkscape::XML::Node *_repr; + + void path_set_avoid(); + void path_set_ignore(); + void orthogonal_toggled(); + void graph_layout(); + void directed_graph_layout_toggled(); + void nooverlaps_graph_layout_toggled(); + void curvature_changed(); + void spacing_changed(); + void length_changed(); + void selection_changed(Inkscape::Selection *selection); + +protected: + ConnectorToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); + + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const * /*old_value*/, + gchar const * /*new_value*/, + bool /*is_interactive*/, + gpointer data); +}; + +} +} +} + +#endif /* !SEEN_CONNECTOR_TOOLBAR_H */ diff --git a/src/ui/toolbar/dropper-toolbar.cpp b/src/ui/toolbar/dropper-toolbar.cpp new file mode 100644 index 0000000..83a18c3 --- /dev/null +++ b/src/ui/toolbar/dropper-toolbar.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dropper aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include "dropper-toolbar.h" +#include "document-undo.h" +#include "preferences.h" +#include "desktop.h" + +#include "ui/widget/canvas.h" // Grab focus + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +void DropperToolbar::on_pick_alpha_button_toggled() +{ + auto active = _pick_alpha_button->get_active(); + + auto prefs = Inkscape::Preferences::get(); + prefs->setInt( "/tools/dropper/pick", active ); + + _set_alpha_button->set_sensitive(active); + _desktop->canvas->grab_focus(); +} + +void DropperToolbar::on_set_alpha_button_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool( "/tools/dropper/setalpha", _set_alpha_button->get_active( ) ); + _desktop->canvas->grab_focus(); +} + +/* + * TODO: Would like to add swatch of current color. + * TODO: Add queue of last 5 or so colors selected with new swatches so that + * can drag and drop places. Will provide a nice mixing palette. + */ +DropperToolbar::DropperToolbar(SPDesktop *desktop) + : Toolbar(desktop) +{ + // Add widgets to toolbar + add_label(_("Opacity:")); + _pick_alpha_button = add_toggle_button(_("Pick"), + _("Pick both the color and the alpha (transparency) under cursor; " + "otherwise, pick only the visible color premultiplied by alpha")); + _set_alpha_button = add_toggle_button(_("Assign"), + _("If alpha was picked, assign it to selection " + "as fill or stroke transparency")); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Set initial state of widgets + auto pickAlpha = prefs->getInt( "/tools/dropper/pick", 1 ); + auto setAlpha = prefs->getBool( "/tools/dropper/setalpha", true); + + _pick_alpha_button->set_active(pickAlpha); + _set_alpha_button->set_active(setAlpha); + + // Make sure the set-alpha button is disabled if we're not picking alpha + _set_alpha_button->set_sensitive(pickAlpha); + + // Connect signal handlers + auto pick_alpha_button_toggled_cb = sigc::mem_fun(*this, &DropperToolbar::on_pick_alpha_button_toggled); + auto set_alpha_button_toggled_cb = sigc::mem_fun(*this, &DropperToolbar::on_set_alpha_button_toggled); + + _pick_alpha_button->signal_toggled().connect(pick_alpha_button_toggled_cb); + _set_alpha_button->signal_toggled().connect(set_alpha_button_toggled_cb); + + show_all(); +} + +GtkWidget * +DropperToolbar::create(SPDesktop *desktop) +{ + auto toolbar = Gtk::manage(new DropperToolbar(desktop)); + return GTK_WIDGET(toolbar->gobj()); +} +} +} +} + +/* + 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/toolbar/dropper-toolbar.h b/src/ui/toolbar/dropper-toolbar.h new file mode 100644 index 0000000..c8aa42f --- /dev/null +++ b/src/ui/toolbar/dropper-toolbar.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_DROPPER_TOOLBAR_H +#define SEEN_DROPPER_TOOLBAR_H + +/** + * @file + * Dropper aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +/** + * \brief A toolbar for controlling the dropper tool + */ +class DropperToolbar : public Toolbar { +private: + // Tool widgets + Gtk::ToggleToolButton *_pick_alpha_button; ///< Control whether to pick opacity + Gtk::ToggleToolButton *_set_alpha_button; ///< Control whether to set opacity + + // Event handlers + void on_pick_alpha_button_toggled(); + void on_set_alpha_button_toggled(); + +protected: + DropperToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; +} +} +} +#endif /* !SEEN_DROPPER_TOOLBAR_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/toolbar/eraser-toolbar.cpp b/src/ui/toolbar/eraser-toolbar.cpp new file mode 100644 index 0000000..33487f4 --- /dev/null +++ b/src/ui/toolbar/eraser-toolbar.cpp @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Erasor aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "eraser-toolbar.h" + +#include <array> + +#include <glibmm/i18n.h> + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "ui/icon-names.h" +#include "ui/simple-pref-pusher.h" +#include "ui/tools/eraser-tool.h" + +#include "ui/widget/canvas.h" // Focus widget +#include "ui/widget/spin-button-tool-item.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +EraserToolbar::EraserToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _freeze(false) +{ + auto prefs = Inkscape::Preferences::get(); + gint const eraser_mode = prefs->getInt("/tools/eraser/mode", _modeAsInt(Tools::DEFAULT_ERASER_MODE)); + // Mode + { + add_label(_("Mode:")); + + Gtk::RadioToolButton::Group mode_group; + + std::vector<Gtk::RadioToolButton *> mode_buttons; + + auto delete_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Delete"))); + delete_btn->set_tooltip_text(_("Delete objects touched by eraser")); + delete_btn->set_icon_name(INKSCAPE_ICON("draw-eraser-delete-objects")); + mode_buttons.push_back(delete_btn); + + auto cut_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Cut"))); + cut_btn->set_tooltip_text(_("Cut out from paths and shapes")); + cut_btn->set_icon_name(INKSCAPE_ICON("path-difference")); + mode_buttons.push_back(cut_btn); + + auto clip_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Clip"))); + clip_btn->set_tooltip_text(_("Clip from objects")); + clip_btn->set_icon_name(INKSCAPE_ICON("path-intersection")); + mode_buttons.push_back(clip_btn); + + mode_buttons[eraser_mode]->set_active(); + + int btn_index = 0; + + for (auto btn : mode_buttons) + { + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &EraserToolbar::mode_changed), btn_index++)); + } + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Width */ + { + std::vector<Glib::ustring> labels = {_("(no width)"), _("(hairline)"), "", "", "", _("(default)"), "", "", "", "", _("(broad stroke)")}; + std::vector<double> values = { 0, 1, 3, 5, 10, 15, 20, 30, 50, 75, 100}; + auto width_val = prefs->getDouble("/tools/eraser/width", 15); + _width_adj = Gtk::Adjustment::create(width_val, 0, 100, 1.0, 10.0); + _width = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-width", _("Width:"), _width_adj, 1, 0)); + _width->set_tooltip_text(_("The width of the eraser pen (relative to the visible canvas area)")); + _width->set_focus_widget(desktop->canvas); + _width->set_custom_numeric_menu_data(values, labels); + _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::width_value_changed)); + // TODO: Allow SpinButtonToolItem to display as a slider + add(*_width); + _width->set_sensitive(true); + } + + /* Use Pressure button */ + { + _usepressure = add_toggle_button(_("Eraser Pressure"), + _("Use the pressure of the input device to alter the width of the pen")); + _usepressure->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _pressure_pusher.reset(new UI::SimplePrefPusher(_usepressure, "/tools/eraser/usepressure")); + _usepressure->signal_toggled().connect(sigc::mem_fun(*this, &EraserToolbar::usepressure_toggled)); + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Thinning */ + { + std::vector<Glib::ustring> labels = {_("(speed blows up stroke)"), "", "", _("(slight widening)"), _("(constant width)"), _("(slight thinning, default)"), "", "", _("(speed deflates stroke)")}; + std::vector<double> values = { -100, -40, -20, -10, 0, 10, 20, 40, 100}; + auto thinning_val = prefs->getDouble("/tools/eraser/thinning", 10); + _thinning_adj = Gtk::Adjustment::create(thinning_val, -100, 100, 1, 10.0); + _thinning = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-thinning", _("Thinning:"), _thinning_adj, 1, 0)); + _thinning->set_tooltip_text(_("How much velocity thins the stroke (> 0 makes fast strokes thinner, < 0 makes them broader, 0 makes width independent of velocity)")); + _thinning->set_custom_numeric_menu_data(values, labels); + _thinning->set_focus_widget(desktop->canvas); + _thinning_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::velthin_value_changed)); + add(*_thinning); + _thinning->set_sensitive(true); + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Cap Rounding */ + { + std::vector<Glib::ustring> labels = {_("(blunt caps, default)"), _("(slightly bulging)"), "", "", _("(approximately round)"), _("(long protruding caps)")}; + std::vector<double> values = { 0, 0.3, 0.5, 1.0, 1.4, 5.0}; + auto cap_rounding_val = prefs->getDouble("/tools/eraser/cap_rounding", 0.0); + _cap_rounding_adj = Gtk::Adjustment::create(cap_rounding_val, 0.0, 5.0, 0.01, 0.1); + // TRANSLATORS: "cap" means "end" (both start and finish) here + _cap_rounding = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-cap-rounding", _("Caps:"), _cap_rounding_adj, 0.01, 2)); + _cap_rounding->set_tooltip_text(_("Increase to make caps at the ends of strokes protrude more (0 = no caps, 1 = round caps)")); + _cap_rounding->set_custom_numeric_menu_data(values, labels); + _cap_rounding->set_focus_widget(desktop->canvas); + _cap_rounding_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::cap_rounding_value_changed)); + add(*_cap_rounding); + _cap_rounding->set_sensitive(true); + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Tremor */ + { + std::vector<Glib::ustring> labels = {_("(smooth line)"), _("(slight tremor)"), _("(noticeable tremor)"), "", "", _("(maximum tremor)")}; + std::vector<double> values = { 0, 10, 20, 40, 60, 100}; + auto tremor_val = prefs->getDouble("/tools/eraser/tremor", 0.0); + _tremor_adj = Gtk::Adjustment::create(tremor_val, 0.0, 100, 1, 10.0); + _tremor = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-tremor", _("Tremor:"), _tremor_adj, 1, 0)); + _tremor->set_tooltip_text(_("Increase to make strokes rugged and trembling")); + _tremor->set_custom_numeric_menu_data(values, labels); + _tremor->set_focus_widget(desktop->canvas); + _tremor_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::tremor_value_changed)); + + // TODO: Allow slider appearance + add(*_tremor); + _tremor->set_sensitive(true); + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Mass */ + { + std::vector<Glib::ustring> labels = {_("(no inertia)"), _("(slight smoothing, default)"), _("(noticeable lagging)"), "", "", _("(maximum inertia)")}; + std::vector<double> values = { 0.0, 2, 10, 20, 50, 100}; + auto mass_val = prefs->getDouble("/tools/eraser/mass", 10.0); + _mass_adj = Gtk::Adjustment::create(mass_val, 0.0, 100, 1, 10.0); + _mass = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-mass", _("Mass:"), _mass_adj, 1, 0)); + _mass->set_tooltip_text(_("Increase to make the eraser drag behind, as if slowed by inertia")); + _mass->set_custom_numeric_menu_data(values, labels); + _mass->set_focus_widget(desktop->canvas); + _mass_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::mass_value_changed)); + // TODO: Allow slider appearance + add(*_mass); + _mass->set_sensitive(true); + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Overlap */ + { + _split = add_toggle_button(_("Break apart cut items"), + _("Break apart cut items")); + _split->set_icon_name(INKSCAPE_ICON("distribute-randomize")); + _split->set_active( prefs->getBool("/tools/eraser/break_apart", false) ); + _split->signal_toggled().connect(sigc::mem_fun(*this, &EraserToolbar::toggle_break_apart)); + } + + show_all(); + + set_eraser_mode_visibility(eraser_mode); +} + +GtkWidget * +EraserToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new EraserToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +/** + * @brief Computes the integer value representing eraser mode + * @param mode A mode of the eraser tool, from the enum EraserToolMode + * @return the integer to be stored in the prefs as the selected mode + */ +guint EraserToolbar::_modeAsInt(Inkscape::UI::Tools::EraserToolMode mode) +{ + using namespace Inkscape::UI::Tools; + + if (mode == EraserToolMode::DELETE) { + return 0; + } else if (mode == EraserToolMode::CUT) { + return 1; + } else if (mode == EraserToolMode::CLIP) { + return 2; + } else { + return _modeAsInt(DEFAULT_ERASER_MODE); + } +} + +void +EraserToolbar::mode_changed(int mode) +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt( "/tools/eraser/mode", mode ); + } + + set_eraser_mode_visibility(mode); + + // only take action if run by the attr_changed listener + if (!_freeze) { + // in turn, prevent listener from responding + _freeze = true; + + /* + if ( eraser_mode != ERASER_MODE_DELETE ) { + } else { + } + */ + // TODO finish implementation + + _freeze = false; + } +} + +void +EraserToolbar::set_eraser_mode_visibility(const guint eraser_mode) +{ + using namespace Inkscape::UI::Tools; + + _split->set_visible(eraser_mode == _modeAsInt(EraserToolMode::CUT)); + + const gboolean visibility = (eraser_mode != _modeAsInt(EraserToolMode::DELETE)); + + const std::array<Gtk::Widget *, 6> arr = {_cap_rounding, + _mass, + _thinning, + _tremor, + _usepressure, + _width}; + for (auto widget : arr) { + widget->set_visible(visibility); + } + + for (auto separator : _separators) { + separator->set_visible(visibility); + } +} + +void +EraserToolbar::width_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/eraser/width", _width_adj->get_value() ); +} + +void +EraserToolbar::mass_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/eraser/mass", _mass_adj->get_value() ); +} + +void +EraserToolbar::velthin_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/eraser/thinning", _thinning_adj->get_value() ); +} + +void +EraserToolbar::cap_rounding_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/eraser/cap_rounding", _cap_rounding_adj->get_value() ); +} + +void +EraserToolbar::tremor_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/eraser/tremor", _tremor_adj->get_value() ); +} + +void +EraserToolbar::toggle_break_apart() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _split->get_active(); + prefs->setBool("/tools/eraser/break_apart", active); +} + +void +EraserToolbar::usepressure_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/eraser/usepressure", _usepressure->get_active()); +} + +} +} +} + +/* + 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/toolbar/eraser-toolbar.h b/src/ui/toolbar/eraser-toolbar.h new file mode 100644 index 0000000..c993e46 --- /dev/null +++ b/src/ui/toolbar/eraser-toolbar.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_ERASOR_TOOLBAR_H +#define SEEN_ERASOR_TOOLBAR_H + +/** + * @file + * Erasor aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class SeparatorToolItem; +} + +namespace Inkscape { +namespace UI { +class SimplePrefPusher; + +namespace Tools { +enum class EraserToolMode; +extern EraserToolMode const DEFAULT_ERASER_MODE; +} // namespace Tools + +namespace Widget { +class SpinButtonToolItem; +} // namespace Widget + +namespace Toolbar { +class EraserToolbar : public Toolbar { +private: + UI::Widget::SpinButtonToolItem *_width; + UI::Widget::SpinButtonToolItem *_mass; + UI::Widget::SpinButtonToolItem *_thinning; + UI::Widget::SpinButtonToolItem *_cap_rounding; + UI::Widget::SpinButtonToolItem *_tremor; + + Gtk::ToggleToolButton *_usepressure; + Gtk::ToggleToolButton *_split; + + Glib::RefPtr<Gtk::Adjustment> _width_adj; + Glib::RefPtr<Gtk::Adjustment> _mass_adj; + Glib::RefPtr<Gtk::Adjustment> _thinning_adj; + Glib::RefPtr<Gtk::Adjustment> _cap_rounding_adj; + Glib::RefPtr<Gtk::Adjustment> _tremor_adj; + + std::unique_ptr<SimplePrefPusher> _pressure_pusher; + + std::vector<Gtk::SeparatorToolItem *> _separators; + + bool _freeze; + + static guint _modeAsInt(Inkscape::UI::Tools::EraserToolMode mode); + void mode_changed(int mode); + void set_eraser_mode_visibility(const guint eraser_mode); + void width_value_changed(); + void mass_value_changed(); + void velthin_value_changed(); + void cap_rounding_value_changed(); + void tremor_value_changed(); + static void update_presets_list(gpointer data); + void toggle_break_apart(); + void usepressure_toggled(); + +protected: + EraserToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_ERASOR_TOOLBAR_H */ diff --git a/src/ui/toolbar/gradient-toolbar.cpp b/src/ui/toolbar/gradient-toolbar.cpp new file mode 100644 index 0000000..d2faeb1 --- /dev/null +++ b/src/ui/toolbar/gradient-toolbar.cpp @@ -0,0 +1,1173 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient aux toolbar + * + * Authors: + * bulia byak <bulia@dr.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Abhishek Sharma + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include <gtkmm/comboboxtext.h> +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "gradient-drag.h" +#include "gradient-toolbar.h" +#include "selection.h" + +#include "object/sp-defs.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-stop.h" +#include "style.h" + +#include "ui/icon-names.h" +#include "ui/tools/gradient-tool.h" +#include "ui/util.h" +#include "ui/widget/canvas.h" +#include "ui/widget/color-preview.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/gradient-image.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/gradient-vector-selector.h" + +using Inkscape::DocumentUndo; +using Inkscape::UI::Tools::ToolBase; + +static bool blocked = false; + +void gr_apply_gradient_to_item( SPItem *item, SPGradient *gr, SPGradientType initialType, Inkscape::PaintTarget initialMode, Inkscape::PaintTarget mode ) +{ + SPStyle *style = item->style; + bool isFill = (mode == Inkscape::FOR_FILL); + if (style + && (isFill ? style->fill.isPaintserver() : style->stroke.isPaintserver()) + //&& SP_IS_GRADIENT(isFill ? style->getFillPaintServer() : style->getStrokePaintServer()) ) { + && (isFill ? SP_IS_GRADIENT(style->getFillPaintServer()) : SP_IS_GRADIENT(style->getStrokePaintServer())) ) { + SPPaintServer *server = isFill ? style->getFillPaintServer() : style->getStrokePaintServer(); + if ( SP_IS_LINEARGRADIENT(server) ) { + sp_item_set_gradient(item, gr, SP_GRADIENT_TYPE_LINEAR, mode); + } else if ( SP_IS_RADIALGRADIENT(server) ) { + sp_item_set_gradient(item, gr, SP_GRADIENT_TYPE_RADIAL, mode); + } + } + else if (initialMode == mode) + { + sp_item_set_gradient(item, gr, initialType, mode); + } +} + +/** +Applies gradient vector gr to the gradients attached to the selected dragger of drag, or if none, +to all objects in selection. If there was no previous gradient on an item, uses gradient type and +fill/stroke setting from preferences to create new default (linear: left/right; radial: centered) +gradient. +*/ +void gr_apply_gradient(Inkscape::Selection *selection, GrDrag *drag, SPGradient *gr) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + SPGradientType initialType = static_cast<SPGradientType>(prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR)); + Inkscape::PaintTarget initialMode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + + // GRADIENTFIXME: make this work for multiple selected draggers. + + // First try selected dragger + if (drag && !drag->selected.empty()) { + GrDragger *dragger = *(drag->selected.begin()); + for(auto draggable : dragger->draggables) { //for all draggables of dragger + gr_apply_gradient_to_item(draggable->item, gr, initialType, initialMode, draggable->fill_or_stroke); + } + return; + } + + // If no drag or no dragger selected, act on selection + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + gr_apply_gradient_to_item(*i, gr, initialType, initialMode, initialMode); + } +} + +int gr_vector_list(Glib::RefPtr<Gtk::ListStore> store, SPDesktop *desktop, + bool selection_empty, SPGradient *gr_selected, bool gr_multi) +{ + int selected = -1; + + if (!blocked) { + std::cerr << "gr_vector_list: should be blocked!" << std::endl; + } + + // Get list of gradients in document. + SPDocument *document = desktop->getDocument(); + std::vector<SPObject *> gl; + std::vector<SPObject *> gradients = document->getResourceList( "gradient" ); + for (auto gradient : gradients) { + SPGradient *grad = SP_GRADIENT(gradient); + if ( grad->hasStops() && !grad->isSolid() ) { + gl.push_back(gradient); + } + } + + store->clear(); + + Inkscape::UI::Widget::ComboToolItemColumns columns; + Gtk::TreeModel::Row row; + + if (gl.empty()) { + // The document has no gradients + + row = *(store->append()); + row[columns.col_label ] = _("No gradient"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + + } else if (selection_empty) { + // Document has gradients, but nothing is currently selected. + + row = *(store->append()); + row[columns.col_label ] = _("Nothing Selected"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + + } else { + + if (gr_selected == nullptr) { + row = *(store->append()); + row[columns.col_label ] = _("No gradient"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + } + + if (gr_multi) { + row = *(store->append()); + row[columns.col_label ] = _("Multiple gradients"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + } + + int idx = 0; + for (auto it : gl) { + SPGradient *gradient = SP_GRADIENT(it); + + Glib::ustring label = gr_prepare_label(gradient); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = sp_gradient_to_pixbuf_ref(gradient, 64, 16); + + row = *(store->append()); + row[columns.col_label ] = label; + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_pixbuf ] = pixbuf; + row[columns.col_data ] = gradient; + row[columns.col_sensitive] = true; + + if (gradient == gr_selected) { + selected = idx; + } + idx ++; + } + + if (gr_multi) { + selected = 0; // This will show "Multiple Gradients" + } + } + + return selected; +} + +/* + * Get the gradient of the selected desktop item + * This is gradient containing the repeat settings, not the underlying "getVector" href linked gradient. + */ +void gr_get_dt_selected_gradient(Inkscape::Selection *selection, SPGradient *&gr_selected) +{ + SPGradient *gradient = nullptr; + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i;// get the items gradient, not the getVector() version + SPStyle *style = item->style; + SPPaintServer *server = nullptr; + + if (style && (style->fill.isPaintserver())) { + server = item->style->getFillPaintServer(); + } + if (style && (style->stroke.isPaintserver())) { + server = item->style->getStrokePaintServer(); + } + + if ( SP_IS_GRADIENT(server) ) { + gradient = SP_GRADIENT(server); + } + } + + if (gradient && gradient->isSolid()) { + gradient = nullptr; + } + + if (gradient) { + gr_selected = gradient; + } +} + +/* + * Get the current selection and dragger status from the desktop + */ +void gr_read_selection( Inkscape::Selection *selection, + GrDrag *drag, + SPGradient *&gr_selected, + bool &gr_multi, + SPGradientSpread &spr_selected, + bool &spr_multi ) +{ + if (drag && !drag->selected.empty()) { + // GRADIENTFIXME: make this work for more than one selected dragger? + GrDragger *dragger = *(drag->selected.begin()); + for(auto draggable : dragger->draggables) { //for all draggables of dragger + SPGradient *gradient = sp_item_gradient_get_vector(draggable->item, draggable->fill_or_stroke); + SPGradientSpread spread = sp_item_gradient_get_spread(draggable->item, draggable->fill_or_stroke); + + if (gradient && gradient->isSolid()) { + gradient = nullptr; + } + + if (gradient && (gradient != gr_selected)) { + if (gr_selected) { + gr_multi = true; + } else { + gr_selected = gradient; + } + } + if (spread != spr_selected) { + if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) { + spr_multi = true; + } else { + spr_selected = spread; + } + } + } + return; + } + + // If no selected dragger, read desktop selection + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + SPStyle *style = item->style; + + if (style && (style->fill.isPaintserver())) { + SPPaintServer *server = item->style->getFillPaintServer(); + if ( SP_IS_GRADIENT(server) ) { + SPGradient *gradient = SP_GRADIENT(server)->getVector(); + SPGradientSpread spread = SP_GRADIENT(server)->fetchSpread(); + + if (gradient && gradient->isSolid()) { + gradient = nullptr; + } + + if (gradient && (gradient != gr_selected)) { + if (gr_selected) { + gr_multi = true; + } else { + gr_selected = gradient; + } + } + if (spread != spr_selected) { + if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) { + spr_multi = true; + } else { + spr_selected = spread; + } + } + } + } + if (style && (style->stroke.isPaintserver())) { + SPPaintServer *server = item->style->getStrokePaintServer(); + if ( SP_IS_GRADIENT(server) ) { + SPGradient *gradient = SP_GRADIENT(server)->getVector(); + SPGradientSpread spread = SP_GRADIENT(server)->fetchSpread(); + + if (gradient && gradient->isSolid()) { + gradient = nullptr; + } + + if (gradient && (gradient != gr_selected)) { + if (gr_selected) { + gr_multi = true; + } else { + gr_selected = gradient; + } + } + if (spread != spr_selected) { + if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) { + spr_multi = true; + } else { + spr_selected = spread; + } + } + } + } + } + } + +namespace Inkscape { +namespace UI { +namespace Toolbar { +GradientToolbar::GradientToolbar(SPDesktop *desktop) + : Toolbar(desktop) +{ + auto prefs = Inkscape::Preferences::get(); + + /* New gradient linear or radial */ + { + add_label(_("New:")); + + Gtk::RadioToolButton::Group new_type_group; + + auto linear_button = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("linear"))); + linear_button->set_tooltip_text(_("Create linear gradient")); + linear_button->set_icon_name(INKSCAPE_ICON("paint-gradient-linear")); + _new_type_buttons.push_back(linear_button); + + auto radial_button = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("radial"))); + radial_button->set_tooltip_text(_("Create radial (elliptic or circular) gradient")); + radial_button->set_icon_name(INKSCAPE_ICON("paint-gradient-radial")); + _new_type_buttons.push_back(radial_button); + + gint mode = prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR); + _new_type_buttons[ mode == SP_GRADIENT_TYPE_LINEAR ? 0 : 1 ]->set_active(); // linear == 1, radial == 2 + + int btn_index = 0; + for (auto btn : _new_type_buttons) + { + btn->set_sensitive(true); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &GradientToolbar::new_type_changed), btn_index++)); + add(*btn); + } + } + + /* New gradient on fill or stroke*/ + { + Gtk::RadioToolButton::Group new_fillstroke_group; + + auto fill_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("fill"))); + fill_btn->set_tooltip_text(_("Create gradient in the fill")); + fill_btn->set_icon_name(INKSCAPE_ICON("object-fill")); + _new_fillstroke_buttons.push_back(fill_btn); + + auto stroke_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("stroke"))); + stroke_btn->set_tooltip_text(_("Create gradient in the stroke")); + stroke_btn->set_icon_name(INKSCAPE_ICON("object-stroke")); + _new_fillstroke_buttons.push_back(stroke_btn); + + auto fsmode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + _new_fillstroke_buttons[ fsmode == Inkscape::FOR_FILL ? 0 : 1 ]->set_active(); + + auto btn_index = 0; + for (auto btn : _new_fillstroke_buttons) + { + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &GradientToolbar::new_fillstroke_changed), btn_index++)); + btn->set_sensitive(); + add(*btn); + } + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Gradient Select list*/ + { + UI::Widget::ComboToolItemColumns columns; + + auto store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("No gradient"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; + + _select_cb = UI::Widget::ComboToolItem::create(_("Select"), // Label + "", // Tooltip + "Not Used", // Icon + store ); // Tree store + + _select_cb->use_icon( false ); + _select_cb->use_pixbuf( true ); + _select_cb->use_group_label( true ); + _select_cb->set_active( 0 ); + _select_cb->set_sensitive( false ); + + add(*_select_cb); + _select_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::gradient_changed)); + } + + // Gradients Linked toggle + { + _linked_item = add_toggle_button(_("Link gradients"), + _("Link gradients to change all related gradients")); + _linked_item->set_icon_name(INKSCAPE_ICON("object-unlocked")); + _linked_item->signal_toggled().connect(sigc::mem_fun(*this, &GradientToolbar::linked_changed)); + + bool linkedmode = prefs->getBool("/options/forkgradientvectors/value", true); + _linked_item->set_active(!linkedmode); + } + + /* Reverse */ + { + _stops_reverse_item = Gtk::manage(new Gtk::ToolButton(_("Reverse"))); + _stops_reverse_item->set_tooltip_text(_("Reverse the direction of the gradient")); + _stops_reverse_item->set_icon_name(INKSCAPE_ICON("object-flip-horizontal")); + _stops_reverse_item->signal_clicked().connect(sigc::mem_fun(*this, &GradientToolbar::reverse)); + add(*_stops_reverse_item); + _stops_reverse_item->set_sensitive(false); + } + + // Gradient Spread type (how a gradient is drawn outside its nominal area) + { + UI::Widget::ComboToolItemColumns columns; + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + std::vector<gchar*> spread_dropdown_items_list = { + const_cast<gchar *>(C_("Gradient repeat type", "None")), + _("Reflected"), + _("Direct") + }; + + for (auto item: spread_dropdown_items_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label ] = item; + row[columns.col_sensitive] = true; + } + + _spread_cb = Gtk::manage(UI::Widget::ComboToolItem::create(_("Repeat"), + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/pservers.html#LinearGradientSpreadMethodAttribute + _("Whether to fill with flat color beyond the ends of the gradient vector " + "(spreadMethod=\"pad\"), or repeat the gradient in the same direction " + "(spreadMethod=\"repeat\"), or repeat the gradient in alternating opposite " + "directions (spreadMethod=\"reflect\")"), + "Not Used", store)); + _spread_cb->use_group_label(true); + + _spread_cb->set_active(0); + _spread_cb->set_sensitive(false); + + _spread_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::spread_changed)); + add(*_spread_cb); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Gradient Stop list */ + { + UI::Widget::ComboToolItemColumns columns; + + auto store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("No stops"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; + + _stop_cb = + UI::Widget::ComboToolItem::create(_("Stops" ), // Label + "", // Tooltip + "Not Used", // Icon + store ); // Tree store + + _stop_cb->use_icon( false ); + _stop_cb->use_pixbuf( true ); + _stop_cb->use_group_label( true ); + _stop_cb->set_active( 0 ); + _stop_cb->set_sensitive( false ); + + add(*_stop_cb); + _stop_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::stop_changed)); + } + + /* Offset */ + { + auto offset_val = prefs->getDouble("/tools/gradient/stopoffset", 0); + _offset_adj = Gtk::Adjustment::create(offset_val, 0.0, 1.0, 0.01, 0.1); + _offset_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("gradient-stopoffset", C_("Gradient", "Offset:"), _offset_adj, 0.01, 2)); + _offset_item->set_tooltip_text(_("Offset of selected stop")); + _offset_item->set_focus_widget(desktop->canvas); + _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &GradientToolbar::stop_offset_adjustment_changed)); + add(*_offset_item); + _offset_item->set_sensitive(false); + } + + /* Add stop */ + { + _stops_add_item = Gtk::manage(new Gtk::ToolButton(_("Insert new stop"))); + _stops_add_item->set_tooltip_text(_("Insert new stop")); + _stops_add_item->set_icon_name(INKSCAPE_ICON("node-add")); + _stops_add_item->signal_clicked().connect(sigc::mem_fun(*this, &GradientToolbar::add_stop)); + add(*_stops_add_item); + _stops_add_item->set_sensitive(false); + } + + /* Delete stop */ + { + _stops_delete_item = Gtk::manage(new Gtk::ToolButton(_("Delete stop"))); + _stops_delete_item->set_tooltip_text(_("Delete stop")); + _stops_delete_item->set_icon_name(INKSCAPE_ICON("node-delete")); + _stops_delete_item->signal_clicked().connect(sigc::mem_fun(*this, &GradientToolbar::remove_stop)); + add(*_stops_delete_item); + _stops_delete_item->set_sensitive(false); + } + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &GradientToolbar::check_ec)); + + show_all(); +} + +/** + * Gradient auxiliary toolbar construction and setup. + * + */ +GtkWidget * +GradientToolbar::create(SPDesktop * desktop) +{ + auto toolbar = new GradientToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +GradientToolbar::new_type_changed(int mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/gradient/newgradient", + mode == 0 ? SP_GRADIENT_TYPE_LINEAR : SP_GRADIENT_TYPE_RADIAL); +} + +void +GradientToolbar::new_fillstroke_changed(int mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::PaintTarget fsmode = (mode == 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + prefs->setInt("/tools/gradient/newfillorstroke", (fsmode == Inkscape::FOR_FILL) ? 1 : 0); +} + +/* + * User selected a gradient from the combobox + */ +void +GradientToolbar::gradient_changed(int active) +{ + if (blocked) { + return; + } + + if (active < 0) { + return; + } + + blocked = true; + + SPGradient *gr = get_selected_gradient(); + + if (gr) { + gr = sp_gradient_ensure_vector_normalized(gr); + + Inkscape::Selection *selection = _desktop->getSelection(); + ToolBase *ev = _desktop->getEventContext(); + + gr_apply_gradient(selection, ev ? ev->get_drag() : nullptr, gr); + + DocumentUndo::done(_desktop->getDocument(), _("Assign gradient to object"), INKSCAPE_ICON("color-gradient")); + } + + blocked = false; +} + +/** + * \brief Return gradient selected in menu + */ +SPGradient * +GradientToolbar::get_selected_gradient() +{ + int active = _select_cb->get_active(); + + auto store = _select_cb->get_store(); + auto row = store->children()[active]; + UI::Widget::ComboToolItemColumns columns; + + void* pointer = row[columns.col_data]; + SPGradient *gr = static_cast<SPGradient *>(pointer); + + return gr; +} + +/** + * \brief User selected a spread method from the combobox + */ +void +GradientToolbar::spread_changed(int active) +{ + if (blocked) { + return; + } + + blocked = true; + + Inkscape::Selection *selection = _desktop->getSelection(); + SPGradient *gradient = nullptr; + gr_get_dt_selected_gradient(selection, gradient); + + if (gradient) { + SPGradientSpread spread = (SPGradientSpread) active; + gradient->setSpread(spread); + gradient->updateRepr(); + + DocumentUndo::done(_desktop->getDocument(), _("Set gradient repeat"), INKSCAPE_ICON("color-gradient")); + } + + blocked = false; +} + +/** + * \brief User selected a stop from the combobox + */ +void +GradientToolbar::stop_changed(int active) +{ + if (blocked) { + return; + } + + blocked = true; + + ToolBase *ev = _desktop->getEventContext(); + SPGradient *gr = get_selected_gradient(); + + select_dragger_by_stop(gr, ev); + + blocked = false; +} + +void +GradientToolbar::select_dragger_by_stop(SPGradient *gradient, + ToolBase *ev) +{ + if (!blocked) { + std::cerr << "select_dragger_by_stop: should be blocked!" << std::endl; + } + + if (!ev || !gradient) { + return; + } + + GrDrag *drag = ev->get_drag(); + if (!drag) { + return; + } + + SPStop *stop = get_selected_stop(); + + drag->selectByStop(stop, false, true); + + stop_set_offset(); +} + +/** + * \brief Get stop selected by menu + */ +SPStop * +GradientToolbar::get_selected_stop() +{ + int active = _stop_cb->get_active(); + + auto store = _stop_cb->get_store(); + auto row = store->children()[active]; + UI::Widget::ComboToolItemColumns columns; + void* pointer = row[columns.col_data]; + SPStop *stop = static_cast<SPStop *>(pointer); + + return stop; +} + +/** + * Change desktop dragger selection to this stop + * + * Set the offset widget value (based on which stop is selected) + */ +void +GradientToolbar::stop_set_offset() +{ + if (!blocked) { + std::cerr << "gr_stop_set_offset: should be blocked!" << std::endl; + } + + SPStop *stop = get_selected_stop(); + if (!stop) { + // std::cerr << "gr_stop_set_offset: no stop!" << std::endl; + return; + } + + if (!_offset_item) { + return; + } + bool isEndStop = false; + + SPStop *prev = nullptr; + prev = stop->getPrevStop(); + if (prev != nullptr ) { + _offset_adj->set_lower(prev->offset); + } else { + isEndStop = true; + _offset_adj->set_lower(0); + } + + SPStop *next = nullptr; + next = stop->getNextStop(); + if (next != nullptr ) { + _offset_adj->set_upper(next->offset); + } else { + isEndStop = true; + _offset_adj->set_upper(1.0); + } + + _offset_adj->set_value(stop->offset); + _offset_item->set_sensitive( !isEndStop ); +} + +/** + * \brief User changed the offset + */ +void +GradientToolbar::stop_offset_adjustment_changed() +{ + if (blocked) { + return; + } + + blocked = true; + + SPStop *stop = get_selected_stop(); + if (stop) { + stop->offset = _offset_adj->get_value(); + stop->getRepr()->setAttributeCssDouble("offset", stop->offset); + + DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", _("Change gradient stop offset"), INKSCAPE_ICON("color-gradient")); + } + + blocked = false; +} + +/** + * \brief Add stop to gradient + */ +void +GradientToolbar::add_stop() +{ + if (!_desktop) { + return; + } + + auto selection = _desktop->getSelection(); + if (!selection) { + return; + } + + auto ev = _desktop->getEventContext(); + if (auto rc = SP_GRADIENT_CONTEXT(ev)) { + rc->add_stops_between_selected_stops(); + } +} + +/** + * \brief Remove stop from vector + */ +void +GradientToolbar::remove_stop() +{ + if (!_desktop) { + return; + } + + auto selection = _desktop->getSelection(); // take from desktop, not from args + if (!selection) { + return; + } + + auto ev = _desktop->getEventContext(); + GrDrag *drag = nullptr; + if (ev) { + drag = ev->get_drag(); + } + + if (drag) { + drag->deleteSelected(); + } +} + +/** + * \brief Reverse vector + */ +void +GradientToolbar::reverse() +{ + sp_gradient_reverse_selected_gradients(_desktop); +} + +/** + * \brief Lock or unlock links + */ +void +GradientToolbar::linked_changed() +{ + bool active = _linked_item->get_active(); + if ( active ) { + _linked_item->set_icon_name(INKSCAPE_ICON("object-locked")); + } else { + _linked_item->set_icon_name(INKSCAPE_ICON("object-unlocked")); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/forkgradientvectors/value", !active); +} + +// lp:1327267 +/** + * Checks the current tool and connects gradient aux toolbox signals if it happens to be the gradient tool. + * Called every time the current tool changes by signal emission. + */ +void +GradientToolbar::check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (SP_IS_GRADIENT_CONTEXT(ec)) { + Inkscape::Selection *selection = desktop->getSelection(); + SPDocument *document = desktop->getDocument(); + + // connect to selection modified and changed signals + _connection_changed = selection->connectChanged(sigc::mem_fun(*this, &GradientToolbar::selection_changed)); + _connection_modified = selection->connectModified(sigc::mem_fun(*this, &GradientToolbar::selection_modified)); + _connection_subselection_changed = desktop->connect_gradient_stop_selected([=](void* sender, SPStop* stop){ + drag_selection_changed(nullptr); + }); + + // Is this necessary? Couldn't hurt. + selection_changed(selection); + + // connect to release and modified signals of the defs (i.e. when someone changes gradient) + _connection_defs_release = document->getDefs()->connectRelease(sigc::mem_fun(*this, &GradientToolbar::defs_release)); + _connection_defs_modified = document->getDefs()->connectModified(sigc::mem_fun(*this, &GradientToolbar::defs_modified)); + } else { + if (_connection_changed) + _connection_changed.disconnect(); + if (_connection_modified) + _connection_modified.disconnect(); + if (_connection_subselection_changed) + _connection_subselection_changed.disconnect(); + if (_connection_defs_release) + _connection_defs_release.disconnect(); + if (_connection_defs_modified) + _connection_defs_modified.disconnect(); + } +} + +/** + * Core function, setup all the widgets whenever something changes on the desktop + */ +void +GradientToolbar::selection_changed(Inkscape::Selection * /*selection*/) +{ + if (blocked) + return; + + blocked = true; + + if (!_desktop) { + return; + } + + Inkscape::Selection *selection = _desktop->getSelection(); // take from desktop, not from args + if (selection) { + + ToolBase *ev = _desktop->getEventContext(); + GrDrag *drag = nullptr; + if (ev) { + drag = ev->get_drag(); + } + + SPGradient *gr_selected = nullptr; + SPGradientSpread spr_selected = SP_GRADIENT_SPREAD_UNDEFINED; + bool gr_multi = false; + bool spr_multi = false; + + gr_read_selection(selection, drag, gr_selected, gr_multi, spr_selected, spr_multi); + + // Gradient selection menu + auto store = _select_cb->get_store(); + int gradient = gr_vector_list (store, _desktop, selection->isEmpty(), gr_selected, gr_multi); + + if (gradient < 0) { + // No selection or no gradients + _select_cb->set_active( 0 ); + _select_cb->set_sensitive (false); + } else { + // Single gradient or multiple gradients + _select_cb->set_active( gradient ); + _select_cb->set_sensitive (true); + } + + // Spread menu + _spread_cb->set_sensitive( gr_selected && !gr_multi ); + _spread_cb->set_active( gr_selected ? (int)spr_selected : 0 ); + + _stops_add_item->set_sensitive((gr_selected && !gr_multi && drag && !drag->selected.empty())); + _stops_delete_item->set_sensitive((gr_selected && !gr_multi && drag && !drag->selected.empty())); + _stops_reverse_item->set_sensitive((gr_selected!= nullptr)); + + _stop_cb->set_sensitive( gr_selected && !gr_multi); + + update_stop_list (gr_selected, nullptr, gr_multi); + select_stop_by_draggers(gr_selected, ev); + } + + blocked = false; +} + +/** + * \brief Construct stop list + */ +int +GradientToolbar::update_stop_list( SPGradient *gradient, SPStop *new_stop, bool gr_multi) +{ + if (!blocked) { + std::cerr << "update_stop_list should be blocked!" << std::endl; + } + + int selected = -1; + + auto store = _stop_cb->get_store(); + + if (!store) { + return selected; + } + + store->clear(); + + UI::Widget::ComboToolItemColumns columns; + Gtk::TreeModel::Row row; + + if (!SP_IS_GRADIENT(gradient)) { + // No valid gradient + + row = *(store->append()); + row[columns.col_label ] = _("No gradient"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + + } else if (!gradient->hasStops()) { + // Has gradient but it has no stops + + row = *(store->append()); + row[columns.col_label ] = _("No stops in gradient"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + + } else { + // Gradient has stops + + // Get list of stops + for (auto& ochild: gradient->children) { + if (SP_IS_STOP(&ochild)) { + + SPStop *stop = SP_STOP(&ochild); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = sp_gradstop_to_pixbuf_ref (stop, 32, 16); + + Inkscape::XML::Node *repr = reinterpret_cast<SPItem *>(&ochild)->getRepr(); + Glib::ustring label = gr_ellipsize_text(repr->attribute("id"), 25); + + row = *(store->append()); + row[columns.col_label ] = label; + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_pixbuf ] = pixbuf; + row[columns.col_data ] = stop; + row[columns.col_sensitive] = true; + } + } + } + + if (new_stop != nullptr) { + selected = select_stop_in_list (gradient, new_stop); + } + + return selected; +} + +/** + * \brief Find position of new_stop in menu. + */ +int +GradientToolbar::select_stop_in_list(SPGradient *gradient, SPStop *new_stop) +{ + int i = 0; + for (auto& ochild: gradient->children) { + if (SP_IS_STOP(&ochild)) { + if (&ochild == new_stop) { + return i; + } + i++; + } + } + return -1; +} + +/** + * \brief Set stop in menu to match stops selected by draggers + */ +void +GradientToolbar::select_stop_by_draggers(SPGradient *gradient, ToolBase *ev) +{ + if (!blocked) { + std::cerr << "select_stop_by_draggers should be blocked!" << std::endl; + } + + if (!ev || !gradient) + return; + + SPGradient *vector = gradient->getVector(); + if (!vector) + return; + + GrDrag *drag = ev->get_drag(); + + if (!drag || drag->selected.empty()) { + _stop_cb->set_active(0); + stop_set_offset(); + return; + } + + gint n = 0; + SPStop *stop = nullptr; + int selected = -1; + + // For all selected draggers + for(auto dragger : drag->selected) { + + // For all draggables of dragger + for(auto draggable : dragger->draggables) { + + if (draggable->point_type != POINT_RG_FOCUS) { + n++; + if (n > 1) break; + } + + stop = vector->getFirstStop(); + + switch (draggable->point_type) { + case POINT_LG_MID: + case POINT_RG_MID1: + case POINT_RG_MID2: + stop = sp_get_stop_i(vector, draggable->point_i); + break; + case POINT_LG_END: + case POINT_RG_R1: + case POINT_RG_R2: + stop = sp_last_stop(vector); + break; + default: + break; + } + } + if (n > 1) break; + } + + if (n > 1) { + // Multiple stops selected + if (_offset_item) { + _offset_item->set_sensitive(false); + } + + // Stop list always updated first... reinsert "Multiple stops" as first entry. + UI::Widget::ComboToolItemColumns columns; + auto store = _stop_cb->get_store(); + + auto row = *(store->prepend()); + row[columns.col_label ] = _("Multiple stops"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; + selected = 0; + + } else { + selected = select_stop_in_list(gradient, stop); + } + + if (selected < 0) { + _stop_cb->set_active (0); + _stop_cb->set_sensitive (false); + } else { + _stop_cb->set_active (selected); + _stop_cb->set_sensitive (true); + stop_set_offset(); + } +} + +void +GradientToolbar::selection_modified(Inkscape::Selection *selection, guint /*flags*/) +{ + selection_changed(selection); +} + +void +GradientToolbar::drag_selection_changed(gpointer /*dragger*/) +{ + selection_changed(nullptr); +} + +void +GradientToolbar::defs_release(SPObject * /*defs*/) +{ + selection_changed(nullptr); +} + +void +GradientToolbar::defs_modified(SPObject * /*defs*/, guint /*flags*/) +{ + selection_changed(nullptr); +} + +} +} +} +/* + 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/toolbar/gradient-toolbar.h b/src/ui/toolbar/gradient-toolbar.h new file mode 100644 index 0000000..96beb0f --- /dev/null +++ b/src/ui/toolbar/gradient-toolbar.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_TOOLBAR_H +#define SEEN_GRADIENT_TOOLBAR_H + +/* + * Gradient aux toolbar + * + * Authors: + * bulia byak <bulia@dr.com> + * + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; +class SPGradient; +class SPStop; +class SPObject; + +namespace Gtk { +class ComboBoxText; +class RadioToolButton; +class ToolButton; +class ToolItem; +} + +namespace Inkscape { +class Selection; + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class ComboToolItem; +class SpinButtonToolItem; +} + +namespace Toolbar { +class GradientToolbar : public Toolbar { +private: + std::vector<Gtk::RadioToolButton *> _new_type_buttons; + std::vector<Gtk::RadioToolButton *> _new_fillstroke_buttons; + UI::Widget::ComboToolItem *_select_cb; + UI::Widget::ComboToolItem *_spread_cb; + UI::Widget::ComboToolItem *_stop_cb; + + Gtk::ToolButton *_stops_add_item; + Gtk::ToolButton *_stops_delete_item; + Gtk::ToolButton *_stops_reverse_item; + Gtk::ToggleToolButton *_linked_item; + + UI::Widget::SpinButtonToolItem *_offset_item; + + Glib::RefPtr<Gtk::Adjustment> _offset_adj; + + void new_type_changed(int mode); + void new_fillstroke_changed(int mode); + void gradient_changed(int active); + SPGradient * get_selected_gradient(); + void spread_changed(int active); + void stop_changed(int active); + void select_dragger_by_stop(SPGradient *gradient, + UI::Tools::ToolBase *ev); + SPStop * get_selected_stop(); + void stop_set_offset(); + void stop_offset_adjustment_changed(); + void add_stop(); + void remove_stop(); + void reverse(); + void linked_changed(); + void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_changed(Inkscape::Selection *selection); + int update_stop_list( SPGradient *gradient, SPStop *new_stop, bool gr_multi); + int select_stop_in_list(SPGradient *gradient, SPStop *new_stop); + void select_stop_by_draggers(SPGradient *gradient, UI::Tools::ToolBase *ev); + void selection_modified(Inkscape::Selection *selection, guint flags); + void drag_selection_changed(gpointer dragger); + void defs_release(SPObject * defs); + void defs_modified(SPObject *defs, guint flags); + + sigc::connection _connection_changed; + sigc::connection _connection_modified; + sigc::connection _connection_subselection_changed; + sigc::connection _connection_defs_release; + sigc::connection _connection_defs_modified; + +protected: + GradientToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_GRADIENT_TOOLBAR_H */ diff --git a/src/ui/toolbar/lpe-toolbar.cpp b/src/ui/toolbar/lpe-toolbar.cpp new file mode 100644 index 0000000..df45e22 --- /dev/null +++ b/src/ui/toolbar/lpe-toolbar.cpp @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * LPE aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-toolbar.h" + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "live_effects/lpe-line_segment.h" + +#include "ui/dialog/dialog-container.h" +#include "ui/icon-names.h" +#include "ui/tools/lpe-tool.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/unit-tracker.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::DocumentUndo; +using Inkscape::UI::Tools::ToolBase; +using Inkscape::UI::Tools::LpeTool; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +LPEToolbar::LPEToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _tracker(new UnitTracker(Util::UNIT_TYPE_LINEAR)), + _freeze(false), + _currentlpe(nullptr), + _currentlpeitem(nullptr) +{ + _tracker->setActiveUnit(_desktop->getNamedView()->display_units); + + auto unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + auto prefs = Inkscape::Preferences::get(); + prefs->setString("/tools/lpetool/unit", unit->abbr); + + /* Automatically create a list of LPEs that get added to the toolbar **/ + { + Gtk::RadioToolButton::Group mode_group; + + // The first toggle button represents the state that no subtool is active. + auto inactive_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("All inactive"))); + inactive_mode_btn->set_tooltip_text(_("No geometric tool is active")); + inactive_mode_btn->set_icon_name(INKSCAPE_ICON("draw-geometry-inactive")); + _mode_buttons.push_back(inactive_mode_btn); + + Inkscape::LivePathEffect::EffectType type; + for (int i = 1; i < num_subtools; ++i) { // i == 0 ia INVALIDE_LPE. + + type = lpesubtools[i].type; + + auto btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, Inkscape::LivePathEffect::LPETypeConverter.get_label(type))); + btn->set_tooltip_text(_(Inkscape::LivePathEffect::LPETypeConverter.get_label(type).c_str())); + btn->set_icon_name(lpesubtools[i].icon_name); + _mode_buttons.push_back(btn); + } + + int btn_idx = 0; + for (auto btn : _mode_buttons) { + btn->set_sensitive(true); + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &LPEToolbar::mode_changed), btn_idx++)); + } + + int mode = prefs->getInt("/tools/lpetool/mode", 0); + _mode_buttons[mode]->set_active(); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Show limiting bounding box */ + { + _show_bbox_item = add_toggle_button(_("Show limiting bounding box"), + _("Show bounding box (used to cut infinite lines)")); + _show_bbox_item->set_icon_name(INKSCAPE_ICON("show-bounding-box")); + _show_bbox_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::toggle_show_bbox)); + _show_bbox_item->set_active(prefs->getBool( "/tools/lpetool/show_bbox", true )); + } + + /* Set limiting bounding box to bbox of current selection */ + { + // TODO: Shouldn't this just be a button (not toggle button)? + _bbox_from_selection_item = add_toggle_button(_("Get limiting bounding box from selection"), + _("Set limiting bounding box (used to cut infinite lines) to the bounding box of current selection")); + _bbox_from_selection_item->set_icon_name(INKSCAPE_ICON("draw-geometry-set-bounding-box")); + _bbox_from_selection_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::toggle_set_bbox)); + _bbox_from_selection_item->set_active(false); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Combo box to choose line segment type */ + { + UI::Widget::ComboToolItemColumns columns; + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + std::vector<gchar*> line_segment_dropdown_items_list = { + _("Closed"), + _("Open start"), + _("Open end"), + _("Open both") + }; + + for (auto item: line_segment_dropdown_items_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label ] = item; + row[columns.col_sensitive] = true; + } + + _line_segment_combo = Gtk::manage(UI::Widget::ComboToolItem::create(_("Line Type"), _("Choose a line segment type"), "Not Used", store)); + _line_segment_combo->use_group_label(false); + + _line_segment_combo->set_active(0); + + _line_segment_combo->signal_changed().connect(sigc::mem_fun(*this, &LPEToolbar::change_line_segment_type)); + add(*_line_segment_combo); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Display measuring info for selected items */ + { + _measuring_item = add_toggle_button(_("Display measuring info"), + _("Display measuring info for selected items")); + _measuring_item->set_icon_name(INKSCAPE_ICON("draw-geometry-show-measuring-info")); + _measuring_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::toggle_show_measuring_info)); + _measuring_item->set_active( prefs->getBool( "/tools/lpetool/show_measuring_info", true ) ); + } + + // Add the units menu + { + _units_item = _tracker->create_tool_item(_("Units"), ("") ); + add(*_units_item); + _units_item->signal_changed_after().connect(sigc::mem_fun(*this, &LPEToolbar::unit_changed)); + _units_item->set_sensitive( prefs->getBool("/tools/lpetool/show_measuring_info", true)); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Open LPE dialog (to adapt parameters numerically) */ + { + // TODO: Shouldn't this be a regular Gtk::ToolButton (not toggle)? + _open_lpe_dialog_item = add_toggle_button(_("Open LPE dialog"), + _("Open LPE dialog (to adapt parameters numerically)")); + _open_lpe_dialog_item->set_icon_name(INKSCAPE_ICON("dialog-geometry")); + _open_lpe_dialog_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::open_lpe_dialog)); + _open_lpe_dialog_item->set_active(false); + } + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &LPEToolbar::watch_ec)); + + show_all(); +} + +void +LPEToolbar::set_mode(int mode) +{ + _mode_buttons[mode]->set_active(); +} + +GtkWidget * +LPEToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new LPEToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +// this is called when the mode is changed via the toolbar (i.e., one of the subtool buttons is pressed) +void +LPEToolbar::mode_changed(int mode) +{ + using namespace Inkscape::LivePathEffect; + + ToolBase *ec = _desktop->event_context; + if (!SP_IS_LPETOOL_CONTEXT(ec)) { + return; + } + + // only take action if run by the attr_changed listener + if (!_freeze) { + // in turn, prevent listener from responding + _freeze = true; + + EffectType type = lpesubtools[mode].type; + + LpeTool *lc = SP_LPETOOL_CONTEXT(_desktop->event_context); + bool success = lpetool_try_construction(lc, type); + if (success) { + // since the construction was already performed, we set the state back to inactive + _mode_buttons[0]->set_active(); + mode = 0; + } else { + // switch to the chosen subtool + SP_LPETOOL_CONTEXT(_desktop->event_context)->mode = type; + } + + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt( "/tools/lpetool/mode", mode ); + } + + _freeze = false; + } +} + +void +LPEToolbar::toggle_show_bbox() { + auto prefs = Inkscape::Preferences::get(); + + bool show = _show_bbox_item->get_active(); + prefs->setBool("/tools/lpetool/show_bbox", show); + + LpeTool *lc = dynamic_cast<LpeTool *>(_desktop->event_context); + if (lc) { + lpetool_context_reset_limiting_bbox(lc); + } +} + +void +LPEToolbar::toggle_set_bbox() +{ + auto selection = _desktop->selection; + + auto bbox = selection->visualBounds(); + + if (bbox) { + Geom::Point A(bbox->min()); + Geom::Point B(bbox->max()); + + A *= _desktop->doc2dt(); + B *= _desktop->doc2dt(); + + // TODO: should we provide a way to store points in prefs? + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/lpetool/bbox_upperleftx", A[Geom::X]); + prefs->setDouble("/tools/lpetool/bbox_upperlefty", A[Geom::Y]); + prefs->setDouble("/tools/lpetool/bbox_lowerrightx", B[Geom::X]); + prefs->setDouble("/tools/lpetool/bbox_lowerrighty", B[Geom::Y]); + + lpetool_context_reset_limiting_bbox(SP_LPETOOL_CONTEXT(_desktop->event_context)); + } + + _bbox_from_selection_item->set_active(false); +} + +void +LPEToolbar::change_line_segment_type(int mode) +{ + using namespace Inkscape::LivePathEffect; + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + auto line_seg = dynamic_cast<LPELineSegment *>(_currentlpe); + + if (_currentlpeitem && line_seg) { + line_seg->end_type.param_set_value(static_cast<Inkscape::LivePathEffect::EndType>(mode)); + sp_lpe_item_update_patheffect(_currentlpeitem, true, true); + } + + _freeze = false; +} + +void +LPEToolbar::toggle_show_measuring_info() +{ + LpeTool *lc = dynamic_cast<LpeTool *>(_desktop->event_context); + if (!lc) { + return; + } + + bool show = _measuring_item->get_active(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/lpetool/show_measuring_info", show); + + lpetool_show_measuring_info(lc, show); + + _units_item->set_sensitive( show ); +} + +void +LPEToolbar::unit_changed(int /* NotUsed */) +{ + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/tools/lpetool/unit", unit->abbr); + + if (SP_IS_LPETOOL_CONTEXT(_desktop->event_context)) { + LpeTool *lc = SP_LPETOOL_CONTEXT(_desktop->event_context); + lpetool_delete_measuring_items(lc); + lpetool_create_measuring_items(lc); + } +} + +void +LPEToolbar::open_lpe_dialog() +{ + if (dynamic_cast<LpeTool *>(_desktop->event_context)) { + _desktop->getContainer()->new_dialog("LivePathEffect"); + } else { + std::cerr << "LPEToolbar::open_lpe_dialog: LPEToolbar active but current tool is not LPE tool!" << std::endl; + } + _open_lpe_dialog_item->set_active(false); +} + +void +LPEToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (SP_IS_LPETOOL_CONTEXT(ec)) { + // Watch selection + c_selection_modified = desktop->getSelection()->connectModified(sigc::mem_fun(*this, &LPEToolbar::sel_modified)); + c_selection_changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &LPEToolbar::sel_changed)); + sel_changed(desktop->getSelection()); + } else { + if (c_selection_modified) + c_selection_modified.disconnect(); + if (c_selection_changed) + c_selection_changed.disconnect(); + } +} + +void +LPEToolbar::sel_modified(Inkscape::Selection *selection, guint /*flags*/) +{ + ToolBase *ec = selection->desktop()->event_context; + if (SP_IS_LPETOOL_CONTEXT(ec)) { + lpetool_update_measuring_items(SP_LPETOOL_CONTEXT(ec)); + } +} + +void +LPEToolbar::sel_changed(Inkscape::Selection *selection) +{ + using namespace Inkscape::LivePathEffect; + ToolBase *ec = selection->desktop()->event_context; + if (!SP_IS_LPETOOL_CONTEXT(ec)) { + return; + } + LpeTool *lc = SP_LPETOOL_CONTEXT(ec); + + lpetool_delete_measuring_items(lc); + lpetool_create_measuring_items(lc, selection); + + // activate line segment combo box if a single item with LPELineSegment is selected + SPItem *item = selection->singleItem(); + if (item && SP_IS_LPE_ITEM(item) && lpetool_item_has_construction(lc, item)) { + + SPLPEItem *lpeitem = SP_LPE_ITEM(item); + Effect* lpe = lpeitem->getCurrentLPE(); + if (lpe && lpe->effectType() == LINE_SEGMENT) { + LPELineSegment *lpels = static_cast<LPELineSegment*>(lpe); + _currentlpe = lpe; + _currentlpeitem = lpeitem; + _line_segment_combo->set_sensitive(true); + _line_segment_combo->set_active( lpels->end_type.get_value() ); + } else { + _currentlpe = nullptr; + _currentlpeitem = nullptr; + _line_segment_combo->set_sensitive(false); + } + + } else { + _currentlpe = nullptr; + _currentlpeitem = nullptr; + _line_segment_combo->set_sensitive(false); + } +} + +} +} +} + +/* + 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/toolbar/lpe-toolbar.h b/src/ui/toolbar/lpe-toolbar.h new file mode 100644 index 0000000..903d9da --- /dev/null +++ b/src/ui/toolbar/lpe-toolbar.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_LPE_TOOLBAR_H +#define SEEN_LPE_TOOLBAR_H + +/** + * @file + * LPE aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +class SPDesktop; +class SPLPEItem; + +namespace Gtk { +class RadioToolButton; +} + +namespace Inkscape { +class Selection; + +namespace LivePathEffect { +class Effect; +} + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class ComboToolItem; +class UnitTracker; +} + +namespace Toolbar { +class LPEToolbar : public Toolbar { +private: + std::unique_ptr<UI::Widget::UnitTracker> _tracker; + std::vector<Gtk::RadioToolButton *> _mode_buttons; + Gtk::ToggleToolButton *_show_bbox_item; + Gtk::ToggleToolButton *_bbox_from_selection_item; + Gtk::ToggleToolButton *_measuring_item; + Gtk::ToggleToolButton *_open_lpe_dialog_item; + UI::Widget::ComboToolItem *_line_segment_combo; + UI::Widget::ComboToolItem *_units_item; + + bool _freeze; + + LivePathEffect::Effect *_currentlpe; + SPLPEItem *_currentlpeitem; + + sigc::connection c_selection_modified; + sigc::connection c_selection_changed; + + void mode_changed(int mode); + void unit_changed(int not_used); + void sel_modified(Inkscape::Selection *selection, guint flags); + void sel_changed(Inkscape::Selection *selection); + void change_line_segment_type(int mode); + void watch_ec(SPDesktop* desktop, UI::Tools::ToolBase* ec); + + void toggle_show_bbox(); + void toggle_set_bbox(); + void toggle_show_measuring_info(); + void open_lpe_dialog(); + +protected: + LPEToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); + void set_mode(int mode); +}; + +} +} +} + +#endif /* !SEEN_LPE_TOOLBAR_H */ diff --git a/src/ui/toolbar/marker-toolbar.cpp b/src/ui/toolbar/marker-toolbar.cpp new file mode 100644 index 0000000..d60f2d6 --- /dev/null +++ b/src/ui/toolbar/marker-toolbar.cpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Marker edit mode toolbar - onCanvas marker editing of marker orientation, position, scale + *//* + * Authors: + * see git history + * Rachana Podaralla <rpodaralla3@gatech.edu> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include "marker-toolbar.h" +#include "document-undo.h" +#include "preferences.h" +#include "desktop.h" +#include "ui/widget/canvas.h" +namespace Inkscape { +namespace UI { +namespace Toolbar { + +MarkerToolbar::MarkerToolbar(SPDesktop *desktop) + : Toolbar(desktop) +{ +} + +GtkWidget* MarkerToolbar::create(SPDesktop *desktop) +{ + auto toolbar = Gtk::manage(new MarkerToolbar(desktop)); + return GTK_WIDGET(toolbar->gobj()); +} + +}}}
\ No newline at end of file diff --git a/src/ui/toolbar/marker-toolbar.h b/src/ui/toolbar/marker-toolbar.h new file mode 100644 index 0000000..f5f4d64 --- /dev/null +++ b/src/ui/toolbar/marker-toolbar.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Marker edit mode toolbar - onCanvas marker editing of marker orientation, position, scale + *//* + * Authors: + * see git history + * Rachana Podaralla <rpodaralla3@gatech.edu> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_MARKER_TOOLBAR_H +#define SEEN_MARKER_TOOLBAR_H + +#include "toolbar.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +class MarkerToolbar : public Toolbar { +protected: + MarkerToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +}}} +#endif
\ No newline at end of file diff --git a/src/ui/toolbar/measure-toolbar.cpp b/src/ui/toolbar/measure-toolbar.cpp new file mode 100644 index 0000000..92ca4c5 --- /dev/null +++ b/src/ui/toolbar/measure-toolbar.cpp @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Measure aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "measure-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "message-stack.h" +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/tools/measure-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::Util::Unit; +using Inkscape::DocumentUndo; +using Inkscape::UI::Tools::MeasureTool; + +static MeasureTool *get_measure_tool(SPDesktop *desktop) +{ + if (desktop) { + return dynamic_cast<MeasureTool *>(desktop->event_context); + } + return nullptr; +} + + + +namespace Inkscape { +namespace UI { +namespace Toolbar { +MeasureToolbar::MeasureToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)) +{ + auto prefs = Inkscape::Preferences::get(); + auto unit = desktop->getNamedView()->getDisplayUnit(); + _tracker->setActiveUnitByAbbr(prefs->getString("/tools/measure/unit", unit->abbr).c_str()); + + /* Font Size */ + { + auto font_size_val = prefs->getDouble("/tools/measure/fontsize", 10.0); + _font_size_adj = Gtk::Adjustment::create(font_size_val, 1.0, 36.0, 1.0, 4.0); + auto font_size_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-fontsize", _("Font Size:"), _font_size_adj, 0, 2)); + font_size_item->set_tooltip_text(_("The font size to be used in the measurement labels")); + font_size_item->set_focus_widget(desktop->canvas); + _font_size_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::fontsize_value_changed)); + add(*font_size_item); + } + + /* Precision */ + { + auto precision_val = prefs->getDouble("/tools/measure/precision", 2); + _precision_adj = Gtk::Adjustment::create(precision_val, 0, 10, 1, 0); + auto precision_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-precision", _("Precision:"), _precision_adj, 0, 0)); + precision_item->set_tooltip_text(_("Decimal precision of measure")); + precision_item->set_focus_widget(desktop->canvas); + _precision_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::precision_value_changed)); + add(*precision_item); + } + + /* Scale */ + { + auto scale_val = prefs->getDouble("/tools/measure/scale", 100.0); + _scale_adj = Gtk::Adjustment::create(scale_val, 0.0, 90000.0, 1.0, 4.0); + auto scale_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-scale", _("Scale %:"), _scale_adj, 0, 3)); + scale_item->set_tooltip_text(_("Scale the results")); + scale_item->set_focus_widget(desktop->canvas); + _scale_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::scale_value_changed)); + add(*scale_item); + } + + /* units label */ + { + auto unit_label = Gtk::manage(new UI::Widget::LabelToolItem(_("Units:"))); + unit_label->set_tooltip_text(_("The units to be used for the measurements")); + unit_label->set_use_markup(true); + add(*unit_label); + } + + /* units menu */ + { + auto ti = _tracker->create_tool_item(_("Units"), _("The units to be used for the measurements") ); + ti->signal_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::unit_changed)); + add(*ti); + } + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + + /* measure only selected */ + { + _only_selected_item = add_toggle_button(_("Measure only selected"), + _("Measure only selected")); + _only_selected_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-center")); + _only_selected_item->set_active(prefs->getBool("/tools/measure/only_selected", false)); + _only_selected_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_only_selected)); + } + + /* ignore_1st_and_last */ + { + _ignore_1st_and_last_item = add_toggle_button(_("Ignore first and last"), + _("Ignore first and last")); + _ignore_1st_and_last_item->set_icon_name(INKSCAPE_ICON("draw-geometry-line-segment")); + _ignore_1st_and_last_item->set_active(prefs->getBool("/tools/measure/ignore_1st_and_last", true)); + _ignore_1st_and_last_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_ignore_1st_and_last)); + } + + /* measure in betweens */ + { + _inbetween_item = add_toggle_button(_("Show measures between items"), + _("Show measures between items")); + _inbetween_item->set_icon_name(INKSCAPE_ICON("distribute-randomize")); + _inbetween_item->set_active(prefs->getBool("/tools/measure/show_in_between", true)); + _inbetween_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_show_in_between)); + } + + /* only visible */ + { + _show_hidden_item = add_toggle_button(_("Show hidden intersections"), + _("Show hidden intersections")); + _show_hidden_item->set_icon_name(INKSCAPE_ICON("object-hidden")); + _show_hidden_item->set_active(prefs->getBool("/tools/measure/show_hidden", true)); + _show_hidden_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_show_hidden)) ; + } + + /* measure only current layer */ + { + _all_layers_item = add_toggle_button(_("Measure all layers"), + _("Measure all layers")); + _all_layers_item->set_icon_name(INKSCAPE_ICON("dialog-layers")); + _all_layers_item->set_active(prefs->getBool("/tools/measure/all_layers", true)); + _all_layers_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_all_layers)); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* toggle start end */ + { + _reverse_item = Gtk::manage(new Gtk::ToolButton(_("Reverse measure"))); + _reverse_item->set_tooltip_text(_("Reverse measure")); + _reverse_item->set_icon_name(INKSCAPE_ICON("draw-geometry-mirror")); + _reverse_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::reverse_knots)); + add(*_reverse_item); + } + + /* phantom measure */ + { + _to_phantom_item = Gtk::manage(new Gtk::ToolButton(_("Phantom measure"))); + _to_phantom_item->set_tooltip_text(_("Phantom measure")); + _to_phantom_item->set_icon_name(INKSCAPE_ICON("selection-make-bitmap-copy")); + _to_phantom_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_phantom)); + add(*_to_phantom_item); + } + + /* to guides */ + { + _to_guides_item = Gtk::manage(new Gtk::ToolButton(_("To guides"))); + _to_guides_item->set_tooltip_text(_("To guides")); + _to_guides_item->set_icon_name(INKSCAPE_ICON("guides")); + _to_guides_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_guides)); + add(*_to_guides_item); + } + + /* to item */ + { + _to_item_item = Gtk::manage(new Gtk::ToolButton(_("Convert to item"))); + _to_item_item->set_tooltip_text(_("Convert to item")); + _to_item_item->set_icon_name(INKSCAPE_ICON("path-reverse")); + _to_item_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_item)); + add(*_to_item_item); + } + + /* to mark dimensions */ + { + _mark_dimension_item = Gtk::manage(new Gtk::ToolButton(_("Mark Dimension"))); + _mark_dimension_item->set_tooltip_text(_("Mark Dimension")); + _mark_dimension_item->set_icon_name(INKSCAPE_ICON("tool-pointer")); + _mark_dimension_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_mark_dimension)); + add(*_mark_dimension_item); + } + + /* Offset */ + { + auto offset_val = prefs->getDouble("/tools/measure/offset", 5.0); + _offset_adj = Gtk::Adjustment::create(offset_val, 0.0, 90000.0, 1.0, 4.0); + auto offset_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-offset", _("Offset:"), _offset_adj, 0, 2)); + offset_item->set_tooltip_text(_("Mark dimension offset")); + offset_item->set_focus_widget(desktop->canvas); + _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::offset_value_changed)); + add(*offset_item); + } + + show_all(); +} + +GtkWidget * +MeasureToolbar::create(SPDesktop * desktop) +{ + auto toolbar = new MeasureToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} // MeasureToolbar::prep() + +void +MeasureToolbar::fontsize_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/measure/fontsize"), + _font_size_adj->get_value()); + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->showCanvasItems(); + } + } +} + +void +MeasureToolbar::unit_changed(int /* notUsed */) +{ + Glib::ustring const unit = _tracker->getActiveUnit()->abbr; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/tools/measure/unit", unit); + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::precision_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(Glib::ustring("/tools/measure/precision"), + _precision_adj->get_value()); + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->showCanvasItems(); + } + } +} + +void +MeasureToolbar::scale_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/measure/scale"), + _scale_adj->get_value()); + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->showCanvasItems(); + } + } +} + +void +MeasureToolbar::offset_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/measure/offset"), + _offset_adj->get_value()); + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->showCanvasItems(); + } + } +} + +void +MeasureToolbar::toggle_only_selected() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _only_selected_item->get_active(); + prefs->setBool("/tools/measure/only_selected", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Measures only selected.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Measure all.")); + } + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::toggle_ignore_1st_and_last() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _ignore_1st_and_last_item->get_active(); + prefs->setBool("/tools/measure/ignore_1st_and_last", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Start and end measures inactive.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Start and end measures active.")); + } + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::toggle_show_in_between() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _inbetween_item->get_active(); + prefs->setBool("/tools/measure/show_in_between", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Compute all elements.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Compute max length.")); + } + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::toggle_show_hidden() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _show_hidden_item->get_active(); + prefs->setBool("/tools/measure/show_hidden", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Show all crossings.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Show visible crossings.")); + } + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::toggle_all_layers() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _all_layers_item->get_active(); + prefs->setBool("/tools/measure/all_layers", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Use all layers in the measure.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Use current layer in the measure.")); + } + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::reverse_knots() +{ + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->reverseKnots(); + } +} + +void +MeasureToolbar::to_phantom() +{ + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->toPhantom(); + } +} + +void +MeasureToolbar::to_guides() +{ + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->toGuides(); + } +} + +void +MeasureToolbar::to_item() +{ + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->toItem(); + } +} + +void +MeasureToolbar::to_mark_dimension() +{ + MeasureTool *mt = get_measure_tool(_desktop); + if (mt) { + mt->toMarkDimension(); + } +} + +} +} +} + + +/* + 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/toolbar/measure-toolbar.h b/src/ui/toolbar/measure-toolbar.h new file mode 100644 index 0000000..a922fa1 --- /dev/null +++ b/src/ui/toolbar/measure-toolbar.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_MEASURE_TOOLBAR_H +#define SEEN_MEASURE_TOOLBAR_H + +/** + * @file + * Measure aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Widget { +class UnitTracker; +} + +namespace Toolbar { +class MeasureToolbar : public Toolbar { +private: + UI::Widget::UnitTracker *_tracker; + Glib::RefPtr<Gtk::Adjustment> _font_size_adj; + Glib::RefPtr<Gtk::Adjustment> _precision_adj; + Glib::RefPtr<Gtk::Adjustment> _scale_adj; + Glib::RefPtr<Gtk::Adjustment> _offset_adj; + + Gtk::ToggleToolButton *_only_selected_item; + Gtk::ToggleToolButton *_ignore_1st_and_last_item; + Gtk::ToggleToolButton *_inbetween_item; + Gtk::ToggleToolButton *_show_hidden_item; + Gtk::ToggleToolButton *_all_layers_item; + + Gtk::ToolButton *_reverse_item; + Gtk::ToolButton *_to_phantom_item; + Gtk::ToolButton *_to_guides_item; + Gtk::ToolButton *_to_item_item; + Gtk::ToolButton *_mark_dimension_item; + + void fontsize_value_changed(); + void unit_changed(int notUsed); + void precision_value_changed(); + void scale_value_changed(); + void offset_value_changed(); + void toggle_only_selected(); + void toggle_ignore_1st_and_last(); + void toggle_show_hidden(); + void toggle_show_in_between(); + void toggle_all_layers(); + void reverse_knots(); + void to_phantom(); + void to_guides(); + void to_item(); + void to_mark_dimension(); + +protected: + MeasureToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_MEASURE_TOOLBAR_H */ diff --git a/src/ui/toolbar/mesh-toolbar.cpp b/src/ui/toolbar/mesh-toolbar.cpp new file mode 100644 index 0000000..a1ed631 --- /dev/null +++ b/src/ui/toolbar/mesh-toolbar.cpp @@ -0,0 +1,613 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient aux toolbar + * + * Authors: + * bulia byak <bulia@dr.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Abhishek Sharma + * Tavmjong Bah <tavjong@free.fr> + * + * Copyright (C) 2012 Tavmjong Bah + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "mesh-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/comboboxtext.h> +#include <gtkmm/messagedialog.h> +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "gradient-chemistry.h" +#include "gradient-drag.h" +#include "inkscape.h" + +#include "object/sp-defs.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-stop.h" +#include "style.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-names.h" +#include "ui/simple-pref-pusher.h" +#include "ui/tools/gradient-tool.h" +#include "ui/tools/mesh-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/color-preview.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/gradient-image.h" +#include "ui/widget/spin-button-tool-item.h" + +using Inkscape::DocumentUndo; +using Inkscape::UI::Tools::MeshTool; + +static bool blocked = false; + +// Get a list of selected meshes taking into account fill/stroke toggles +std::vector<SPMeshGradient *> ms_get_dt_selected_gradients(Inkscape::Selection *selection) +{ + std::vector<SPMeshGradient *> ms_selected; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool edit_fill = prefs->getBool("/tools/mesh/edit_fill", true); + bool edit_stroke = prefs->getBool("/tools/mesh/edit_stroke", true); + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i;// get the items gradient, not the getVector() version + SPStyle *style = item->style; + + if (style) { + + + if (edit_fill && style->fill.isPaintserver()) { + SPPaintServer *server = item->style->getFillPaintServer(); + SPMeshGradient *mesh = dynamic_cast<SPMeshGradient *>(server); + if (mesh) { + ms_selected.push_back(mesh); + } + } + + if (edit_stroke && style->stroke.isPaintserver()) { + SPPaintServer *server = item->style->getStrokePaintServer(); + SPMeshGradient *mesh = dynamic_cast<SPMeshGradient *>(server); + if (mesh) { + ms_selected.push_back(mesh); + } + } + } + + } + return ms_selected; +} + + +/* + * Get the current selection status from the desktop + */ +void ms_read_selection( Inkscape::Selection *selection, + SPMeshGradient *&ms_selected, + bool &ms_selected_multi, + SPMeshType &ms_type, + bool &ms_type_multi ) +{ + ms_selected = nullptr; + ms_selected_multi = false; + ms_type = SP_MESH_TYPE_COONS; + ms_type_multi = false; + + bool first = true; + + // Read desktop selection, taking into account fill/stroke toggles + std::vector<SPMeshGradient *> meshes = ms_get_dt_selected_gradients( selection ); + for (auto & meshe : meshes) { + if (first) { + ms_selected = meshe; + ms_type = meshe->type; + first = false; + } else { + if (ms_selected != meshe) { + ms_selected_multi = true; + } + if (ms_type != meshe->type) { + ms_type_multi = true; + } + } + } +} + + +/* + * Callback functions for user actions + */ + + +/** Temporary hack: Returns the mesh tool in the active desktop. + * Will go away during tool refactoring. */ +static MeshTool *get_mesh_tool() +{ + MeshTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (SP_IS_MESH_CONTEXT(ec)) { + tool = static_cast<MeshTool*>(ec); + } + } + return tool; +} + + +namespace Inkscape { +namespace UI { +namespace Toolbar { +MeshToolbar::MeshToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _edit_fill_pusher(nullptr) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /* New mesh: normal or conical */ + { + add_label(_("New:")); + + Gtk::RadioToolButton::Group new_type_group; + + auto normal_type_btn = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("normal"))); + normal_type_btn->set_tooltip_text(_("Create mesh gradient")); + normal_type_btn->set_icon_name(INKSCAPE_ICON("paint-gradient-mesh")); + _new_type_buttons.push_back(normal_type_btn); + + auto conical_type_btn = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("conical"))); + conical_type_btn->set_tooltip_text(_("Create conical gradient")); + conical_type_btn->set_icon_name(INKSCAPE_ICON("paint-gradient-conical")); + _new_type_buttons.push_back(conical_type_btn); + + int btn_idx = 0; + for (auto btn : _new_type_buttons) { + add(*btn); + btn->set_sensitive(); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &MeshToolbar::new_geometry_changed), btn_idx++)); + } + + gint mode = prefs->getInt("/tools/mesh/mesh_geometry", SP_MESH_GEOMETRY_NORMAL); + _new_type_buttons[mode]->set_active(); + } + + /* New gradient on fill or stroke*/ + { + Gtk::RadioToolButton::Group new_fillstroke_group; + + auto fill_button = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("fill"))); + fill_button->set_tooltip_text(_("Create gradient in the fill")); + fill_button->set_icon_name(INKSCAPE_ICON("object-fill")); + _new_fillstroke_buttons.push_back(fill_button); + + auto stroke_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("stroke"))); + stroke_btn->set_tooltip_text(_("Create gradient in the stroke")); + stroke_btn->set_icon_name(INKSCAPE_ICON("object-stroke")); + _new_fillstroke_buttons.push_back(stroke_btn); + + int btn_idx = 0; + for(auto btn : _new_fillstroke_buttons) { + add(*btn); + btn->set_sensitive(true); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &MeshToolbar::new_fillstroke_changed), btn_idx++)); + } + + gint mode = prefs->getInt("/tools/mesh/newfillorstroke"); + _new_fillstroke_buttons[mode]->set_active(); + } + + /* Number of mesh rows */ + { + std::vector<double> values = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + auto rows_val = prefs->getDouble("/tools/mesh/mesh_rows", 1); + _row_adj = Gtk::Adjustment::create(rows_val, 1, 20, 1, 1); + auto row_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("mesh-row", _("Rows:"), _row_adj, 1.0, 0)); + row_item->set_tooltip_text(_("Number of rows in new mesh")); + row_item->set_custom_numeric_menu_data(values); + row_item->set_focus_widget(desktop->canvas); + _row_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeshToolbar::row_changed)); + add(*row_item); + row_item->set_sensitive(true); + } + + /* Number of mesh columns */ + { + std::vector<double> values = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + auto col_val = prefs->getDouble("/tools/mesh/mesh_cols", 1); + _col_adj = Gtk::Adjustment::create(col_val, 1, 20, 1, 1); + auto col_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("mesh-col", _("Columns:"), _col_adj, 1.0, 0)); + col_item->set_tooltip_text(_("Number of columns in new mesh")); + col_item->set_custom_numeric_menu_data(values); + col_item->set_focus_widget(desktop->canvas); + _col_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeshToolbar::col_changed)); + add(*col_item); + col_item->set_sensitive(true); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + // TODO: These were disabled in the UI file. Either activate or delete +#if 0 + /* Edit fill mesh */ + { + _edit_fill_item = add_toggle_button(_("Edit Fill"), + _("Edit fill mesh")); + _edit_fill_item->set_icon_name(INKSCAPE_ICON("object-fill")); + _edit_fill_pusher.reset(new UI::SimplePrefPusher(_edit_fill_item, "/tools/mesh/edit_fill")); + _edit_fill_item->signal_toggled().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_fill_stroke)); + } + + /* Edit stroke mesh */ + { + _edit_stroke_item = add_toggle_button(_("Edit Stroke"), + _("Edit stroke mesh")); + _edit_stroke_item->set_icon_name(INKSCAPE_ICON("object-stroke")); + _edit_stroke_pusher.reset(new UI::SimplePrefPusher(_edit_stroke_item, "/tools/mesh/edit_stroke")); + _edit_stroke_item->signal_toggled().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_fill_stroke)); + } + + /* Show/hide side and tensor handles */ + { + auto show_handles_item = add_toggle_button(_("Show Handles"), + _("Show handles")); + show_handles_item->set_icon_name(INKSCAPE_ICON("show-node-handles")); + _show_handles_pusher.reset(new UI::SimplePrefPusher(show_handles_item, "/tools/mesh/show_handles")); + show_handles_item->signal_toggled().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_handles)); + } +#endif + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &MeshToolbar::watch_ec)); + + { + auto btn = Gtk::manage(new Gtk::ToolButton(_("Toggle Sides"))); + btn->set_tooltip_text(_("Toggle selected sides between Beziers and lines.")); + btn->set_icon_name(INKSCAPE_ICON("node-segment-line")); + btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_sides)); + add(*btn); + } + + { + auto btn = Gtk::manage(new Gtk::ToolButton(_("Make elliptical"))); + btn->set_tooltip_text(_("Make selected sides elliptical by changing length of handles. Works best if handles already approximate ellipse.")); + btn->set_icon_name(INKSCAPE_ICON("node-segment-curve")); + btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::make_elliptical)); + add(*btn); + } + + { + auto btn = Gtk::manage(new Gtk::ToolButton(_("Pick colors:"))); + btn->set_tooltip_text(_("Pick colors for selected corner nodes from underneath mesh.")); + btn->set_icon_name(INKSCAPE_ICON("color-picker")); + btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::pick_colors)); + add(*btn); + } + + + { + auto btn = Gtk::manage(new Gtk::ToolButton(_("Scale mesh to bounding box:"))); + btn->set_tooltip_text(_("Scale mesh to fit inside bounding box.")); + btn->set_icon_name(INKSCAPE_ICON("mesh-gradient-fit")); + btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::fit_mesh)); + add(*btn); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Warning */ + { + auto btn = Gtk::manage(new Gtk::ToolButton(_("WARNING: Mesh SVG Syntax Subject to Change"))); + btn->set_tooltip_text(_("WARNING: Mesh SVG Syntax Subject to Change")); + btn->set_icon_name(INKSCAPE_ICON("dialog-warning")); + add(*btn); + btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::warning_popup)); + btn->set_sensitive(true); + } + + /* Type */ + { + UI::Widget::ComboToolItemColumns columns; + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = C_("Type", "Coons"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Bicubic"); + row[columns.col_sensitive] = true; + + _select_type_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Smoothing"), + // TRANSLATORS: Type of Smoothing. See https://en.wikipedia.org/wiki/Coons_patch + _("Coons: no smoothing. Bicubic: smoothing across patch boundaries."), + "Not Used", store)); + _select_type_item->use_group_label(true); + + _select_type_item->set_active(0); + + _select_type_item->signal_changed().connect(sigc::mem_fun(*this, &MeshToolbar::type_changed)); + add(*_select_type_item); + } + + show_all(); +} + +/** + * Mesh auxiliary toolbar construction and setup. + * Don't forget to add to XML in widgets/toolbox.cpp! + * + */ +GtkWidget * +MeshToolbar::create(SPDesktop * desktop) +{ + auto toolbar = new MeshToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +MeshToolbar::new_geometry_changed(int mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/mesh/mesh_geometry", mode); +} + +void +MeshToolbar::new_fillstroke_changed(int mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/mesh/newfillorstroke", mode); +} + +void +MeshToolbar::row_changed() +{ + if (blocked) { + return; + } + + blocked = TRUE; + + int rows = _row_adj->get_value(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + prefs->setInt("/tools/mesh/mesh_rows", rows); + + blocked = FALSE; +} + +void +MeshToolbar::col_changed() +{ + if (blocked) { + return; + } + + blocked = TRUE; + + int cols = _col_adj->get_value(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + prefs->setInt("/tools/mesh/mesh_cols", cols); + + blocked = FALSE; +} + +void +MeshToolbar::toggle_fill_stroke() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("tools/mesh/edit_fill", _edit_fill_item->get_active()); + prefs->setBool("tools/mesh/edit_stroke", _edit_stroke_item->get_active()); + + MeshTool *mt = get_mesh_tool(); + if (mt) { + GrDrag *drag = mt->get_drag(); + drag->updateDraggers(); + drag->updateLines(); + drag->updateLevels(); + selection_changed(nullptr); // Need to update Type widget + } +} + +void +MeshToolbar::toggle_handles() +{ + MeshTool *mt = get_mesh_tool(); + if (mt) { + GrDrag *drag = mt->get_drag(); + drag->refreshDraggers(); + } +} + +void +MeshToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (SP_IS_MESH_CONTEXT(ec)) { + // connect to selection modified and changed signals + Inkscape::Selection *selection = desktop->getSelection(); + SPDocument *document = desktop->getDocument(); + + c_selection_changed = selection->connectChanged(sigc::mem_fun(*this, &MeshToolbar::selection_changed)); + c_selection_modified = selection->connectModified(sigc::mem_fun(*this, &MeshToolbar::selection_modified)); + c_subselection_changed = desktop->connectToolSubselectionChanged(sigc::mem_fun(*this, &MeshToolbar::drag_selection_changed)); + + c_defs_release = document->getDefs()->connectRelease(sigc::mem_fun(*this, &MeshToolbar::defs_release)); + c_defs_modified = document->getDefs()->connectModified(sigc::mem_fun(*this, &MeshToolbar::defs_modified)); + selection_changed(selection); + } else { + if (c_selection_changed) + c_selection_changed.disconnect(); + if (c_selection_modified) + c_selection_modified.disconnect(); + if (c_subselection_changed) + c_subselection_changed.disconnect(); + if (c_defs_release) + c_defs_release.disconnect(); + if (c_defs_modified) + c_defs_modified.disconnect(); + } +} + +void +MeshToolbar::selection_modified(Inkscape::Selection *selection, guint /*flags*/) +{ + selection_changed(selection); +} + +void +MeshToolbar::drag_selection_changed(gpointer /*dragger*/) +{ + selection_changed(nullptr); +} + +void +MeshToolbar::defs_release(SPObject * /*defs*/) +{ + selection_changed(nullptr); +} + +void +MeshToolbar::defs_modified(SPObject * /*defs*/, guint /*flags*/) +{ + selection_changed(nullptr); +} + +/* + * Core function, setup all the widgets whenever something changes on the desktop + */ +void +MeshToolbar::selection_changed(Inkscape::Selection * /* selection */) +{ + // std::cout << "ms_tb_selection_changed" << std::endl; + + if (blocked) + return; + + if (!_desktop) { + return; + } + + Inkscape::Selection *selection = _desktop->getSelection(); // take from desktop, not from args + if (selection) { + // ToolBase *ev = sp_desktop_event_context(desktop); + // GrDrag *drag = NULL; + // if (ev) { + // drag = ev->get_drag(); + // // Hide/show handles? + // } + + SPMeshGradient *ms_selected = nullptr; + SPMeshType ms_type = SP_MESH_TYPE_COONS; + bool ms_selected_multi = false; + bool ms_type_multi = false; + ms_read_selection( selection, ms_selected, ms_selected_multi, ms_type, ms_type_multi ); + // std::cout << " type: " << ms_type << std::endl; + + if (_select_type_item) { + _select_type_item->set_sensitive(!ms_type_multi); + blocked = TRUE; + _select_type_item->set_active(ms_type); + blocked = FALSE; + } + } +} + +void +MeshToolbar::warning_popup() +{ + char *msg = _("Mesh gradients are part of SVG 2:\n" + "* Syntax may change.\n" + "* Web browser implementation is not guaranteed.\n" + "\n" + "For web: convert to bitmap (Edit->Make bitmap copy).\n" + "For print: export to PDF."); + Gtk::MessageDialog dialog(msg, false, Gtk::MESSAGE_WARNING, + Gtk::BUTTONS_OK, true); + dialog.run(); +} + +/** + * Sets mesh type: Coons, Bicubic + */ +void +MeshToolbar::type_changed(int mode) +{ + if (blocked) { + return; + } + + Inkscape::Selection *selection = _desktop->getSelection(); + std::vector<SPMeshGradient *> meshes = ms_get_dt_selected_gradients(selection); + + SPMeshType type = (SPMeshType) mode; + for (auto & meshe : meshes) { + meshe->type = type; + meshe->type_set = true; + meshe->updateRepr(); + } + if (!meshes.empty() ) { + DocumentUndo::done(_desktop->getDocument(), _("Set mesh type"), INKSCAPE_ICON("mesh-gradient")); + } +} + +void +MeshToolbar::toggle_sides() +{ + if (MeshTool *mt = get_mesh_tool()) { + mt->corner_operation(MG_CORNER_SIDE_TOGGLE); + } +} + +void +MeshToolbar::make_elliptical() +{ + if (MeshTool *mt = get_mesh_tool()) { + mt->corner_operation(MG_CORNER_SIDE_ARC); + } +} + +void +MeshToolbar::pick_colors() +{ + if (MeshTool *mt = get_mesh_tool()) { + mt->corner_operation(MG_CORNER_COLOR_PICK); + } +} + +void +MeshToolbar::fit_mesh() +{ + if (MeshTool *mt = get_mesh_tool()) { + mt->fit_mesh_in_bbox(); + } +} + + +} +} +} + +/* + 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/toolbar/mesh-toolbar.h b/src/ui/toolbar/mesh-toolbar.h new file mode 100644 index 0000000..2df4411 --- /dev/null +++ b/src/ui/toolbar/mesh-toolbar.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_MESH_TOOLBAR_H +#define SEEN_MESH_TOOLBAR_H + +/* + * Mesh aux toolbar + * + * Authors: + * bulia byak <bulia@dr.com> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2012 authors + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; +class SPObject; + +namespace Gtk { +class RadioToolButton; +} + +namespace Inkscape { +class Selection; + +namespace UI { +class SimplePrefPusher; + +namespace Tools { +class ToolBase; +} + +namespace Widget { +class ComboToolItem; +class SpinButtonToolItem; +} + +namespace Toolbar { +class MeshToolbar : public Toolbar { +private: + std::vector<Gtk::RadioToolButton *> _new_type_buttons; + std::vector<Gtk::RadioToolButton *> _new_fillstroke_buttons; + UI::Widget::ComboToolItem *_select_type_item; + + Gtk::ToggleToolButton *_edit_fill_item; + Gtk::ToggleToolButton *_edit_stroke_item; + + Glib::RefPtr<Gtk::Adjustment> _row_adj; + Glib::RefPtr<Gtk::Adjustment> _col_adj; + + std::unique_ptr<UI::SimplePrefPusher> _edit_fill_pusher; + std::unique_ptr<UI::SimplePrefPusher> _edit_stroke_pusher; + std::unique_ptr<UI::SimplePrefPusher> _show_handles_pusher; + + sigc::connection c_selection_changed; + sigc::connection c_selection_modified; + sigc::connection c_subselection_changed; + sigc::connection c_defs_release; + sigc::connection c_defs_modified; + + void new_geometry_changed(int mode); + void new_fillstroke_changed(int mode); + void row_changed(); + void col_changed(); + void toggle_fill_stroke(); + void selection_changed(Inkscape::Selection *selection); + void toggle_handles(); + void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_modified(Inkscape::Selection *selection, guint flags); + void drag_selection_changed(gpointer dragger); + void defs_release(SPObject *defs); + void defs_modified(SPObject *defs, guint flags); + void warning_popup(); + void type_changed(int mode); + void toggle_sides(); + void make_elliptical(); + void pick_colors(); + void fit_mesh(); + +protected: + MeshToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_MESH_TOOLBAR_H */ diff --git a/src/ui/toolbar/node-toolbar.cpp b/src/ui/toolbar/node-toolbar.cpp new file mode 100644 index 0000000..d3be665 --- /dev/null +++ b/src/ui/toolbar/node-toolbar.cpp @@ -0,0 +1,663 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Node aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "node-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/adjustment.h> +#include <gtkmm/image.h> +#include <gtkmm/menutoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "inkscape.h" +#include "selection-chemistry.h" + +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/simple-pref-pusher.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +#include "widgets/widget-sizes.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; +using Inkscape::UI::Tools::NodeTool; + +/** Temporary hack: Returns the node tool in the active desktop. + * Will go away during tool refactoring. */ +static NodeTool *get_node_tool() +{ + NodeTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (INK_IS_NODE_TOOL(ec)) { + tool = static_cast<NodeTool*>(ec); + } + } + return tool; +} + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +NodeToolbar::NodeToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)), + _freeze(false) +{ + auto prefs = Inkscape::Preferences::get(); + + Unit doc_units = *desktop->getNamedView()->display_units; + _tracker->setActiveUnit(&doc_units); + + { + auto insert_node_item = Gtk::manage(new Gtk::MenuToolButton()); + insert_node_item->set_icon_name(INKSCAPE_ICON("node-add")); + insert_node_item->set_label(_("Insert node")); + insert_node_item->set_tooltip_text(_("Insert new nodes into selected segments")); + insert_node_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add)); + + auto insert_node_menu = Gtk::manage(new Gtk::Menu()); + + { + // TODO: Consider moving back to icons in menu? + //auto insert_min_x_icon = Gtk::manage(new Gtk::Image()); + //insert_min_x_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_min_x"), Gtk::ICON_SIZE_MENU); + //auto insert_min_x_item = Gtk::manage(new Gtk::MenuItem(*insert_min_x_icon)); + auto insert_min_x_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at min X"))); + insert_min_x_item->set_tooltip_text(_("Insert new nodes at min X into selected segments")); + insert_min_x_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_min_x)); + insert_node_menu->append(*insert_min_x_item); + } + { + //auto insert_max_x_icon = Gtk::manage(new Gtk::Image()); + //insert_max_x_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_max_x"), Gtk::ICON_SIZE_MENU); + //auto insert_max_x_item = Gtk::manage(new Gtk::MenuItem(*insert_max_x_icon)); + auto insert_max_x_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at max X"))); + insert_max_x_item->set_tooltip_text(_("Insert new nodes at max X into selected segments")); + insert_max_x_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_max_x)); + insert_node_menu->append(*insert_max_x_item); + } + { + //auto insert_min_y_icon = Gtk::manage(new Gtk::Image()); + //insert_min_y_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_min_y"), Gtk::ICON_SIZE_MENU); + //auto insert_min_y_item = Gtk::manage(new Gtk::MenuItem(*insert_min_y_icon)); + auto insert_min_y_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at min Y"))); + insert_min_y_item->set_tooltip_text(_("Insert new nodes at min Y into selected segments")); + insert_min_y_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_min_y)); + insert_node_menu->append(*insert_min_y_item); + } + { + //auto insert_max_y_icon = Gtk::manage(new Gtk::Image()); + //insert_max_y_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_max_y"), Gtk::ICON_SIZE_MENU); + //auto insert_max_y_item = Gtk::manage(new Gtk::MenuItem(*insert_max_y_icon)); + auto insert_max_y_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at max Y"))); + insert_max_y_item->set_tooltip_text(_("Insert new nodes at max Y into selected segments")); + insert_max_y_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_max_y)); + insert_node_menu->append(*insert_max_y_item); + } + + insert_node_menu->show_all(); + insert_node_item->set_menu(*insert_node_menu); + add(*insert_node_item); + } + + { + auto delete_item = Gtk::manage(new Gtk::ToolButton(_("Delete node"))); + delete_item->set_tooltip_text(_("Delete selected nodes")); + delete_item->set_icon_name(INKSCAPE_ICON("node-delete")); + delete_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_delete)); + add(*delete_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto join_item = Gtk::manage(new Gtk::ToolButton(_("Join nodes"))); + join_item->set_tooltip_text(_("Join selected nodes")); + join_item->set_icon_name(INKSCAPE_ICON("node-join")); + join_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_join)); + add(*join_item); + } + + { + auto break_item = Gtk::manage(new Gtk::ToolButton(_("Break nodes"))); + break_item->set_tooltip_text(_("Break path at selected nodes")); + break_item->set_icon_name(INKSCAPE_ICON("node-break")); + break_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_break)); + add(*break_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto join_segment_item = Gtk::manage(new Gtk::ToolButton(_("Join with segment"))); + join_segment_item->set_tooltip_text(_("Join selected endnodes with a new segment")); + join_segment_item->set_icon_name(INKSCAPE_ICON("node-join-segment")); + join_segment_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_join_segment)); + add(*join_segment_item); + } + + { + auto delete_segment_item = Gtk::manage(new Gtk::ToolButton(_("Delete segment"))); + delete_segment_item->set_tooltip_text(_("Delete segment between two non-endpoint nodes")); + delete_segment_item->set_icon_name(INKSCAPE_ICON("node-delete-segment")); + delete_segment_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_delete_segment)); + add(*delete_segment_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto cusp_item = Gtk::manage(new Gtk::ToolButton(_("Node Cusp"))); + cusp_item->set_tooltip_text(_("Make selected nodes corner")); + cusp_item->set_icon_name(INKSCAPE_ICON("node-type-cusp")); + cusp_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_cusp)); + add(*cusp_item); + } + + { + auto smooth_item = Gtk::manage(new Gtk::ToolButton(_("Node Smooth"))); + smooth_item->set_tooltip_text(_("Make selected nodes smooth")); + smooth_item->set_icon_name(INKSCAPE_ICON("node-type-smooth")); + smooth_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_smooth)); + add(*smooth_item); + } + + { + auto symmetric_item = Gtk::manage(new Gtk::ToolButton(_("Node Symmetric"))); + symmetric_item->set_tooltip_text(_("Make selected nodes symmetric")); + symmetric_item->set_icon_name(INKSCAPE_ICON("node-type-symmetric")); + symmetric_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_symmetrical)); + add(*symmetric_item); + } + + { + auto auto_item = Gtk::manage(new Gtk::ToolButton(_("Node Auto"))); + auto_item->set_tooltip_text(_("Make selected nodes auto-smooth")); + auto_item->set_icon_name(INKSCAPE_ICON("node-type-auto-smooth")); + auto_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_auto)); + add(*auto_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto line_item = Gtk::manage(new Gtk::ToolButton(_("Node Line"))); + line_item->set_tooltip_text(_("Make selected segments lines")); + line_item->set_icon_name(INKSCAPE_ICON("node-segment-line")); + line_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_toline)); + add(*line_item); + } + + { + auto curve_item = Gtk::manage(new Gtk::ToolButton(_("Node Curve"))); + curve_item->set_tooltip_text(_("Make selected segments curves")); + curve_item->set_icon_name(INKSCAPE_ICON("node-segment-curve")); + curve_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_tocurve)); + add(*curve_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto object_to_path_item = Gtk::manage(new Gtk::ToolButton(_("_Object to Path"))); + object_to_path_item->set_tooltip_text(_("Convert selected object to path")); + object_to_path_item->set_icon_name(INKSCAPE_ICON("object-to-path")); + // Must use C API until GTK4. + gtk_actionable_set_action_name(GTK_ACTIONABLE(object_to_path_item->gobj()), "app.object-to-path"); + add(*object_to_path_item); + } + + { + auto stroke_to_path_item = Gtk::manage(new Gtk::ToolButton(_("_Stroke to Path"))); + stroke_to_path_item->set_tooltip_text(_("Convert selected object's stroke to paths")); + stroke_to_path_item->set_icon_name(INKSCAPE_ICON("stroke-to-path")); + // Must use C API until GTK4. + gtk_actionable_set_action_name(GTK_ACTIONABLE(stroke_to_path_item->gobj()), "app.object-stroke-to-path"); + add(*stroke_to_path_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* X coord of selected node(s) */ + { + std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + auto nodes_x_val = prefs->getDouble("/tools/nodes/Xcoord", 0); + _nodes_x_adj = Gtk::Adjustment::create(nodes_x_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _nodes_x_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("node-x", _("X:"), _nodes_x_adj)); + _nodes_x_item->set_tooltip_text(_("X coordinate of selected node(s)")); + _nodes_x_item->set_custom_numeric_menu_data(values); + _tracker->addAdjustment(_nodes_x_adj->gobj()); + _nodes_x_item->get_spin_button()->addUnitTracker(_tracker.get()); + _nodes_x_item->set_focus_widget(desktop->canvas); + _nodes_x_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::value_changed), Geom::X)); + _nodes_x_item->set_sensitive(false); + add(*_nodes_x_item); + } + + /* Y coord of selected node(s) */ + { + std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + auto nodes_y_val = prefs->getDouble("/tools/nodes/Ycoord", 0); + _nodes_y_adj = Gtk::Adjustment::create(nodes_y_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _nodes_y_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("node-y", _("Y:"), _nodes_y_adj)); + _nodes_y_item->set_tooltip_text(_("Y coordinate of selected node(s)")); + _nodes_y_item->set_custom_numeric_menu_data(values); + _tracker->addAdjustment(_nodes_y_adj->gobj()); + _nodes_y_item->get_spin_button()->addUnitTracker(_tracker.get()); + _nodes_y_item->set_focus_widget(desktop->canvas); + _nodes_y_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::value_changed), Geom::Y)); + _nodes_y_item->set_sensitive(false); + add(*_nodes_y_item); + } + + // add the units menu + { + auto unit_menu = _tracker->create_tool_item(_("Units"), ("")); + add(*unit_menu); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + _object_edit_clip_path_item = add_toggle_button(_("Edit clipping paths"), + _("Show clipping path(s) of selected object(s)")); + _object_edit_clip_path_item->set_icon_name(INKSCAPE_ICON("path-clip-edit")); + _pusher_edit_clipping_paths.reset(new SimplePrefPusher(_object_edit_clip_path_item, "/tools/nodes/edit_clipping_paths")); + _object_edit_clip_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), + _object_edit_clip_path_item, + "/tools/nodes/edit_clipping_paths")); + } + + { + _object_edit_mask_path_item = add_toggle_button(_("Edit masks"), + _("Show mask(s) of selected object(s)")); + _object_edit_mask_path_item->set_icon_name(INKSCAPE_ICON("path-mask-edit")); + _pusher_edit_masks.reset(new SimplePrefPusher(_object_edit_mask_path_item, "/tools/nodes/edit_masks")); + _object_edit_mask_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), + _object_edit_mask_path_item, + "/tools/nodes/edit_masks")); + } + + { + _nodes_lpeedit_item = Gtk::manage(new Gtk::ToolButton(N_("Next path effect parameter"))); + _nodes_lpeedit_item->set_tooltip_text(N_("Show next editable path effect parameter")); + _nodes_lpeedit_item->set_icon_name(INKSCAPE_ICON("path-effect-parameter-next")); + // Must use C API until GTK4. + gtk_actionable_set_action_name(GTK_ACTIONABLE(_nodes_lpeedit_item->gobj()), "win.path-effect-parameter-next"); + add(*_nodes_lpeedit_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + _show_transform_handles_item = add_toggle_button(_("Show Transform Handles"), + _("Show transformation handles for selected nodes")); + _show_transform_handles_item->set_icon_name(INKSCAPE_ICON("node-transform")); + _pusher_show_transform_handles.reset(new UI::SimplePrefPusher(_show_transform_handles_item, "/tools/nodes/show_transform_handles")); + _show_transform_handles_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), + _show_transform_handles_item, + "/tools/nodes/show_transform_handles")); + } + + { + _show_handles_item = add_toggle_button(_("Show Handles"), + _("Show Bezier handles of selected nodes")); + _show_handles_item->set_icon_name(INKSCAPE_ICON("show-node-handles")); + _pusher_show_handles.reset(new UI::SimplePrefPusher(_show_handles_item, "/tools/nodes/show_handles")); + _show_handles_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), + _show_handles_item, + "/tools/nodes/show_handles")); + } + + { + _show_helper_path_item = add_toggle_button(_("Show Outline"), + _("Show path outline (without path effects)")); + _show_helper_path_item->set_icon_name(INKSCAPE_ICON("show-path-outline")); + _pusher_show_outline.reset(new UI::SimplePrefPusher(_show_helper_path_item, "/tools/nodes/show_outline")); + _show_helper_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), + _show_helper_path_item, + "/tools/nodes/show_outline")); + } + + sel_changed(desktop->getSelection()); + desktop->connectEventContextChanged(sigc::mem_fun(*this, &NodeToolbar::watch_ec)); + + show_all(); +} + +GtkWidget * +NodeToolbar::create(SPDesktop *desktop) +{ + auto holder = new NodeToolbar(desktop); + return GTK_WIDGET(holder->gobj()); +} // NodeToolbar::prep() + +void +NodeToolbar::value_changed(Geom::Dim2 d) +{ + auto adj = (d == Geom::X) ? _nodes_x_adj : _nodes_y_adj; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (!_tracker) { + return; + } + + Unit const *unit = _tracker->getActiveUnit(); + + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + prefs->setDouble(Glib::ustring("/tools/nodes/") + (d == Geom::X ? "x" : "y"), + Quantity::convert(adj->get_value(), unit, "px")); + } + + // quit if run by the attr_changed listener + if (_freeze || _tracker->isUpdating()) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + NodeTool *nt = get_node_tool(); + if (nt && !nt->_selected_nodes->empty()) { + double val = Quantity::convert(adj->get_value(), unit, "px"); + double oldval = nt->_selected_nodes->pointwiseBounds()->midpoint()[d]; + Geom::Point delta(0,0); + delta[d] = val - oldval; + nt->_multipath->move(delta); + } + + _freeze = false; +} + +void +NodeToolbar::sel_changed(Inkscape::Selection *selection) +{ + SPItem *item = selection->singleItem(); + if (item && SP_IS_LPE_ITEM(item)) { + if (SP_LPE_ITEM(item)->hasPathEffect()) { + _nodes_lpeedit_item->set_sensitive(true); + } else { + _nodes_lpeedit_item->set_sensitive(false); + } + } else { + _nodes_lpeedit_item->set_sensitive(false); + } +} + +void +NodeToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (INK_IS_NODE_TOOL(ec)) { + // watch selection + c_selection_changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &NodeToolbar::sel_changed)); + c_selection_modified = desktop->getSelection()->connectModified(sigc::mem_fun(*this, &NodeToolbar::sel_modified)); + c_subselection_changed = desktop->connect_control_point_selected([=](void* sender, Inkscape::UI::ControlPointSelection* selection) { + coord_changed(selection); + }); + + sel_changed(desktop->getSelection()); + } else { + if (c_selection_changed) + c_selection_changed.disconnect(); + if (c_selection_modified) + c_selection_modified.disconnect(); + if (c_subselection_changed) + c_subselection_changed.disconnect(); + } +} + +void +NodeToolbar::sel_modified(Inkscape::Selection *selection, guint /*flags*/) +{ + sel_changed(selection); +} + +/* is called when the node selection is modified */ +void +NodeToolbar::coord_changed(Inkscape::UI::ControlPointSelection* selected_nodes) // gpointer /*shape_editor*/) +{ + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + if (!_tracker) { + return; + } + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + if (!selected_nodes || selected_nodes->empty()) { + // no path selected + _nodes_x_item->set_sensitive(false); + _nodes_y_item->set_sensitive(false); + } else { + _nodes_x_item->set_sensitive(true); + _nodes_y_item->set_sensitive(true); + Geom::Coord oldx = Quantity::convert(_nodes_x_adj->get_value(), unit, "px"); + Geom::Coord oldy = Quantity::convert(_nodes_y_adj->get_value(), unit, "px"); + Geom::Point mid = selected_nodes->pointwiseBounds()->midpoint(); + + if (oldx != mid[Geom::X]) { + _nodes_x_adj->set_value(Quantity::convert(mid[Geom::X], "px", unit)); + } + if (oldy != mid[Geom::Y]) { + _nodes_y_adj->set_value(Quantity::convert(mid[Geom::Y], "px", unit)); + } + } + + _freeze = false; +} + +void +NodeToolbar::edit_add() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->insertNodes(); + } +} + +void +NodeToolbar::edit_add_min_x() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MIN_X); + } +} + +void +NodeToolbar::edit_add_max_x() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MAX_X); + } +} + +void +NodeToolbar::edit_add_min_y() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MIN_Y); + } +} + +void +NodeToolbar::edit_add_max_y() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MAX_Y); + } +} + +void +NodeToolbar::edit_delete() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + nt->_multipath->deleteNodes(prefs->getBool("/tools/nodes/delete_preserves_shape", true)); + } +} + +void +NodeToolbar::edit_join() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->joinNodes(); + } +} + +void +NodeToolbar::edit_break() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->breakNodes(); + } +} + +void +NodeToolbar::edit_delete_segment() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->deleteSegments(); + } +} + +void +NodeToolbar::edit_join_segment() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->joinSegments(); + } +} + +void +NodeToolbar::edit_cusp() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setNodeType(Inkscape::UI::NODE_CUSP); + } +} + +void +NodeToolbar::edit_smooth() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setNodeType(Inkscape::UI::NODE_SMOOTH); + } +} + +void +NodeToolbar::edit_symmetrical() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setNodeType(Inkscape::UI::NODE_SYMMETRIC); + } +} + +void +NodeToolbar::edit_auto() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setNodeType(Inkscape::UI::NODE_AUTO); + } +} + +void +NodeToolbar::edit_toline() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setSegmentType(Inkscape::UI::SEGMENT_STRAIGHT); + } +} + +void +NodeToolbar::edit_tocurve() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setSegmentType(Inkscape::UI::SEGMENT_CUBIC_BEZIER); + } +} + +void +NodeToolbar::on_pref_toggled(Gtk::ToggleToolButton *item, + const Glib::ustring& path) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(path, item->get_active()); +} + +} +} +} + +/* + 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/toolbar/node-toolbar.h b/src/ui/toolbar/node-toolbar.h new file mode 100644 index 0000000..9723922 --- /dev/null +++ b/src/ui/toolbar/node-toolbar.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NODE_TOOLBAR_H +#define SEEN_NODE_TOOLBAR_H + +/** + * @file + * Node aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" +#include "2geom/coord.h" + +class SPDesktop; + +namespace Inkscape { +class Selection; + +namespace UI { +class SimplePrefPusher; +class ControlPointSelection; + +namespace Tools { +class ToolBase; +} + +namespace Widget { +class SpinButtonToolItem; +class UnitTracker; +} + +namespace Toolbar { +class NodeToolbar : public Toolbar { +private: + std::unique_ptr<UI::Widget::UnitTracker> _tracker; + + std::unique_ptr<UI::SimplePrefPusher> _pusher_show_transform_handles; + std::unique_ptr<UI::SimplePrefPusher> _pusher_show_handles; + std::unique_ptr<UI::SimplePrefPusher> _pusher_show_outline; + std::unique_ptr<UI::SimplePrefPusher> _pusher_edit_clipping_paths; + std::unique_ptr<UI::SimplePrefPusher> _pusher_edit_masks; + + Gtk::ToggleToolButton *_object_edit_clip_path_item; + Gtk::ToggleToolButton *_object_edit_mask_path_item; + Gtk::ToggleToolButton *_show_transform_handles_item; + Gtk::ToggleToolButton *_show_handles_item; + Gtk::ToggleToolButton *_show_helper_path_item; + + Gtk::ToolButton *_nodes_lpeedit_item; + + UI::Widget::SpinButtonToolItem *_nodes_x_item; + UI::Widget::SpinButtonToolItem *_nodes_y_item; + + Glib::RefPtr<Gtk::Adjustment> _nodes_x_adj; + Glib::RefPtr<Gtk::Adjustment> _nodes_y_adj; + + bool _freeze; + + sigc::connection c_selection_changed; + sigc::connection c_selection_modified; + sigc::connection c_subselection_changed; + + void value_changed(Geom::Dim2 d); + void sel_changed(Inkscape::Selection *selection); + void sel_modified(Inkscape::Selection *selection, guint /*flags*/); + void coord_changed(Inkscape::UI::ControlPointSelection* selected_nodes); + void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void edit_add(); + void edit_add_min_x(); + void edit_add_max_x(); + void edit_add_min_y(); + void edit_add_max_y(); + void edit_delete(); + void edit_join(); + void edit_break(); + void edit_join_segment(); + void edit_delete_segment(); + void edit_cusp(); + void edit_smooth(); + void edit_symmetrical(); + void edit_auto(); + void edit_toline(); + void edit_tocurve(); + void on_pref_toggled(Gtk::ToggleToolButton *item, + const Glib::ustring& path); + +protected: + NodeToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; +} +} +} +#endif /* !SEEN_SELECT_TOOLBAR_H */ diff --git a/src/ui/toolbar/page-toolbar.cpp b/src/ui/toolbar/page-toolbar.cpp new file mode 100644 index 0000000..d845872 --- /dev/null +++ b/src/ui/toolbar/page-toolbar.cpp @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Page aux toolbar: Temp until we convert all toolbars to ui files with Gio::Actions. + */ +/* Authors: + * Martin Owens <doctormo@geek-2.com> + + * Copyright (C) 2021 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "page-toolbar.h" + +#include <glibmm/i18n.h> +#include <gtkmm.h> +#include <regex> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "io/resource.h" +#include "object/sp-namedview.h" +#include "object/sp-page.h" +#include "ui/icon-names.h" +#include "ui/tools/pages-tool.h" +#include "util/paper.h" +#include "util/units.h" + +using Inkscape::IO::Resource::UIS; + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +PageToolbar::PageToolbar(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &builder, SPDesktop *desktop) + : Gtk::Toolbar(cobject) + , _desktop(desktop) + , combo_page_sizes(nullptr) + , text_page_label(nullptr) +{ + builder->get_widget("page_sizes", combo_page_sizes); + builder->get_widget("page_label", text_page_label); + builder->get_widget("page_pos", label_page_pos); + builder->get_widget("page_backward", btn_page_backward); + builder->get_widget("page_foreward", btn_page_foreward); + builder->get_widget("page_delete", btn_page_delete); + builder->get_widget("page_move_objects", btn_move_toggle); + builder->get_widget("sep1", sep1); + + if (text_page_label) { + text_page_label->signal_changed().connect(sigc::mem_fun(*this, &PageToolbar::labelEdited)); + } + + if (combo_page_sizes) { + combo_page_sizes->signal_changed().connect(sigc::mem_fun(*this, &PageToolbar::sizeChoose)); + entry_page_sizes = dynamic_cast<Gtk::Entry *>(combo_page_sizes->get_child()); + if (entry_page_sizes) { + entry_page_sizes->set_placeholder_text(_("ex.: 100x100cm")); + entry_page_sizes->set_tooltip_text(_("Type in width & height of a page. (ex.: 15x10cm, 10in x 100mm)\n" + "or choose preset from dropdown.")); + entry_page_sizes->signal_activate().connect(sigc::mem_fun(*this, &PageToolbar::sizeChanged)); + entry_page_sizes->signal_icon_press().connect([=](Gtk::EntryIconPosition, const GdkEventButton*){ + _document->getPageManager().changeOrientation(); + DocumentUndo::maybeDone(_document, "page-resize", _("Resize Page"), INKSCAPE_ICON("tool-pages")); + setSizeText(); + }); + entry_page_sizes->signal_focus_in_event().connect([=](GdkEventFocus* focus){ + entry_page_sizes->set_text(""); + return false; + }); + } + auto& page_sizes = Inkscape::PaperSize::getPageSizes(); + for (int i = 0; i < page_sizes.size(); i++) { + combo_page_sizes->append(std::to_string(i), page_sizes[i].getDescription(false)); + } + } + + // Watch for when the tool changes + _ec_connection = _desktop->connectEventContextChanged(sigc::mem_fun(*this, &PageToolbar::toolChanged)); + _doc_connection = _desktop->connectDocumentReplaced([=](SPDesktop *desktop, SPDocument *doc) { + if (doc) { + toolChanged(desktop, desktop->getEventContext()); + } + }); + + // Constructed by a builder, so we're going to protect the widget from destruction. + this->reference(); + was_referenced = true; +} + +void PageToolbar::on_parent_changed(Gtk::Widget *) +{ + if (was_referenced) { + // Undo the gtkbuilder protection now that we have a parent + this->unreference(); + was_referenced = false; + } +} + +PageToolbar::~PageToolbar() +{ + _ec_connection.disconnect(); + _doc_connection.disconnect(); + toolChanged(nullptr, nullptr); +} + +void PageToolbar::toolChanged(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *ec) +{ + // Disconnect previous page changed signal + _page_selected.disconnect(); + _pages_changed.disconnect(); + _page_modified.disconnect(); + _document = nullptr; + + if (dynamic_cast<Inkscape::UI::Tools::PagesTool *>(ec)) { + // Save the document and page_manager for future use. + if ((_document = desktop->getDocument())) { + auto &page_manager = _document->getPageManager(); + // Connect the page changed signal and indicate changed + _pages_changed = page_manager.connectPagesChanged(sigc::mem_fun(*this, &PageToolbar::pagesChanged)); + _page_selected = page_manager.connectPageSelected(sigc::mem_fun(*this, &PageToolbar::selectionChanged)); + // Update everything now. + pagesChanged(); + } + } +} + +void PageToolbar::labelEdited() +{ + auto text = text_page_label->get_text(); + if (auto page = _document->getPageManager().getSelected()) { + page->setLabel(text.empty() ? nullptr : text.c_str()); + DocumentUndo::maybeDone(_document, "page-relabel", _("Relabel Page"), INKSCAPE_ICON("tool-pages")); + } +} + +void PageToolbar::sizeChoose() +{ + auto &pm = _document->getPageManager(); + try { + auto p_rect = pm.getSelectedPageRect(); + bool landscape = p_rect.width() > p_rect.height(); + + auto page_id = std::stoi(combo_page_sizes->get_active_id()); + auto& page_sizes = Inkscape::PaperSize::getPageSizes(); + if (page_id >= 0 && page_id < page_sizes.size()) { + auto&& ps = page_sizes[page_id]; + // Keep page orientation while selecting size + auto width = ps.unit->convert(ps.size[landscape], "px"); + auto height = ps.unit->convert(ps.size[!landscape], "px"); + pm.resizePage(width, height); + setSizeText(); + DocumentUndo::maybeDone(_document, "page-resize", _("Resize Page"), INKSCAPE_ICON("tool-pages")); + } + } catch (std::invalid_argument const &e) { + // Ignore because user is typing into Entry + } +} + +double PageToolbar::_unit_to_size(std::string number, std::string unit_str, std::string backup) +{ + // We always support comma, even if not in that particular locale. + std::replace(number.begin(), number.end(), ',', '.'); + double value = std::stod(number); + + // Get the best unit, for example 50x40cm means cm for both + if (unit_str.empty() && !backup.empty()) + unit_str = backup; + if (unit_str == "\"") + unit_str = "in"; + + // Output is always in px as it's the most useful. + auto px = Inkscape::Util::unit_table.getUnit("px"); + + // Convert from user entered unit to display unit + if (!unit_str.empty()) + return Inkscape::Util::Quantity::convert(value, unit_str, px); + + // Default unit is the document's display unit + auto unit = _document->getDisplayUnit(); + return Inkscape::Util::Quantity::convert(value, unit, px); +} + +/** + * A manually typed input size, parse out what we can understand from + * the text or ignore it if the text can't be parsed. + * + * Format: 50cm x 40mm + * 20',40" + * 30,4-40.2 + */ +void PageToolbar::sizeChanged() +{ + // Parse the size out of the typed text if possible. + auto text = std::string(combo_page_sizes->get_active_text()); + // This does not support negative values, because pages can not be negatively sized. + static std::string arg = "([0-9]+[\\.,]?[0-9]*|\\.[0-9]+) ?(px|mm|cm|in|\\\")?"; + // We can't support × here since it's UTF8 and this doesn't match + static std::regex re_size("^ *" + arg + " *([ *Xx,\\-]) *" + arg + " *$"); + + std::smatch matches; + if (std::regex_match(text, matches, re_size)) { + double width = _unit_to_size(matches[1], matches[2], matches[5]); + double height = _unit_to_size(matches[4], matches[5], matches[2]); + if (width > 0 && height > 0) { + _document->getPageManager().resizePage(width, height); + } + } + setSizeText(); +} + +/** + * Sets the size of the current page into the entry page size. + */ +void PageToolbar::setSizeText(SPPage *page) +{ + if (!page) + page = _document->getPageManager().getSelected(); + + auto unit = _document->getDisplayUnit(); + double width = _document->getWidth().value(unit); + double height = _document->getHeight().value(unit); + if (page) { + auto px = Inkscape::Util::unit_table.getUnit("px"); + auto rect = page->getDesktopRect(); + width = px->convert(rect.width(), unit); + height = px->convert(rect.height(), unit); + } + // Orientation button + std::string icon = width > height ? "page-landscape" : "page-portrait"; + if (width == height) { + entry_page_sizes->unset_icon(Gtk::ENTRY_ICON_SECONDARY); + } else { + entry_page_sizes->set_icon_from_icon_name(INKSCAPE_ICON(icon), Gtk::ENTRY_ICON_SECONDARY); + } + + if (auto page_size = Inkscape::PaperSize::findPaperSize(width, height, unit)) { + entry_page_sizes->set_text(page_size->getDescription(width > height)); + } else { + entry_page_sizes->set_text(Inkscape::PaperSize::toDescription(_("Custom"), width, height, unit)); + } + // Select text if box is currently in focus. + if (entry_page_sizes->has_focus()) { + entry_page_sizes->select_region(0, -1); + } +} + +void PageToolbar::pagesChanged() +{ + selectionChanged(_document->getPageManager().getSelected()); +} + +void PageToolbar::selectionChanged(SPPage *page) +{ + _page_modified.disconnect(); + auto &page_manager = _document->getPageManager(); + text_page_label->set_tooltip_text(_("Page label")); + + // Set label widget content with page label. + if (page) { + text_page_label->set_sensitive(true); + text_page_label->set_placeholder_text(page->getDefaultLabel()); + + if (auto label = page->label()) { + text_page_label->set_text(label); + } else { + text_page_label->set_text(""); + } + + // TRANSLATORS: "%1" is replaced with the page we are on, and "%2" is the total number of pages. + auto label = Glib::ustring::compose(_("%1/%2"), page->getPagePosition(), page_manager.getPageCount()); + label_page_pos->set_label(label); + + _page_modified = page->connectModified([=](SPObject *obj, unsigned int flags) { + if (auto page = dynamic_cast<SPPage *>(obj)) { + // Make sure we don't 'select' on removal of the page + if (flags & SP_OBJECT_MODIFIED_FLAG) { + selectionChanged(page); + } + } + }); + } else { + text_page_label->set_text(""); + text_page_label->set_sensitive(false); + text_page_label->set_placeholder_text(_("Single Page Document")); + label_page_pos->set_label(_("1/-")); + + _page_modified = _document->connectModified([=](guint) { + selectionChanged(nullptr); + }); + } + if (!page_manager.hasPrevPage() && !page_manager.hasNextPage() && !page) { + sep1->set_visible(false); + label_page_pos->get_parent()->set_visible(false); + btn_page_backward->set_visible(false); + btn_page_foreward->set_visible(false); + btn_page_delete->set_visible(false); + btn_move_toggle->set_sensitive(false); + } else { + // Set the forward and backward button sensitivities + sep1->set_visible(true); + label_page_pos->get_parent()->set_visible(true); + btn_page_backward->set_visible(true); + btn_page_foreward->set_visible(true); + btn_page_backward->set_sensitive(page_manager.hasPrevPage()); + btn_page_foreward->set_sensitive(page_manager.hasNextPage()); + btn_page_delete->set_visible(true); + btn_move_toggle->set_sensitive(true); + } + setSizeText(page); +} + +GtkWidget *PageToolbar::create(SPDesktop *desktop) +{ + Glib::ustring page_toolbar_builder_file = get_filename(UIS, "toolbar-page.ui"); + PageToolbar *toolbar = nullptr; + + try { + auto builder = Gtk::Builder::create_from_file(page_toolbar_builder_file); + builder->get_widget_derived("page-toolbar", toolbar, desktop); + + if (!toolbar) { + std::cerr << "InkscapeWindow: Failed to load page toolbar!" << std::endl; + return nullptr; + } + // Usually we should be packing this widget into a parent before the builder + // is destroyed, but the create method expects a blind widget so this widget + // contains a special keep-alive pattern which can be removed when refactoring. + } catch (const Glib::Error &ex) { + std::cerr << "PageToolbar: " << page_toolbar_builder_file << " file not read! " << ex.what().raw() << std::endl; + } + return GTK_WIDGET(toolbar->gobj()); +} + + +} // namespace Toolbar +} // 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/toolbar/page-toolbar.h b/src/ui/toolbar/page-toolbar.h new file mode 100644 index 0000000..8a2ec08 --- /dev/null +++ b/src/ui/toolbar/page-toolbar.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PAGE_TOOLBAR_H +#define SEEN_PAGE_TOOLBAR_H + +/** + * @file + * Page toolbar + */ +/* Authors: + * Martin Owens <doctormo@geek-2.com> + * + * Copyright (C) 2021 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> + +#include "toolbar.h" + +class SPDesktop; +class SPDocument; +class SPPage; + +namespace Inkscape { +class PaperSize; +namespace UI { +namespace Tools { +class ToolBase; +} +namespace Toolbar { + +class PageToolbar : public Gtk::Toolbar +{ +public: + PageToolbar(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &builder, SPDesktop *desktop); + ~PageToolbar() override; + + static GtkWidget *create(SPDesktop *desktop); + +protected: + void labelEdited(); + void sizeChoose(); + void sizeChanged(); + void setSizeText(SPPage *page = nullptr); + +private: + SPDesktop *_desktop; + SPDocument *_document; + + void toolChanged(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *ec); + void pagesChanged(); + void selectionChanged(SPPage *page); + void on_parent_changed(Gtk::Widget *prev) override; + + sigc::connection _ec_connection; + sigc::connection _doc_connection; + sigc::connection _pages_changed; + sigc::connection _page_selected; + sigc::connection _page_modified; + + bool was_referenced; + Gtk::ComboBoxText *combo_page_sizes; + Gtk::Entry *entry_page_sizes; + Gtk::Entry *text_page_label; + Gtk::Label *label_page_pos; + Gtk::ToolButton *btn_page_backward; + Gtk::ToolButton *btn_page_foreward; + Gtk::ToolButton *btn_page_delete; + Gtk::ToolButton *btn_move_toggle; + Gtk::SeparatorToolItem *sep1; + + double _unit_to_size(std::string number, std::string unit_str, std::string backup); +}; + +} // namespace Toolbar +} // namespace UI +} // namespace Inkscape + +#endif /* !SEEN_PAGE_TOOLBAR_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/toolbar/paintbucket-toolbar.cpp b/src/ui/toolbar/paintbucket-toolbar.cpp new file mode 100644 index 0000000..41e4ed9 --- /dev/null +++ b/src/ui/toolbar/paintbucket-toolbar.cpp @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Paint bucket aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "paintbucket-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" + +#include "ui/icon-names.h" +#include "ui/tools/flood-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +PaintbucketToolbar::PaintbucketToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)) +{ + auto prefs = Inkscape::Preferences::get(); + + // Channel + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + for (auto item: Inkscape::UI::Tools::FloodTool::channel_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label ] = _(item.c_str()); + row[columns.col_sensitive] = true; + } + + _channels_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Fill by"), Glib::ustring(), "Not Used", store)); + _channels_item->use_group_label(true); + + int channels = prefs->getInt("/tools/paintbucket/channels", 0); + _channels_item->set_active(channels); + + _channels_item->signal_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::channels_changed)); + add(*_channels_item); + } + + // Spacing spinbox + { + auto threshold_val = prefs->getDouble("/tools/paintbucket/threshold", 5); + _threshold_adj = Gtk::Adjustment::create(threshold_val, 0, 100.0, 1.0, 10.0); + auto threshold_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:paintbucket-threshold", _("Threshold:"), _threshold_adj, 1, 0)); + threshold_item->set_tooltip_text(_("The maximum allowed difference between the clicked pixel and the neighboring pixels to be counted in the fill")); + threshold_item->set_focus_widget(desktop->canvas); + _threshold_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::threshold_changed)); + add(*threshold_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + // Create the units menu. + Glib::ustring stored_unit = prefs->getString("/tools/paintbucket/offsetunits"); + if (!stored_unit.empty()) { + Unit const *u = unit_table.getUnit(stored_unit); + _tracker->setActiveUnit(u); + } + + // Offset spinbox + { + auto offset_val = prefs->getDouble("/tools/paintbucket/offset", 0); + _offset_adj = Gtk::Adjustment::create(offset_val, -1e4, 1e4, 0.1, 0.5); + auto offset_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:paintbucket-offset", _("Grow/shrink by:"), _offset_adj, 1, 2)); + offset_item->set_tooltip_text(_("The amount to grow (positive) or shrink (negative) the created fill path")); + _tracker->addAdjustment(_offset_adj->gobj()); + offset_item->get_spin_button()->addUnitTracker(_tracker); + offset_item->set_focus_widget(desktop->canvas); + _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::offset_changed)); + add(*offset_item); + } + + { + auto unit_menu = _tracker->create_tool_item(_("Units"), ("")); + add(*unit_menu); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Auto Gap */ + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + for (auto item: Inkscape::UI::Tools::FloodTool::gap_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label ] = item; + row[columns.col_sensitive] = true; + } + + _autogap_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Close gaps"), Glib::ustring(), "Not Used", store)); + _autogap_item->use_group_label(true); + + int autogap = prefs->getInt("/tools/paintbucket/autogap", 0); + _autogap_item->set_active(autogap); + + _autogap_item->signal_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::autogap_changed)); + add(*_autogap_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Reset */ + { + auto reset_button = Gtk::manage(new Gtk::ToolButton(_("Defaults"))); + reset_button->set_tooltip_text(_("Reset paint bucket parameters to defaults (use Inkscape Preferences > Tools to change defaults)")); + reset_button->set_icon_name(INKSCAPE_ICON("edit-clear")); + reset_button->signal_clicked().connect(sigc::mem_fun(*this, &PaintbucketToolbar::defaults)); + add(*reset_button); + reset_button->set_sensitive(true); + } + + show_all(); +} + +GtkWidget * +PaintbucketToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new PaintbucketToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +PaintbucketToolbar::channels_changed(int channels) +{ + Inkscape::UI::Tools::FloodTool::set_channels(channels); +} + +void +PaintbucketToolbar::threshold_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/paintbucket/threshold", (gint)_threshold_adj->get_value()); +} + +void +PaintbucketToolbar::offset_changed() +{ + Unit const *unit = _tracker->getActiveUnit(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Don't adjust the offset value because we're saving the + // unit and it'll be correctly handled on load. + prefs->setDouble("/tools/paintbucket/offset", (gdouble)_offset_adj->get_value()); + + g_return_if_fail(unit != nullptr); + prefs->setString("/tools/paintbucket/offsetunits", unit->abbr); +} + +void +PaintbucketToolbar::autogap_changed(int autogap) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/paintbucket/autogap", autogap); +} + +void +PaintbucketToolbar::defaults() +{ + // FIXME: make defaults settable via Inkscape Options + _threshold_adj->set_value(15); + _offset_adj->set_value(0.0); + + _channels_item->set_active(Inkscape::UI::Tools::FLOOD_CHANNELS_RGB); + _autogap_item->set_active(0); +} + +} +} +} + +/* + 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/toolbar/paintbucket-toolbar.h b/src/ui/toolbar/paintbucket-toolbar.h new file mode 100644 index 0000000..d1b1a77 --- /dev/null +++ b/src/ui/toolbar/paintbucket-toolbar.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PAINTBUCKET_TOOLBAR_H +#define SEEN_PAINTBUCKET_TOOLBAR_H + +/** + * @file + * Paintbucket aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Widget { +class UnitTracker; +class ComboToolItem; +} + +namespace Toolbar { +class PaintbucketToolbar : public Toolbar { +private: + UI::Widget::ComboToolItem *_channels_item; + UI::Widget::ComboToolItem *_autogap_item; + + Glib::RefPtr<Gtk::Adjustment> _threshold_adj; + Glib::RefPtr<Gtk::Adjustment> _offset_adj; + + UI::Widget::UnitTracker *_tracker; + + void channels_changed(int channels); + void threshold_changed(); + void offset_changed(); + void autogap_changed(int autogap); + void defaults(); + +protected: + PaintbucketToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_PAINTBUCKET_TOOLBAR_H */ diff --git a/src/ui/toolbar/pencil-toolbar.cpp b/src/ui/toolbar/pencil-toolbar.cpp new file mode 100644 index 0000000..95de66a --- /dev/null +++ b/src/ui/toolbar/pencil-toolbar.cpp @@ -0,0 +1,692 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Pencil and pen toolbars + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "pencil-toolbar.h" + +#include <glibmm/i18n.h> +#include <gtkmm.h> + +#include "desktop.h" +#include "display/curve.h" +#include "live_effects/lpe-bendpath.h" +#include "live_effects/lpe-bspline.h" +#include "live_effects/lpe-patternalongpath.h" +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-simplify.h" +#include "live_effects/lpe-spiro.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" +#include "object/sp-shape.h" +#include "selection.h" +#include "ui/icon-names.h" +#include "ui/tools/freehand-base.h" +#include "ui/tools/pen-tool.h" +#include "ui/tools/pencil-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { +PencilToolbar::PencilToolbar(SPDesktop *desktop, + bool pencil_mode) + : Toolbar(desktop), + _tool_is_pencil(pencil_mode) +{ + auto prefs = Inkscape::Preferences::get(); + + add_freehand_mode_toggle(); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + if (_tool_is_pencil) { + /* Use pressure */ + { + _pressure_item = add_toggle_button(_("Use pressure input"), _("Use pressure input")); + _pressure_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + bool pressure = prefs->getBool("/tools/freehand/pencil/pressure", false); + _pressure_item->set_active(pressure); + _pressure_item->signal_toggled().connect(sigc::mem_fun(*this, &PencilToolbar::use_pencil_pressure)); + } + /* min pressure */ + { + auto minpressure_val = prefs->getDouble("/tools/freehand/pencil/minpressure", 0); + _minpressure_adj = Gtk::Adjustment::create(minpressure_val, 0, 100, 1, 0); + _minpressure = + Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-minpressure", _("Min:"), _minpressure_adj, 0, 0)); + _minpressure->set_tooltip_text(_("Min percent of pressure")); + _minpressure->set_focus_widget(desktop->canvas); + _minpressure_adj->signal_value_changed().connect( + sigc::mem_fun(*this, &PencilToolbar::minpressure_value_changed)); + add(*_minpressure); + } + /* max pressure */ + { + auto maxpressure_val = prefs->getDouble("/tools/freehand/pencil/maxpressure", 30); + _maxpressure_adj = Gtk::Adjustment::create(maxpressure_val, 0, 100, 1, 0); + _maxpressure = + Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-maxpressure", _("Max:"), _maxpressure_adj, 0, 0)); + _maxpressure->set_tooltip_text(_("Max percent of pressure")); + _maxpressure->set_focus_widget(desktop->canvas); + _maxpressure_adj->signal_value_changed().connect( + sigc::mem_fun(*this, &PencilToolbar::maxpressure_value_changed)); + add(*_maxpressure); + } + + /* powerstoke */ + add_powerstroke_cap(); + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Tolerance */ + { + std::vector<Glib::ustring> labels = { _("(many nodes, rough)"), _("(default)"), "", "", "", "", + _("(few nodes, smooth)") }; + std::vector<double> values = { 1, 10, 20, 30, 50, 75, 100 }; + auto tolerance_val = prefs->getDouble("/tools/freehand/pencil/tolerance", 3.0); + _tolerance_adj = Gtk::Adjustment::create(tolerance_val, 0, 100.0, 0.5, 1.0); + auto tolerance_item = + Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-tolerance", _("Smoothing:"), _tolerance_adj, 1, 2)); + tolerance_item->set_tooltip_text(_("How much smoothing (simplifying) is applied to the line")); + tolerance_item->set_custom_numeric_menu_data(values, labels); + tolerance_item->set_focus_widget(desktop->canvas); + _tolerance_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PencilToolbar::tolerance_value_changed)); + add(*tolerance_item); + } + + /* LPE simplify based tolerance */ + { + _simplify = add_toggle_button(_("LPE based interactive simplify"), _("LPE based interactive simplify")); + _simplify->set_icon_name(INKSCAPE_ICON("interactive_simplify")); + _simplify->set_active(prefs->getInt("/tools/freehand/pencil/simplify", 0)); + _simplify->signal_toggled().connect(sigc::mem_fun(*this, &PencilToolbar::simplify_lpe)); + } + + /* LPE simplify flatten */ + { + _flatten_simplify = Gtk::manage(new Gtk::ToolButton(_("LPE simplify flatten"))); + _flatten_simplify->set_tooltip_text(_("LPE simplify flatten")); + _flatten_simplify->set_icon_name(INKSCAPE_ICON("flatten")); + _flatten_simplify->signal_clicked().connect(sigc::mem_fun(*this, &PencilToolbar::simplify_flatten)); + add(*_flatten_simplify); + } + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + } + + /* advanced shape options */ + add_advanced_shape_options(); + + show_all(); + + // Elements must be hidden after show_all() is called + guint freehandMode = prefs->getInt(( _tool_is_pencil ? + "/tools/freehand/pencil/freehand-mode" : + "/tools/freehand/pen/freehand-mode" ), 0); + if (freehandMode != 1 && freehandMode != 2) { + _flatten_spiro_bspline->set_visible(false); + } + if (_tool_is_pencil) { + use_pencil_pressure(); + } +} + +GtkWidget * +PencilToolbar::create_pencil(SPDesktop *desktop) +{ + auto toolbar = new PencilToolbar(desktop, true); + return GTK_WIDGET(toolbar->gobj()); +} + +PencilToolbar::~PencilToolbar() +{ + if(_repr) { + _repr->removeListenerByData(this); + GC::release(_repr); + _repr = nullptr; + } +} + +void +PencilToolbar::mode_changed(int mode) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt(freehand_tool_name() + "/freehand-mode", mode); + + if (mode == 1 || mode == 2) { + _flatten_spiro_bspline->set_visible(true); + } else { + _flatten_spiro_bspline->set_visible(false); + } + + bool visible = (mode != 2); + + if (_simplify) { + _simplify->set_visible(visible); + if (_flatten_simplify) { + _flatten_simplify->set_visible(visible && _simplify->get_active()); + } + } + + // Recall, the PencilToolbar is also used as the PenToolbar with minor changes. + auto *pt = dynamic_cast<Inkscape::UI::Tools::PenTool *>(_desktop->event_context); + if (pt) { + pt->setPolylineMode(); + } +} + +/* This is used in generic functions below to share large portions of code between pen and pencil tool */ +Glib::ustring const +PencilToolbar::freehand_tool_name() +{ + return _tool_is_pencil ? "/tools/freehand/pencil" : "/tools/freehand/pen"; +} + +void +PencilToolbar::add_freehand_mode_toggle() +{ + auto label = Gtk::manage(new UI::Widget::LabelToolItem(_("Mode:"))); + label->set_tooltip_text(_("Mode of new lines drawn by this tool")); + add(*label); + /* Freehand mode toggle buttons */ + Gtk::RadioToolButton::Group mode_group; + auto bezier_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Bezier"))); + bezier_mode_btn->set_tooltip_text(_("Create regular Bezier path")); + bezier_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-bezier")); + _mode_buttons.push_back(bezier_mode_btn); + + auto spiro_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spiro"))); + spiro_mode_btn->set_tooltip_text(_("Create Spiro path")); + spiro_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-spiro")); + _mode_buttons.push_back(spiro_mode_btn); + + auto bspline_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("BSpline"))); + bspline_mode_btn->set_tooltip_text(_("Create BSpline path")); + bspline_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-bspline")); + _mode_buttons.push_back(bspline_mode_btn); + + if (!_tool_is_pencil) { + auto zigzag_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Zigzag"))); + zigzag_mode_btn->set_tooltip_text(_("Create a sequence of straight line segments")); + zigzag_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-polyline")); + _mode_buttons.push_back(zigzag_mode_btn); + + auto paraxial_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Paraxial"))); + paraxial_mode_btn->set_tooltip_text(_("Create a sequence of paraxial line segments")); + paraxial_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-polyline-paraxial")); + _mode_buttons.push_back(paraxial_mode_btn); + } + + int btn_idx = 0; + for (auto btn : _mode_buttons) { + btn->set_sensitive(true); + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &PencilToolbar::mode_changed), btn_idx++)); + } + + auto prefs = Inkscape::Preferences::get(); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* LPE bspline spiro flatten */ + _flatten_spiro_bspline = Gtk::manage(new Gtk::ToolButton(_("Flatten Spiro or BSpline LPE"))); + _flatten_spiro_bspline->set_tooltip_text(_("Flatten Spiro or BSpline LPE")); + _flatten_spiro_bspline->set_icon_name(INKSCAPE_ICON("flatten")); + _flatten_spiro_bspline->signal_clicked().connect(sigc::mem_fun(*this, &PencilToolbar::flatten_spiro_bspline)); + add(*_flatten_spiro_bspline); + + guint freehandMode = prefs->getInt(( _tool_is_pencil ? + "/tools/freehand/pencil/freehand-mode" : + "/tools/freehand/pen/freehand-mode" ), 0); + // freehandMode range is (0,5] for the pen tool, (0,3] for the pencil tool + // freehandMode = 3 is an old way of signifying pressure, set it to 0. + _mode_buttons[(freehandMode < _mode_buttons.size()) ? freehandMode : 0]->set_active(); +} + +void +PencilToolbar::minpressure_value_changed() +{ + assert(_tool_is_pencil); + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/freehand/pencil/minpressure", _minpressure_adj->get_value()); +} + +void +PencilToolbar::maxpressure_value_changed() +{ + assert(_tool_is_pencil); + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/freehand/pencil/maxpressure", _maxpressure_adj->get_value()); +} + +void +PencilToolbar::shapewidth_value_changed() +{ + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::Selection *selection = _desktop->getSelection(); + SPItem *item = selection->singleItem(); + SPLPEItem *lpeitem = nullptr; + if (item) { + lpeitem = dynamic_cast<SPLPEItem *>(item); + } + using namespace Inkscape::LivePathEffect; + double width = _shapescale_adj->get_value(); + switch (_shape_item->get_active()) { + case Inkscape::UI::Tools::TRIANGLE_IN: + case Inkscape::UI::Tools::TRIANGLE_OUT: + prefs->setDouble("/live_effects/powerstroke/width", width); + if (lpeitem) { + LPEPowerStroke *effect = dynamic_cast<LPEPowerStroke *>(lpeitem->getFirstPathEffectOfType(POWERSTROKE)); + if (effect) { + std::vector<Geom::Point> points = effect->offset_points.data(); + if (points.size() == 1) { + points[0][Geom::Y] = width; + effect->offset_points.param_set_and_write_new_value(points); + } + } + } + break; + case Inkscape::UI::Tools::ELLIPSE: + case Inkscape::UI::Tools::CLIPBOARD: + // The scale of the clipboard isn't known, so getting it to the right size isn't possible. + prefs->setDouble("/live_effects/skeletal/width", width); + if (lpeitem) { + LPEPatternAlongPath *effect = + dynamic_cast<LPEPatternAlongPath *>(lpeitem->getFirstPathEffectOfType(PATTERN_ALONG_PATH)); + if (effect) { + effect->prop_scale.param_set_value(width); + sp_lpe_item_update_patheffect(lpeitem, false, true); + } + } + break; + case Inkscape::UI::Tools::BEND_CLIPBOARD: + prefs->setDouble("/live_effects/bend_path/width", width); + if (lpeitem) { + LPEBendPath *effect = dynamic_cast<LPEBendPath *>(lpeitem->getFirstPathEffectOfType(BEND_PATH)); + if (effect) { + effect->prop_scale.param_set_value(width); + sp_lpe_item_update_patheffect(lpeitem, false, true); + } + } + break; + case Inkscape::UI::Tools::NONE: + case Inkscape::UI::Tools::LAST_APPLIED: + default: + break; + } +} + +void +PencilToolbar::use_pencil_pressure() { + assert(_tool_is_pencil); + bool pressure = _pressure_item->get_active(); + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/freehand/pencil/pressure", pressure); + if (pressure) { + _minpressure->set_visible(true); + _maxpressure->set_visible(true); + _cap_item->set_visible(true); + _shape_item->set_visible(false); + _shapescale->set_visible(false); + _simplify->set_visible(false); + _flatten_spiro_bspline->set_visible(false); + _flatten_simplify->set_visible(false); + for (auto button : _mode_buttons) { + button->set_sensitive(false); + } + } else { + guint freehandMode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0); + + _minpressure->set_visible(false); + _maxpressure->set_visible(false); + _cap_item->set_visible(false); + _shape_item->set_visible(true); + _shapescale->set_visible(true); + bool simplify_visible = freehandMode != 2; + _simplify->set_visible(simplify_visible); + _flatten_simplify->set_visible(simplify_visible && _simplify->get_active()); + if (freehandMode == 1 || freehandMode == 2) { + _flatten_spiro_bspline->set_visible(true); + } + for (auto button : _mode_buttons) { + button->set_sensitive(true); + } + } +} + +void +PencilToolbar::add_advanced_shape_options() +{ + /*advanced shape options */ + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + std::vector<gchar*> freehand_shape_dropdown_items_list = { + const_cast<gchar *>(C_("Freehand shape", "None")), + _("Triangle in"), + _("Triangle out"), + _("Ellipse"), + _("From clipboard"), + _("Bend from clipboard"), + _("Last applied") + }; + + for (auto item:freehand_shape_dropdown_items_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label ] = item; + row[columns.col_sensitive] = true; + } + + _shape_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Shape"), _("Shape of new paths drawn by this tool"), "Not Used", store)); + _shape_item->use_group_label(true); + + auto prefs = Inkscape::Preferences::get(); + int shape = prefs->getInt((_tool_is_pencil ? + "/tools/freehand/pencil/shape" : + "/tools/freehand/pen/shape" ), 0); + _shape_item->set_active(shape); + + _shape_item->signal_changed().connect(sigc::mem_fun(*this, &PencilToolbar::change_shape)); + add(*_shape_item); + + /* power width setting */ + { + _shapescale_adj = Gtk::Adjustment::create(2.0, 0.0, 1000.0, 0.5, 1.0); + _shapescale = + Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-maxpressure", _("Scale:"), _shapescale_adj, 1, 2)); + _shapescale->set_tooltip_text(_("Scale of the width of the power stroke shape.")); + _shapescale->set_focus_widget(_desktop->canvas); + _shapescale_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PencilToolbar::shapewidth_value_changed)); + update_width_value(shape); + add(*_shapescale); + } +} + +void +PencilToolbar::change_shape(int shape) { + auto prefs = Inkscape::Preferences::get(); + prefs->setInt(freehand_tool_name() + "/shape", shape); + update_width_value(shape); +} + +void +PencilToolbar::update_width_value(int shape) { + /* Update shape width with correct width */ + auto prefs = Inkscape::Preferences::get(); + double width = 1.0; + _shapescale->set_sensitive(true); + double powerstrokedefsize = 10 / (0.265 * _desktop->getDocument()->getDocumentScale()[0] * 2.0); + switch (shape) { + case Inkscape::UI::Tools::TRIANGLE_IN: + case Inkscape::UI::Tools::TRIANGLE_OUT: + width = prefs->getDouble("/live_effects/powerstroke/width", powerstrokedefsize); + break; + case Inkscape::UI::Tools::ELLIPSE: + case Inkscape::UI::Tools::CLIPBOARD: + width = prefs->getDouble("/live_effects/skeletal/width", 1.0); + break; + case Inkscape::UI::Tools::BEND_CLIPBOARD: + width = prefs->getDouble("/live_effects/bend_path/width", 1.0); + break; + case Inkscape::UI::Tools::NONE: // Apply width from style? + case Inkscape::UI::Tools::LAST_APPLIED: + default: + _shapescale->set_sensitive(false); + break; + } + _shapescale_adj->set_value(width); +} + +void PencilToolbar::add_powerstroke_cap() +{ + /* Powerstroke cap */ + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + std::vector<gchar *> powerstroke_cap_items_list = { const_cast<gchar *>(C_("Cap", "Butt")), _("Square"), _("Round"), + _("Peak"), _("Zero width") }; + for (auto item : powerstroke_cap_items_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label] = item; + row[columns.col_sensitive] = true; + } + + _cap_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Caps"), _("Line endings when drawing with pressure-sensitive PowerPencil"), "Not Used", store)); + + auto prefs = Inkscape::Preferences::get(); + + int cap = prefs->getInt("/live_effects/powerstroke/powerpencilcap", 2); + _cap_item->set_active(cap); + _cap_item->use_group_label(true); + + _cap_item->signal_changed().connect(sigc::mem_fun(*this, &PencilToolbar::change_cap)); + + add(*_cap_item); +} + +void PencilToolbar::change_cap(int cap) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt("/live_effects/powerstroke/powerpencilcap", cap); +} + +void +PencilToolbar::simplify_lpe() +{ + bool simplify = _simplify->get_active(); + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(freehand_tool_name() + "/simplify", simplify); + _flatten_simplify->set_visible(simplify); +} + +void +PencilToolbar::simplify_flatten() +{ + auto selected = _desktop->getSelection()->items(); + SPLPEItem* lpeitem = nullptr; + for (auto it(selected.begin()); it != selected.end(); ++it){ + lpeitem = dynamic_cast<SPLPEItem*>(*it); + if (lpeitem && lpeitem->hasPathEffect()){ + PathEffectList lpelist = lpeitem->getEffectList(); + PathEffectList::iterator i; + for (i = lpelist.begin(); i != lpelist.end(); ++i) { + LivePathEffectObject *lpeobj = (*i)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (dynamic_cast<Inkscape::LivePathEffect::LPESimplify *>(lpe)) { + SPShape * shape = dynamic_cast<SPShape *>(lpeitem); + if(shape){ + auto c = SPCurve::copy(shape->curveForEdit()); + lpe->doEffect(c.get()); + lpeitem->setCurrentPathEffect(*i); + if (lpelist.size() > 1){ + lpeitem->removeCurrentPathEffect(true); + shape->setCurveBeforeLPE(std::move(c)); + } else { + lpeitem->removeCurrentPathEffect(false); + shape->setCurve(std::move(c)); + } + break; + } + } + } + } + } + } + if (lpeitem) { + _desktop->getSelection()->remove(lpeitem->getRepr()); + _desktop->getSelection()->add(lpeitem->getRepr()); + sp_lpe_item_update_patheffect(lpeitem, false, false); + } +} + +void +PencilToolbar::flatten_spiro_bspline() +{ + auto selected = _desktop->getSelection()->items(); + SPLPEItem* lpeitem = nullptr; + + for (auto it(selected.begin()); it != selected.end(); ++it){ + lpeitem = dynamic_cast<SPLPEItem*>(*it); + if (lpeitem && lpeitem->hasPathEffect()){ + PathEffectList lpelist = lpeitem->getEffectList(); + PathEffectList::iterator i; + for (i = lpelist.begin(); i != lpelist.end(); ++i) { + LivePathEffectObject *lpeobj = (*i)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (dynamic_cast<Inkscape::LivePathEffect::LPEBSpline *>(lpe) || + dynamic_cast<Inkscape::LivePathEffect::LPESpiro *>(lpe)) + { + SPShape * shape = dynamic_cast<SPShape *>(lpeitem); + if(shape){ + auto c = SPCurve::copy(shape->curveForEdit()); + lpe->doEffect(c.get()); + lpeitem->setCurrentPathEffect(*i); + if (lpelist.size() > 1){ + lpeitem->removeCurrentPathEffect(true); + shape->setCurveBeforeLPE(std::move(c)); + } else { + lpeitem->removeCurrentPathEffect(false); + shape->setCurve(std::move(c)); + } + break; + } + } + } + } + } + } + if (lpeitem) { + _desktop->getSelection()->remove(lpeitem->getRepr()); + _desktop->getSelection()->add(lpeitem->getRepr()); + sp_lpe_item_update_patheffect(lpeitem, false, false); + } +} + +GtkWidget * +PencilToolbar::create_pen(SPDesktop *desktop) +{ + auto toolbar = new PencilToolbar(desktop, false); + return GTK_WIDGET(toolbar->gobj()); +} + +void +PencilToolbar::tolerance_value_changed() +{ + assert(_tool_is_pencil); + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _freeze = true; + prefs->setDouble("/tools/freehand/pencil/tolerance", + _tolerance_adj->get_value()); + _freeze = false; + auto selected = _desktop->getSelection()->items(); + for (auto it(selected.begin()); it != selected.end(); ++it){ + SPLPEItem* lpeitem = dynamic_cast<SPLPEItem*>(*it); + if (lpeitem && lpeitem->hasPathEffect()){ + Inkscape::LivePathEffect::Effect *simplify = + lpeitem->getFirstPathEffectOfType(Inkscape::LivePathEffect::SIMPLIFY); + if(simplify){ + Inkscape::LivePathEffect::LPESimplify *lpe_simplify = dynamic_cast<Inkscape::LivePathEffect::LPESimplify*>(simplify->getLPEObj()->get_lpe()); + if (lpe_simplify) { + double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0); + tol = tol/(100.0*(102.0-tol)); + std::ostringstream ss; + ss << tol; + Inkscape::LivePathEffect::Effect *powerstroke = + lpeitem->getFirstPathEffectOfType(Inkscape::LivePathEffect::POWERSTROKE); + bool simplified = false; + if(powerstroke){ + Inkscape::LivePathEffect::LPEPowerStroke *lpe_powerstroke = dynamic_cast<Inkscape::LivePathEffect::LPEPowerStroke*>(powerstroke->getLPEObj()->get_lpe()); + if(lpe_powerstroke){ + lpe_powerstroke->getRepr()->setAttribute("is_visible", "false"); + sp_lpe_item_update_patheffect(lpeitem, false, false); + SPShape *sp_shape = dynamic_cast<SPShape *>(lpeitem); + if (sp_shape) { + guint previous_curve_length = sp_shape->curve()->get_segment_count(); + lpe_simplify->getRepr()->setAttribute("threshold", ss.str()); + sp_lpe_item_update_patheffect(lpeitem, false, false); + simplified = true; + guint curve_length = sp_shape->curve()->get_segment_count(); + std::vector<Geom::Point> ts = lpe_powerstroke->offset_points.data(); + double factor = (double)curve_length/ (double)previous_curve_length; + for (auto & t : ts) { + t[Geom::X] = t[Geom::X] * factor; + } + lpe_powerstroke->offset_points.param_setValue(ts); + } + lpe_powerstroke->getRepr()->setAttribute("is_visible", "true"); + sp_lpe_item_update_patheffect(lpeitem, false, false); + } + } + if(!simplified){ + lpe_simplify->getRepr()->setAttribute("threshold", ss.str()); + } + } + } + } + } +} + +} +} +} +/* + 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/toolbar/pencil-toolbar.h b/src/ui/toolbar/pencil-toolbar.h new file mode 100644 index 0000000..74f0f63 --- /dev/null +++ b/src/ui/toolbar/pencil-toolbar.h @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PENCIL_TOOLBAR_H +#define SEEN_PENCIL_TOOLBAR_H + +/** + * @file + * Pencil and pen toolbars + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> +#include <vector> + +class SPDesktop; + +namespace Gtk { +class RadioToolButton; +class ToggleToolButton; +class ToolButton; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace UI { +namespace Widget { +class SpinButtonToolItem; +class ComboToolItem; +} + +namespace Toolbar { +class PencilToolbar : public Toolbar { +private: + bool const _tool_is_pencil; + std::vector<Gtk::RadioToolButton *> _mode_buttons; + + Gtk::ToggleToolButton *_pressure_item = nullptr; + UI::Widget::SpinButtonToolItem *_minpressure = nullptr; + UI::Widget::SpinButtonToolItem *_maxpressure = nullptr; + UI::Widget::SpinButtonToolItem *_shapescale = nullptr; + + XML::Node *_repr = nullptr; + Gtk::ToolButton *_flatten_spiro_bspline = nullptr; + Gtk::ToolButton *_flatten_simplify = nullptr; + + UI::Widget::ComboToolItem *_shape_item = nullptr; + UI::Widget::ComboToolItem *_cap_item = nullptr; + + Gtk::ToggleToolButton *_simplify = nullptr; + + bool _freeze = false; + + Glib::RefPtr<Gtk::Adjustment> _minpressure_adj; + Glib::RefPtr<Gtk::Adjustment> _maxpressure_adj; + Glib::RefPtr<Gtk::Adjustment> _tolerance_adj; + Glib::RefPtr<Gtk::Adjustment> _shapescale_adj; + + void add_freehand_mode_toggle(); + void mode_changed(int mode); + Glib::ustring const freehand_tool_name(); + void minpressure_value_changed(); + void maxpressure_value_changed(); + void shapewidth_value_changed(); + void use_pencil_pressure(); + void tolerance_value_changed(); + void add_advanced_shape_options(); + void add_powerstroke_cap(); + void change_shape(int shape); + void update_width_value(int shape); + void change_cap(int cap); + void simplify_lpe(); + void simplify_flatten(); + void flatten_spiro_bspline(); + +protected: + PencilToolbar(SPDesktop *desktop, bool pencil_mode); + ~PencilToolbar() override; + +public: + static GtkWidget * create_pencil(SPDesktop *desktop); + static GtkWidget * create_pen(SPDesktop *desktop); +}; +} +} +} + +#endif /* !SEEN_PENCIL_TOOLBAR_H */ diff --git a/src/ui/toolbar/rect-toolbar.cpp b/src/ui/toolbar/rect-toolbar.cpp new file mode 100644 index 0000000..0e59f1e --- /dev/null +++ b/src/ui/toolbar/rect-toolbar.cpp @@ -0,0 +1,416 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Rect aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "rect-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/separatortoolitem.h> +#include <gtkmm/toolbutton.h> + +#include "desktop.h" +#include "document-undo.h" +#include "selection.h" + +#include "object/sp-namedview.h" +#include "object/sp-rect.h" + +#include "ui/icon-names.h" +#include "ui/tools/rect-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +#include "widgets/widget-sizes.h" + +#include "xml/node-event-vector.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::DocumentUndo; +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::Util::unit_table; + +static Inkscape::XML::NodeEventVector rect_tb_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::RectToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +RectToolbar::RectToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)), + _freeze(false), + _single(true), + _repr(nullptr), + _mode_item(Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>")))) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // rx/ry units menu: create + //tracker->addUnit( SP_UNIT_PERCENT, 0 ); + // fixme: add % meaning per cent of the width/height + auto init_units = desktop->getNamedView()->display_units; + _tracker->setActiveUnit(init_units); + _mode_item->set_use_markup(true); + + /* W */ + { + auto width_val = prefs->getDouble("/tools/shapes/rect/width", 0); + width_val = Quantity::convert(width_val, "px", init_units); + + _width_adj = Gtk::Adjustment::create(width_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-width", _("W:"), _width_adj)); + _width_item->get_spin_button()->addUnitTracker(_tracker); + _width_item->set_focus_widget(_desktop->canvas); + _width_item->set_all_tooltip_text(_("Width of rectangle")); + + _width_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed), + _width_adj, + "width", + &SPRect::setVisibleWidth)); + _tracker->addAdjustment(_width_adj->gobj()); + _width_item->set_sensitive(false); + + std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + _width_item->set_custom_numeric_menu_data(values); + } + + /* H */ + { + auto height_val = prefs->getDouble("/tools/shapes/rect/height", 0); + height_val = Quantity::convert(height_val, "px", init_units); + + _height_adj = Gtk::Adjustment::create(height_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _height_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed), + _height_adj, + "height", + &SPRect::setVisibleHeight)); + _tracker->addAdjustment(_height_adj->gobj()); + + std::vector<double> values = { 1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + _height_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-height", _("H:"), _height_adj)); + _height_item->get_spin_button()->addUnitTracker(_tracker); + _height_item->set_custom_numeric_menu_data(values); + _height_item->set_all_tooltip_text(_("Height of rectangle")); + _height_item->set_focus_widget(_desktop->canvas); + _height_item->set_sensitive(false); + } + + /* rx */ + { + std::vector<Glib::ustring> labels = {_("not rounded"), "", "", "", "", "", "", "", ""}; + std::vector<double> values = { 0.5, 1, 2, 3, 5, 10, 20, 50, 100}; + auto rx_val = prefs->getDouble("/tools/shapes/rect/rx", 0); + rx_val = Quantity::convert(rx_val, "px", init_units); + + _rx_adj = Gtk::Adjustment::create(rx_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _rx_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed), + _rx_adj, + "rx", + &SPRect::setVisibleRx)); + _tracker->addAdjustment(_rx_adj->gobj()); + _rx_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-rx", _("Rx:"), _rx_adj)); + _rx_item->get_spin_button()->addUnitTracker(_tracker); + _rx_item->set_all_tooltip_text(_("Horizontal radius of rounded corners")); + _rx_item->set_focus_widget(_desktop->canvas); + _rx_item->set_custom_numeric_menu_data(values, labels); + } + + /* ry */ + { + std::vector<Glib::ustring> labels = {_("not rounded"), "", "", "", "", "", "", "", ""}; + std::vector<double> values = { 0.5, 1, 2, 3, 5, 10, 20, 50, 100}; + auto ry_val = prefs->getDouble("/tools/shapes/rect/ry", 0); + ry_val = Quantity::convert(ry_val, "px", init_units); + + _ry_adj = Gtk::Adjustment::create(ry_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _ry_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed), + _ry_adj, + "ry", + &SPRect::setVisibleRy)); + _tracker->addAdjustment(_ry_adj->gobj()); + _ry_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-ry", _("Ry:"), _ry_adj)); + _ry_item->get_spin_button()->addUnitTracker(_tracker); + _ry_item->set_all_tooltip_text(_("Vertical radius of rounded corners")); + _ry_item->set_focus_widget(_desktop->canvas); + _ry_item->set_custom_numeric_menu_data(values, labels); + } + + // add the units menu + auto unit_menu_ti = _tracker->create_tool_item(_("Units"), ("")); + + /* Reset */ + { + _not_rounded = Gtk::manage(new Gtk::ToolButton(_("Not rounded"))); + _not_rounded->set_tooltip_text(_("Make corners sharp")); + _not_rounded->set_icon_name(INKSCAPE_ICON("rectangle-make-corners-sharp")); + _not_rounded->signal_clicked().connect(sigc::mem_fun(*this, &RectToolbar::defaults)); + _not_rounded->set_sensitive(true); + } + + add(*_mode_item); + add(*_width_item); + add(*_height_item); + add(*_rx_item); + add(*_ry_item); + add(*unit_menu_ti); + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_not_rounded); + show_all(); + + sensitivize(); + + _desktop->connectEventContextChanged(sigc::mem_fun(*this, &RectToolbar::watch_ec)); +} + +RectToolbar::~RectToolbar() +{ + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + _changed.disconnect(); +} + +GtkWidget * +RectToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new RectToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +RectToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name, + void (SPRect::*setter)(gdouble)) +{ + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/shapes/rect/") + value_name, + Quantity::convert(adj->get_value(), unit, "px")); + } + + // quit if run by the attr_changed listener + if (_freeze || _tracker->isUpdating()) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + if (SP_IS_RECT(*i)) { + if (adj->get_value() != 0) { + (SP_RECT(*i)->*setter)(Quantity::convert(adj->get_value(), unit, "px")); + } else { + (*i)->removeAttribute(value_name); + } + modmade = true; + } + } + + sensitivize(); + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), _("Change rectangle"), INKSCAPE_ICON("draw-rectangle")); + } + + _freeze = false; +} + +void +RectToolbar::sensitivize() +{ + if (_rx_adj->get_value() == 0 && _ry_adj->get_value() == 0 && _single) { // only for a single selected rect (for now) + _not_rounded->set_sensitive(false); + } else { + _not_rounded->set_sensitive(true); + } +} + +void +RectToolbar::defaults() +{ + _rx_adj->set_value(0.0); + _ry_adj->set_value(0.0); + + sensitivize(); +} + +void +RectToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + // use of dynamic_cast<> seems wrong here -- we just need to check the current tool + + if (dynamic_cast<Inkscape::UI::Tools::RectTool *>(ec)) { + Inkscape::Selection *sel = desktop->getSelection(); + + _changed = sel->connectChanged(sigc::mem_fun(*this, &RectToolbar::selection_changed)); + + // Synthesize an emission to trigger the update + selection_changed(sel); + } else { + if (_changed) { + _changed.disconnect(); + + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + } + } +} + +/** + * \param selection should not be NULL. + */ +void +RectToolbar::selection_changed(Inkscape::Selection *selection) +{ + int n_selected = 0; + Inkscape::XML::Node *repr = nullptr; + SPItem *item = nullptr; + + if (_repr) { // remove old listener + _item = nullptr; + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + if (SP_IS_RECT(*i)) { + n_selected++; + item = *i; + repr = item->getRepr(); + } + } + + _single = false; + + if (n_selected == 0) { + _mode_item->set_markup(_("<b>New:</b>")); + _width_item->set_sensitive(false); + _height_item->set_sensitive(false); + } else if (n_selected == 1) { + _mode_item->set_markup(_("<b>Change:</b>")); + _single = true; + _width_item->set_sensitive(true); + _height_item->set_sensitive(true); + + if (repr) { + _repr = repr; + _item = item; + Inkscape::GC::anchor(_repr); + _repr->addListener(&rect_tb_repr_events, this); + _repr->synthesizeEvents(&rect_tb_repr_events, this); + } + } else { + // FIXME: implement averaging of all parameters for multiple selected + //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>")); + _mode_item->set_markup(_("<b>Change:</b>")); + sensitivize(); + } +} + +void RectToolbar::event_attr_changed(Inkscape::XML::Node * /*repr*/, gchar const * /*name*/, + gchar const * /*old_value*/, gchar const * /*new_value*/, + bool /*is_interactive*/, gpointer data) +{ + auto toolbar = reinterpret_cast<RectToolbar*>(data); + + // quit if run by the _changed callbacks + if (toolbar->_freeze) { + return; + } + + // in turn, prevent callbacks from responding + toolbar->_freeze = true; + + Unit const *unit = toolbar->_tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + if (toolbar->_item && SP_IS_RECT(toolbar->_item)) { + { + gdouble rx = SP_RECT(toolbar->_item)->getVisibleRx(); + toolbar->_rx_adj->set_value(Quantity::convert(rx, "px", unit)); + } + + { + gdouble ry = SP_RECT(toolbar->_item)->getVisibleRy(); + toolbar->_ry_adj->set_value(Quantity::convert(ry, "px", unit)); + } + + { + gdouble width = SP_RECT(toolbar->_item)->getVisibleWidth(); + toolbar->_width_adj->set_value(Quantity::convert(width, "px", unit)); + } + + { + gdouble height = SP_RECT(toolbar->_item)->getVisibleHeight(); + toolbar->_height_adj->set_value(Quantity::convert(height, "px", unit)); + } + } + + toolbar->sensitivize(); + toolbar->_freeze = false; +} + +} +} +} + + +/* + 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/toolbar/rect-toolbar.h b/src/ui/toolbar/rect-toolbar.h new file mode 100644 index 0000000..4937673 --- /dev/null +++ b/src/ui/toolbar/rect-toolbar.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_RECT_TOOLBAR_H +#define SEEN_RECT_TOOLBAR_H + +/** + * @file + * Rect aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; +class SPItem; +class SPRect; + +namespace Gtk { +class Toolbutton; +} + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class LabelToolItem; +class SpinButtonToolItem; +class UnitTracker; +} + +namespace Toolbar { +class RectToolbar : public Toolbar { +private: + UI::Widget::UnitTracker *_tracker; + + XML::Node *_repr; + SPItem *_item; + + UI::Widget::LabelToolItem *_mode_item; + UI::Widget::SpinButtonToolItem *_width_item; + UI::Widget::SpinButtonToolItem *_height_item; + UI::Widget::SpinButtonToolItem *_rx_item; + UI::Widget::SpinButtonToolItem *_ry_item; + Gtk::ToolButton *_not_rounded; + + Glib::RefPtr<Gtk::Adjustment> _width_adj; + Glib::RefPtr<Gtk::Adjustment> _height_adj; + Glib::RefPtr<Gtk::Adjustment> _rx_adj; + Glib::RefPtr<Gtk::Adjustment> _ry_adj; + + bool _freeze; + bool _single; + + void value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name, + void (SPRect::*setter)(gdouble)); + + void sensitivize(); + void defaults(); + void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_changed(Inkscape::Selection *selection); + + sigc::connection _changed; + +protected: + RectToolbar(SPDesktop *desktop); + ~RectToolbar() override; + +public: + static GtkWidget * create(SPDesktop *desktop); + + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const *old_value, + gchar const *new_value, + bool is_interactive, + gpointer data); + +}; + +} +} +} + +#endif /* !SEEN_RECT_TOOLBAR_H */ diff --git a/src/ui/toolbar/select-toolbar.cpp b/src/ui/toolbar/select-toolbar.cpp new file mode 100644 index 0000000..528b885 --- /dev/null +++ b/src/ui/toolbar/select-toolbar.cpp @@ -0,0 +1,631 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Selector aux toolbar + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2003-2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "select-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/adjustment.h> +#include <gtkmm/separatortoolitem.h> + +#include <2geom/rect.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "selection.h" +#include "message-stack.h" +#include "selection-chemistry.h" + +#include "object/sp-item-transform.h" +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/widget/canvas.h" // Focus widget +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/unit-tracker.h" + +#include "widgets/widget-sizes.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +SelectToolbar::SelectToolbar(SPDesktop *desktop) : + Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)), + _lock_btn(Gtk::manage(new Gtk::ToggleToolButton())), + _select_touch_btn(Gtk::manage(new Gtk::ToggleToolButton())), + _transform_stroke_btn(Gtk::manage(new Gtk::ToggleToolButton())), + _transform_corners_btn(Gtk::manage(new Gtk::ToggleToolButton())), + _transform_gradient_btn(Gtk::manage(new Gtk::ToggleToolButton())), + _transform_pattern_btn(Gtk::manage(new Gtk::ToggleToolButton())), + _update(false), + _action_prefix("selector:toolbar:") +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("Select Al_l"))); + button->set_tooltip_text(N_("Select all objects")); + button->set_icon_name(INKSCAPE_ICON("edit-select-all")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "win.select-all"); + add(*button); + } + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("Select All in All La_yers"))); + button->set_tooltip_text(N_("Select all objects in all visible and unlocked layers")); + button->set_icon_name(INKSCAPE_ICON("edit-select-all-layers")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "win.select-all-layers"); + add(*button); + } + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("D_eselect"))); + button->set_tooltip_text(N_("Deselect any selected objects")); + button->set_icon_name(INKSCAPE_ICON("edit-select-none")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "win.select-none"); + add(*button); + _context_items.push_back(button); + } + + _select_touch_btn->set_label(_("Select by touch")); + _select_touch_btn->set_tooltip_text(_("Toggle selection box to select all touched objects.")); + _select_touch_btn->set_icon_name(INKSCAPE_ICON("selection-touch")); + _select_touch_btn->set_active(prefs->getBool("/tools/select/touch_box", false)); + _select_touch_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_touch)); + + add(*_select_touch_btn); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("Rotate _90\xc2\xb0 CCW"))); + button->set_tooltip_text(N_("Rotate selection 90\xc2\xb0 counter-clockwise")); + button->set_icon_name(INKSCAPE_ICON("object-rotate-left")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.object-rotate-90-ccw"); + add(*button); + _context_items.push_back(button); + } + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("Rotate _90\xc2\xb0 CW"))); + button->set_tooltip_text(N_("Rotate selection 90\xc2\xb0 clockwise")); + button->set_icon_name(INKSCAPE_ICON("object-rotate-right")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.object-rotate-90-cw"); + add(*button); + _context_items.push_back(button); + } + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("Flip _Horizontal"))); + button->set_tooltip_text(N_("Flip selected objects horizontally")); + button->set_icon_name(INKSCAPE_ICON("object-flip-horizontal")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.object-flip-horizontal"); + add(*button); + _context_items.push_back(button); + } + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("Flip _Vertical"))); + button->set_tooltip_text(N_("Flip selected objects vertically")); + button->set_icon_name(INKSCAPE_ICON("object-flip-vertical")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.object-flip-vertical"); + add(*button); + _context_items.push_back(button); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("Raise to _Top"))); + button->set_tooltip_text(N_("Raise selection to top")); + button->set_icon_name(INKSCAPE_ICON("selection-top")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.selection-top"); + add(*button); + _context_items.push_back(button); + } + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("_Raise"))); + button->set_tooltip_text(N_("Raise selection one step")); + button->set_icon_name(INKSCAPE_ICON("selection-raise")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.selection-raise"); + add(*button); + _context_items.push_back(button); + } + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("_Lower"))); + button->set_tooltip_text(N_("Lower selection one step")); + button->set_icon_name(INKSCAPE_ICON("selection-lower")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.selection-lower"); + add(*button); + _context_items.push_back(button); + } + + { + auto button = Gtk::manage(new Gtk::ToolButton(N_("Lower to _Bottom"))); + button->set_tooltip_text(N_("Lower selection to bottom")); + button->set_icon_name(INKSCAPE_ICON("selection-bottom")); + // Must use C API until GTK4 + gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.selection-bottom"); + add(*button); + _context_items.push_back(button); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + _tracker->addUnit(unit_table.getUnit("%")); + _tracker->setActiveUnit( desktop->getNamedView()->display_units ); + + // x-value control + auto x_val = prefs->getDouble("/tools/select/X", 0.0); + _adj_x = Gtk::Adjustment::create(x_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _adj_x->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_x)); + _tracker->addAdjustment(_adj_x->gobj()); + + auto x_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-x", + C_("Select toolbar", "X:"), + _adj_x, + SPIN_STEP, 3)); + x_btn->get_spin_button()->addUnitTracker(_tracker.get()); + x_btn->set_focus_widget(_desktop->getCanvas()); + x_btn->set_all_tooltip_text(C_("Select toolbar", "Horizontal coordinate of selection")); + _context_items.push_back(x_btn); + add(*x_btn); + + // y-value control + auto y_val = prefs->getDouble("/tools/select/Y", 0.0); + _adj_y = Gtk::Adjustment::create(y_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _adj_y->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_y)); + _tracker->addAdjustment(_adj_y->gobj()); + + auto y_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-y", + C_("Select toolbar", "Y:"), + _adj_y, + SPIN_STEP, 3)); + y_btn->get_spin_button()->addUnitTracker(_tracker.get()); + y_btn->set_focus_widget(_desktop->getCanvas()); + y_btn->set_all_tooltip_text(C_("Select toolbar", "Vertical coordinate of selection")); + _context_items.push_back(y_btn); + add(*y_btn); + + // width-value control + auto w_val = prefs->getDouble("/tools/select/width", 0.0); + _adj_w = Gtk::Adjustment::create(w_val, 0.0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _adj_w->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_w)); + _tracker->addAdjustment(_adj_w->gobj()); + + auto w_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-width", + C_("Select toolbar", "W:"), + _adj_w, + SPIN_STEP, 3)); + w_btn->get_spin_button()->addUnitTracker(_tracker.get()); + w_btn->set_focus_widget(_desktop->getCanvas()); + w_btn->set_all_tooltip_text(C_("Select toolbar", "Width of selection")); + _context_items.push_back(w_btn); + add(*w_btn); + + // lock toggle + _lock_btn->set_label(_("Lock width and height")); + _lock_btn->set_tooltip_text(_("When locked, change both width and height by the same proportion")); + _lock_btn->set_icon_name(INKSCAPE_ICON("object-unlocked")); + _lock_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_lock)); + _lock_btn->set_name("lock"); + add(*_lock_btn); + + // height-value control + auto h_val = prefs->getDouble("/tools/select/height", 0.0); + _adj_h = Gtk::Adjustment::create(h_val, 0.0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _adj_h->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_h)); + _tracker->addAdjustment(_adj_h->gobj()); + + auto h_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-height", + C_("Select toolbar", "H:"), + _adj_h, + SPIN_STEP, 3)); + h_btn->get_spin_button()->addUnitTracker(_tracker.get()); + h_btn->set_focus_widget(_desktop->getCanvas()); + h_btn->set_all_tooltip_text(C_("Select toolbar", "Height of selection")); + _context_items.push_back(h_btn); + add(*h_btn); + + // units menu + auto unit_menu = _tracker->create_tool_item(_("Units"), ("") ); + add(*unit_menu); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + _transform_stroke_btn->set_label(_("Scale stroke width")); + _transform_stroke_btn->set_tooltip_text(_("When scaling objects, scale the stroke width by the same proportion")); + _transform_stroke_btn->set_icon_name(INKSCAPE_ICON("transform-affect-stroke")); + _transform_stroke_btn->set_active(prefs->getBool("/options/transform/stroke", true)); + _transform_stroke_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_stroke)); + add(*_transform_stroke_btn); + + _transform_corners_btn->set_label(_("Scale rounded corners")); + _transform_corners_btn->set_tooltip_text(_("When scaling rectangles, scale the radii of rounded corners")); + _transform_corners_btn->set_icon_name(INKSCAPE_ICON("transform-affect-rounded-corners")); + _transform_corners_btn->set_active(prefs->getBool("/options/transform/rectcorners", true)); + _transform_corners_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_corners)); + add(*_transform_corners_btn); + + _transform_gradient_btn->set_label(_("Move gradients")); + _transform_gradient_btn->set_tooltip_text(_("Move gradients (in fill or stroke) along with the objects")); + _transform_gradient_btn->set_icon_name(INKSCAPE_ICON("transform-affect-gradient")); + _transform_gradient_btn->set_active(prefs->getBool("/options/transform/gradient", true)); + _transform_gradient_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_gradient)); + add(*_transform_gradient_btn); + + _transform_pattern_btn->set_label(_("Move patterns")); + _transform_pattern_btn->set_tooltip_text(_("Move patterns (in fill or stroke) along with the objects")); + _transform_pattern_btn->set_icon_name(INKSCAPE_ICON("transform-affect-pattern")); + _transform_pattern_btn->set_active(prefs->getBool("/options/transform/pattern", true)); + _transform_pattern_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_pattern)); + add(*_transform_pattern_btn); + + assert(desktop); + auto *selection = desktop->getSelection(); + + // Force update when selection changes. + _connections.emplace_back( // + selection->connectModified(sigc::mem_fun(*this, &SelectToolbar::on_inkscape_selection_modified))); + _connections.emplace_back( + selection->connectChanged(sigc::mem_fun(*this, &SelectToolbar::on_inkscape_selection_changed))); + + // Update now. + layout_widget_update(selection); + + for (auto item : _context_items) { + if ( item->is_sensitive() ) { + item->set_sensitive(false); + } + } + + show_all(); +} + +void SelectToolbar::on_unrealize() +{ + for (auto &conn : _connections) { + conn.disconnect(); + } + + parent_type::on_unrealize(); +} + +GtkWidget * +SelectToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new SelectToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +SelectToolbar::any_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj) +{ + if (_update) { + return; + } + + if ( !_tracker || _tracker->isUpdating() ) { + /* + * When only units are being changed, don't treat changes + * to adjuster values as object changes. + */ + return; + } + _update = true; + + SPDesktop *desktop = _desktop; + Inkscape::Selection *selection = desktop->getSelection(); + SPDocument *document = desktop->getDocument(); + + document->ensureUpToDate (); + + Geom::OptRect bbox_vis = selection->visualBounds(); + Geom::OptRect bbox_geom = selection->geometricBounds(); + Geom::OptRect bbox_user = selection->preferredBounds(); + + if ( !bbox_user ) { + _update = false; + return; + } + + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + gdouble old_w = bbox_user->dimensions()[Geom::X]; + gdouble old_h = bbox_user->dimensions()[Geom::Y]; + gdouble new_w, new_h, new_x, new_y = 0; + + if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) { + new_w = Quantity::convert(_adj_w->get_value(), unit, "px"); + new_h = Quantity::convert(_adj_h->get_value(), unit, "px"); + new_x = Quantity::convert(_adj_x->get_value(), unit, "px"); + new_y = Quantity::convert(_adj_y->get_value(), unit, "px"); + + } else { + gdouble old_x = bbox_user->min()[Geom::X] + (old_w * selection->anchor_x); + gdouble old_y = bbox_user->min()[Geom::Y] + (old_h * selection->anchor_y); + + new_x = old_x * (_adj_x->get_value() / 100 / unit->factor); + new_y = old_y * (_adj_y->get_value() / 100 / unit->factor); + new_w = old_w * (_adj_w->get_value() / 100 / unit->factor); + new_h = old_h * (_adj_h->get_value() / 100 / unit->factor); + } + + // Adjust depending on the selected anchor. + gdouble x0 = (new_x - (old_w * selection->anchor_x)) - ((new_w - old_w) * selection->anchor_x); + gdouble y0 = (new_y - (old_h * selection->anchor_y)) - ((new_h - old_h) * selection->anchor_y); + + gdouble x1 = x0 + new_w; + gdouble xrel = new_w / old_w; + gdouble y1 = y0 + new_h; + gdouble yrel = new_h / old_h; + + // Keep proportions if lock is on + if ( _lock_btn->get_active() ) { + if (adj == _adj_h) { + x1 = x0 + yrel * bbox_user->dimensions()[Geom::X]; + } else if (adj == _adj_w) { + y1 = y0 + xrel * bbox_user->dimensions()[Geom::Y]; + } + } + + // scales and moves, in px + double mh = fabs(x0 - bbox_user->min()[Geom::X]); + double sh = fabs(x1 - bbox_user->max()[Geom::X]); + double mv = fabs(y0 - bbox_user->min()[Geom::Y]); + double sv = fabs(y1 - bbox_user->max()[Geom::Y]); + + // unless the unit is %, convert the scales and moves to the unit + if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) { + mh = Quantity::convert(mh, "px", unit); + sh = Quantity::convert(sh, "px", unit); + mv = Quantity::convert(mv, "px", unit); + sv = Quantity::convert(sv, "px", unit); + } + + char const *const actionkey = get_action_key(mh, sh, mv, sv); + + if (actionkey != nullptr) { + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool preserve = prefs->getBool("/options/preservetransform/value", false); + + Geom::Affine scaler; + if (prefs->getInt("/tools/bounding_box") == 0) { // SPItem::VISUAL_BBOX + scaler = get_scale_transform_for_variable_stroke (*bbox_vis, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1); + } else { + // 1) We could have use the newer get_scale_transform_for_variable_stroke() here, but to avoid regressions + // we'll just use the old get_scale_transform_for_uniform_stroke() for now. + // 2) get_scale_transform_for_uniform_stroke() is intended for visual bounding boxes, not geometrical ones! + // we'll trick it into using a geometric bounding box though, by setting the stroke width to zero + scaler = get_scale_transform_for_uniform_stroke (*bbox_geom, 0, 0, false, false, x0, y0, x1, y1); + } + + selection->applyAffine(scaler); + DocumentUndo::maybeDone(document, actionkey, _("Transform by toolbar"), INKSCAPE_ICON("tool-pointer")); + } + + _update = false; +} + +void +SelectToolbar::layout_widget_update(Inkscape::Selection *sel) +{ + if (_update) { + return; + } + + _update = true; + using Geom::X; + using Geom::Y; + if ( sel && !sel->isEmpty() ) { + Geom::OptRect const bbox(sel->preferredBounds()); + if ( bbox ) { + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + auto width = bbox->dimensions()[X]; + auto height = bbox->dimensions()[Y]; + auto x = bbox->min()[X] + (width * sel->anchor_x); + auto y = bbox->min()[Y] + (height * sel->anchor_y); + + if (unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) { + double const val = unit->factor * 100; + _adj_x->set_value(val); + _adj_y->set_value(val); + _adj_w->set_value(val); + _adj_h->set_value(val); + _tracker->setFullVal( _adj_x->gobj(), x ); + _tracker->setFullVal( _adj_y->gobj(), y ); + _tracker->setFullVal( _adj_w->gobj(), width ); + _tracker->setFullVal( _adj_h->gobj(), height ); + } else { + _adj_x->set_value(Quantity::convert(x, "px", unit)); + _adj_y->set_value(Quantity::convert(y, "px", unit)); + _adj_w->set_value(Quantity::convert(width, "px", unit)); + _adj_h->set_value(Quantity::convert(height, "px", unit)); + } + } + } + + _update = false; +} + +void +SelectToolbar::on_inkscape_selection_modified(Inkscape::Selection *selection, guint flags) +{ + assert(_desktop->getSelection() == selection); + if ((flags & (SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_PARENT_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG ))) + { + layout_widget_update(selection); + } +} + +void +SelectToolbar::on_inkscape_selection_changed(Inkscape::Selection *selection) +{ + assert(_desktop->getSelection() == selection); + { + bool setActive = (selection && !selection->isEmpty()); + + for (auto item : _context_items) { + if ( setActive != item->get_sensitive() ) { + item->set_sensitive(setActive); + } + } + + layout_widget_update(selection); + _selection_seq++; + } +} + +char const *SelectToolbar::get_action_key(double mh, double sh, double mv, double sv) +{ + // do the action only if one of the scales/moves is greater than half the last significant + // digit in the spinbox (currently spinboxes have 3 fractional digits, so that makes 0.0005). If + // the value was changed by the user, the difference will be at least that much; otherwise it's + // just rounding difference between the spinbox value and actual value, so no action is + // performed + double const threshold = 5e-4; + char const *const action = ( mh > threshold ? "move:horizontal:" : + sh > threshold ? "scale:horizontal:" : + mv > threshold ? "move:vertical:" : + sv > threshold ? "scale:vertical:" : nullptr ); + if (!action) { + return nullptr; + } + _action_key = _action_prefix + action + std::to_string(_selection_seq); + return _action_key.c_str(); +} + +void +SelectToolbar::toggle_lock() { + // use this roundabout way of changing image to make sure its size is preserved + auto btn = static_cast<Gtk::ToggleButton*>(_lock_btn->get_child()); + auto image = static_cast<Gtk::Image*>(btn->get_child()); + if (!image) { + g_warning("No GTK image in toolbar button 'lock'"); + return; + } + auto size = image->get_pixel_size(); + + if ( _lock_btn->get_active() ) { + image->set_from_icon_name("object-locked", Gtk::ICON_SIZE_BUTTON); + } else { + image->set_from_icon_name("object-unlocked", Gtk::ICON_SIZE_BUTTON); + } + image->set_pixel_size(size); +} + +void +SelectToolbar::toggle_touch() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/select/touch_box", _select_touch_btn->get_active()); +} + +void +SelectToolbar::toggle_stroke() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool active = _transform_stroke_btn->get_active(); + prefs->setBool("/options/transform/stroke", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>stroke width</b> is <b>scaled</b> when objects are scaled.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>stroke width</b> is <b>not scaled</b> when objects are scaled.")); + } +} + +void +SelectToolbar::toggle_corners() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool active = _transform_corners_btn->get_active(); + prefs->setBool("/options/transform/rectcorners", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>rounded rectangle corners</b> are <b>scaled</b> when rectangles are scaled.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>rounded rectangle corners</b> are <b>not scaled</b> when rectangles are scaled.")); + } +} + +void +SelectToolbar::toggle_gradient() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool active = _transform_gradient_btn->get_active(); + prefs->setBool("/options/transform/gradient", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>gradients</b> are <b>transformed</b> along with their objects when those are transformed (moved, scaled, rotated, or skewed).")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>gradients</b> remain <b>fixed</b> when objects are transformed (moved, scaled, rotated, or skewed).")); + } +} + +void +SelectToolbar::toggle_pattern() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool active = _transform_pattern_btn->get_active(); + prefs->setInt("/options/transform/pattern", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>patterns</b> are <b>transformed</b> along with their objects when those are transformed (moved, scaled, rotated, or skewed).")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>patterns</b> remain <b>fixed</b> when objects are transformed (moved, scaled, rotated, or skewed).")); + } +} + +} +} +} + +/* + 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/toolbar/select-toolbar.h b/src/ui/toolbar/select-toolbar.h new file mode 100644 index 0000000..3d8a2d6 --- /dev/null +++ b/src/ui/toolbar/select-toolbar.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SELECT_TOOLBAR_H +#define SEEN_SELECT_TOOLBAR_H + +/** \file + * Selector aux toolbar + */ +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <bulia@dr.com> + * + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +class SPDesktop; + +namespace Inkscape { +class Selection; + +namespace UI { + +namespace Widget { +class UnitTracker; +} + +namespace Toolbar { + +class SelectToolbar : public Toolbar { + using parent_type = Toolbar; + +private: + std::unique_ptr<UI::Widget::UnitTracker> _tracker; + + Glib::RefPtr<Gtk::Adjustment> _adj_x; + Glib::RefPtr<Gtk::Adjustment> _adj_y; + Glib::RefPtr<Gtk::Adjustment> _adj_w; + Glib::RefPtr<Gtk::Adjustment> _adj_h; + Gtk::ToggleToolButton *_lock_btn; + Gtk::ToggleToolButton *_select_touch_btn; + Gtk::ToggleToolButton *_transform_stroke_btn; + Gtk::ToggleToolButton *_transform_corners_btn; + Gtk::ToggleToolButton *_transform_gradient_btn; + Gtk::ToggleToolButton *_transform_pattern_btn; + + std::vector<Gtk::ToolItem *> _context_items; + + std::vector<sigc::connection> _connections; + + bool _update; + std::uint64_t _selection_seq = 0; ///< Increment to prevent coalescing of consecutive undo events + std::string _action_key; + std::string const _action_prefix; + + char const *get_action_key(double mh, double sh, double mv, double sv); + void any_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj); + void layout_widget_update(Inkscape::Selection *sel); + void on_inkscape_selection_modified(Inkscape::Selection *selection, guint flags); + void on_inkscape_selection_changed(Inkscape::Selection *selection); + void toggle_lock(); + void toggle_touch(); + void toggle_stroke(); + void toggle_corners(); + void toggle_gradient(); + void toggle_pattern(); + +protected: + SelectToolbar(SPDesktop *desktop); + + void on_unrealize() override; + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} +#endif /* !SEEN_SELECT_TOOLBAR_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/toolbar/spiral-toolbar.cpp b/src/ui/toolbar/spiral-toolbar.cpp new file mode 100644 index 0000000..73ea79e --- /dev/null +++ b/src/ui/toolbar/spiral-toolbar.cpp @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Spiral aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spiral-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/separatortoolitem.h> +#include <gtkmm/toolbutton.h> + +#include "desktop.h" +#include "document-undo.h" +#include "selection.h" + +#include "object/sp-spiral.h" + +#include "ui/icon-names.h" +#include "ui/widget/canvas.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +static Inkscape::XML::NodeEventVector spiral_tb_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::SpiralToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +SpiralToolbar::SpiralToolbar(SPDesktop *desktop) : + Toolbar(desktop), + _freeze(false), + _repr(nullptr) +{ + auto prefs = Inkscape::Preferences::get(); + + { + _mode_item = Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>"))); + _mode_item->set_use_markup(true); + add(*_mode_item); + } + + /* Revolution */ + { + std::vector<Glib::ustring> labels = {_("just a curve"), "", _("one full revolution"), "", "", "", "", "", "", ""}; + std::vector<double> values = { 0.01, 0.5, 1, 2, 3, 5, 10, 20, 50, 100}; + auto revolution_val = prefs->getDouble("/tools/shapes/spiral/revolution", 3.0); + _revolution_adj = Gtk::Adjustment::create(revolution_val, 0.01, 1024.0, 0.1, 1.0); + _revolution_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spiral-revolutions", _("Turns:"), _revolution_adj, 1, 2)); + _revolution_item->set_tooltip_text(_("Number of revolutions")); + _revolution_item->set_custom_numeric_menu_data(values, labels); + _revolution_item->set_focus_widget(desktop->getCanvas()); + _revolution_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SpiralToolbar::value_changed), + _revolution_adj, "revolution")); + add(*_revolution_item); + } + + /* Expansion */ + { + std::vector<Glib::ustring> labels = {_("circle"), _("edge is much denser"), _("edge is denser"), _("even"), _("center is denser"), _("center is much denser"), ""}; + std::vector<double> values = { 0, 0.1, 0.5, 1, 1.5, 5, 20}; + auto expansion_val = prefs->getDouble("/tools/shapes/spiral/expansion", 1.0); + _expansion_adj = Gtk::Adjustment::create(expansion_val, 0.0, 1000.0, 0.01, 1.0); + + _expansion_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spiral-expansion", _("Divergence:"), _expansion_adj)); + _expansion_item->set_tooltip_text(_("How much denser/sparser are outer revolutions; 1 = uniform")); + _expansion_item->set_custom_numeric_menu_data(values, labels); + _expansion_item->set_focus_widget(desktop->getCanvas()); + _expansion_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SpiralToolbar::value_changed), + _expansion_adj, "expansion")); + add(*_expansion_item); + } + + /* T0 */ + { + std::vector<Glib::ustring> labels = {_("starts from center"), _("starts mid-way"), _("starts near edge")}; + std::vector<double> values = { 0, 0.5, 0.9}; + auto t0_val = prefs->getDouble("/tools/shapes/spiral/t0", 0.0); + _t0_adj = Gtk::Adjustment::create(t0_val, 0.0, 0.999, 0.01, 1.0); + _t0_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spiral-t0", _("Inner radius:"), _t0_adj)); + _t0_item->set_tooltip_text(_("Radius of the innermost revolution (relative to the spiral size)")); + _t0_item->set_custom_numeric_menu_data(values, labels); + _t0_item->set_focus_widget(desktop->getCanvas()); + _t0_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SpiralToolbar::value_changed), + _t0_adj, "t0")); + add(*_t0_item); + } + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Reset */ + { + _reset_item = Gtk::manage(new Gtk::ToolButton(_("Defaults"))); + _reset_item->set_icon_name(INKSCAPE_ICON("edit-clear")); + _reset_item->set_tooltip_text(_("Reset shape parameters to defaults (use Inkscape Preferences > Tools to change defaults)")); + _reset_item->signal_clicked().connect(sigc::mem_fun(*this, &SpiralToolbar::defaults)); + add(*_reset_item); + } + + _connection.reset(new sigc::connection( + desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &SpiralToolbar::selection_changed)))); + + show_all(); +} + +SpiralToolbar::~SpiralToolbar() +{ + if(_repr) { + _repr->removeListenerByData(this); + GC::release(_repr); + _repr = nullptr; + } + + if(_connection) { + _connection->disconnect(); + } +} + +GtkWidget * +SpiralToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new SpiralToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +SpiralToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, + Glib::ustring const &value_name) +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/shapes/spiral/" + value_name, + adj->get_value()); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + gchar* namespaced_name = g_strconcat("sodipodi:", value_name.data(), nullptr); + + bool modmade = false; + auto itemlist= _desktop->getSelection()->items(); + for(auto i=itemlist.begin();i!=itemlist.end(); ++i){ + SPItem *item = *i; + if (SP_IS_SPIRAL(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + repr->setAttributeSvgDouble(namespaced_name, adj->get_value() ); + item->updateRepr(); + modmade = true; + } + } + + g_free(namespaced_name); + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), _("Change spiral"), INKSCAPE_ICON("draw-spiral")); + } + + _freeze = false; +} + +void +SpiralToolbar::defaults() +{ + // fixme: make settable + gdouble rev = 3; + gdouble exp = 1.0; + gdouble t0 = 0.0; + + _revolution_adj->set_value(rev); + _expansion_adj->set_value(exp); + _t0_adj->set_value(t0); + + if(_desktop->getCanvas()) _desktop->getCanvas()->grab_focus(); +} + +void +SpiralToolbar::selection_changed(Inkscape::Selection *selection) +{ + int n_selected = 0; + Inkscape::XML::Node *repr = nullptr; + + if ( _repr ) { + _repr->removeListenerByData(this); + GC::release(_repr); + _repr = nullptr; + } + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end(); ++i){ + SPItem *item = *i; + if (SP_IS_SPIRAL(item)) { + n_selected++; + repr = item->getRepr(); + } + } + + if (n_selected == 0) { + _mode_item->set_markup(_("<b>New:</b>")); + } else if (n_selected == 1) { + _mode_item->set_markup(_("<b>Change:</b>")); + + if (repr) { + _repr = repr; + Inkscape::GC::anchor(_repr); + _repr->addListener(&spiral_tb_repr_events, this); + _repr->synthesizeEvents(&spiral_tb_repr_events, this); + } + } else { + // FIXME: implement averaging of all parameters for multiple selected + //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>")); + _mode_item->set_markup(_("<b>Change:</b>")); + } +} + +void +SpiralToolbar::event_attr_changed(Inkscape::XML::Node *repr, + gchar const * /*name*/, + gchar const * /*old_value*/, + gchar const * /*new_value*/, + bool /*is_interactive*/, + gpointer data) +{ + auto toolbar = reinterpret_cast<SpiralToolbar *>(data); + + // quit if run by the _changed callbacks + if (toolbar->_freeze) { + return; + } + + // in turn, prevent callbacks from responding + toolbar->_freeze = true; + + double revolution = repr->getAttributeDouble("sodipodi:revolution", 3.0); + toolbar->_revolution_adj->set_value(revolution); + + double expansion = repr->getAttributeDouble("sodipodi:expansion", 1.0); + toolbar->_expansion_adj->set_value(expansion); + + double t0 = repr->getAttributeDouble("sodipodi:t0", 0.0); + toolbar->_t0_adj->set_value(t0); + + toolbar->_freeze = false; +} + +} +} +} + +/* + 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/toolbar/spiral-toolbar.h b/src/ui/toolbar/spiral-toolbar.h new file mode 100644 index 0000000..9c27eb5 --- /dev/null +++ b/src/ui/toolbar/spiral-toolbar.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SPIRAL_TOOLBAR_H +#define SEEN_SPIRAL_TOOLBAR_H + +/** + * @file + * Spiral aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class ToolButton; +} + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Widget { +class LabelToolItem; +class SpinButtonToolItem; +} + +namespace Toolbar { +class SpiralToolbar : public Toolbar { +private: + UI::Widget::LabelToolItem *_mode_item; + + UI::Widget::SpinButtonToolItem *_revolution_item; + UI::Widget::SpinButtonToolItem *_expansion_item; + UI::Widget::SpinButtonToolItem *_t0_item; + + Gtk::ToolButton *_reset_item; + + Glib::RefPtr<Gtk::Adjustment> _revolution_adj; + Glib::RefPtr<Gtk::Adjustment> _expansion_adj; + Glib::RefPtr<Gtk::Adjustment> _t0_adj; + + bool _freeze; + + XML::Node *_repr; + + void value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, + Glib::ustring const &value_name); + void defaults(); + void selection_changed(Inkscape::Selection *selection); + + std::unique_ptr<sigc::connection> _connection; + +protected: + SpiralToolbar(SPDesktop *desktop); + ~SpiralToolbar() override; + +public: + static GtkWidget * create(SPDesktop *desktop); + + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const *old_value, + gchar const *new_value, + bool is_interactive, + gpointer data); +}; +} +} +} + +#endif /* !SEEN_SPIRAL_TOOLBAR_H */ diff --git a/src/ui/toolbar/spray-toolbar.cpp b/src/ui/toolbar/spray-toolbar.cpp new file mode 100644 index 0000000..de6939a --- /dev/null +++ b/src/ui/toolbar/spray-toolbar.cpp @@ -0,0 +1,541 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Spray aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2015 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spray-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" + +#include "ui/icon-names.h" +#include "ui/simple-pref-pusher.h" + +#include "ui/dialog/clonetiler.h" +#include "ui/dialog/dialog-container.h" +#include "ui/dialog/dialog-base.h" + +#include "ui/widget/canvas.h" +#include "ui/widget/spin-button-tool-item.h" + +// Disabled in 0.91 because of Bug #1274831 (crash, spraying an object +// with the mode: spray object in single path) +// Please enable again when working on 1.0 +#define ENABLE_SPRAY_MODE_SINGLE_PATH + +Inkscape::UI::Dialog::CloneTiler *get_clone_tiler_panel(SPDesktop *desktop) +{ + Inkscape::UI::Dialog::DialogBase *dialog = desktop->getContainer()->get_dialog("CloneTiler"); + if (!dialog) { + desktop->getContainer()->new_dialog("CloneTiler"); + return dynamic_cast<Inkscape::UI::Dialog::CloneTiler *>( + desktop->getContainer()->get_dialog("CloneTiler")); + } + return dynamic_cast<Inkscape::UI::Dialog::CloneTiler *>(dialog); +} + +namespace Inkscape { +namespace UI { +namespace Toolbar { +SprayToolbar::SprayToolbar(SPDesktop *desktop) : + Toolbar(desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /* Mode */ + { + add_label(_("Mode:")); + + Gtk::RadioToolButton::Group mode_group; + + auto copy_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spray with copies"))); + copy_mode_btn->set_tooltip_text(_("Spray copies of the initial selection")); + copy_mode_btn->set_icon_name(INKSCAPE_ICON("spray-mode-copy")); + _mode_buttons.push_back(copy_mode_btn); + + auto clone_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spray with clones"))); + clone_mode_btn->set_tooltip_text(_("Spray clones of the initial selection")); + clone_mode_btn->set_icon_name(INKSCAPE_ICON("spray-mode-clone")); + _mode_buttons.push_back(clone_mode_btn); + +#ifdef ENABLE_SPRAY_MODE_SINGLE_PATH + auto union_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spray single path"))); + union_mode_btn->set_tooltip_text(_("Spray objects in a single path")); + union_mode_btn->set_icon_name(INKSCAPE_ICON("spray-mode-union")); + _mode_buttons.push_back(union_mode_btn); +#endif + + auto eraser_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Delete sprayed items"))); + eraser_mode_btn->set_tooltip_text(_("Delete sprayed items from selection")); + eraser_mode_btn->set_icon_name(INKSCAPE_ICON("draw-eraser")); + _mode_buttons.push_back(eraser_mode_btn); + + int btn_idx = 0; + for (auto btn : _mode_buttons) { + btn->set_sensitive(true); + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::mode_changed), btn_idx++)); + } + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Width */ + std::vector<Glib::ustring> labels = {_("(narrow spray)"), "", "", "", _("(default)"), "", "", "", "", _("(broad spray)")}; + std::vector<double> values = { 1, 3, 5, 10, 15, 20, 30, 50, 75, 100}; + auto width_val = prefs->getDouble("/tools/spray/width", 15); + _width_adj = Gtk::Adjustment::create(width_val, 1, 100, 1.0, 10.0); + auto width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-width", _("Width:"), _width_adj, 1, 0)); + width_item->set_tooltip_text(_("The width of the spray area (relative to the visible canvas area)")); + width_item->set_custom_numeric_menu_data(values, labels); + width_item->set_focus_widget(desktop->canvas); + _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::width_value_changed)); + add(*width_item); + width_item->set_sensitive(true); + } + + /* Use Pressure Width button */ + { + auto pressure_item = add_toggle_button(_("Pressure"), + _("Use the pressure of the input device to alter the width of spray area")); + pressure_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _usepressurewidth_pusher.reset(new UI::SimplePrefPusher(pressure_item, "/tools/spray/usepressurewidth")); + pressure_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + pressure_item, + "/tools/spray/usepressurewidth")); + } + + { /* Population */ + std::vector<Glib::ustring> labels = {_("(low population)"), "", "", "", _("(default)"), "", _("(high population)")}; + std::vector<double> values = { 5, 20, 35, 50, 70, 85, 100}; + auto population_val = prefs->getDouble("/tools/spray/population", 70); + _population_adj = Gtk::Adjustment::create(population_val, 1, 100, 1.0, 10.0); + _spray_population = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-population", _("Amount:"), _population_adj, 1, 0)); + _spray_population->set_tooltip_text(_("Adjusts the number of items sprayed per click")); + _spray_population->set_custom_numeric_menu_data(values, labels); + _spray_population->set_focus_widget(desktop->canvas); + _population_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::population_value_changed)); + add(*_spray_population); + _spray_population->set_sensitive(true); + } + + /* Use Pressure Population button */ + { + auto pressure_population_item = add_toggle_button(_("Pressure"), + _("Use the pressure of the input device to alter the amount of sprayed objects")); + pressure_population_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _usepressurepopulation_pusher.reset(new UI::SimplePrefPusher(pressure_population_item, "/tools/spray/usepressurepopulation")); + pressure_population_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + pressure_population_item, + "/tools/spray/usepressurepopulation")); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { /* Rotation */ + std::vector<Glib::ustring> labels = {_("(default)"), "", "", "", "", "", "", _("(high rotation variation)")}; + std::vector<double> values = { 0, 10, 25, 35, 50, 60, 80, 100}; + auto rotation_val = prefs->getDouble("/tools/spray/rotation_variation", 0); + _rotation_adj = Gtk::Adjustment::create(rotation_val, 0, 100, 1.0, 10.0); + _spray_rotation = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-rotation", _("Rotation:"), _rotation_adj, 1, 0)); + // xgettext:no-c-format + _spray_rotation->set_tooltip_text(_("Variation of the rotation of the sprayed objects; 0% for the same rotation than the original object")); + _spray_rotation->set_custom_numeric_menu_data(values, labels); + _spray_rotation->set_focus_widget(desktop->canvas); + _rotation_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::rotation_value_changed)); + add(*_spray_rotation); + _spray_rotation->set_sensitive(); + } + + { /* Scale */ + std::vector<Glib::ustring> labels = {_("(default)"), "", "", "", "", "", "", _("(high scale variation)")}; + std::vector<double> values = { 0, 10, 25, 35, 50, 60, 80, 100}; + auto scale_val = prefs->getDouble("/tools/spray/scale_variation", 0); + _scale_adj = Gtk::Adjustment::create(scale_val, 0, 100, 1.0, 10.0); + _spray_scale = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-scale", C_("Spray tool", "Scale:"), _scale_adj, 1, 0)); + // xgettext:no-c-format + _spray_scale->set_tooltip_text(_("Variation in the scale of the sprayed objects; 0% for the same scale than the original object")); + _spray_scale->set_custom_numeric_menu_data(values, labels); + _spray_scale->set_focus_widget(desktop->canvas); + _scale_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::scale_value_changed)); + add(*_spray_scale); + _spray_scale->set_sensitive(true); + } + + /* Use Pressure Scale button */ + { + _usepressurescale = add_toggle_button(_("Pressure"), + _("Use the pressure of the input device to alter the scale of new items")); + _usepressurescale->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _usepressurescale->set_active(prefs->getBool("/tools/spray/usepressurescale", false)); + _usepressurescale->signal_toggled().connect(sigc::mem_fun(*this, &SprayToolbar::toggle_pressure_scale)); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Standard_deviation */ + std::vector<Glib::ustring> labels = {_("(minimum scatter)"), "", "", "", "", "", _("(default)"), _("(maximum scatter)")}; + std::vector<double> values = { 1, 5, 10, 20, 30, 50, 70, 100}; + auto sd_val = prefs->getDouble("/tools/spray/standard_deviation", 70); + _sd_adj = Gtk::Adjustment::create(sd_val, 1, 100, 1.0, 10.0); + auto sd_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-standard-deviation", C_("Spray tool", "Scatter:"), _sd_adj, 1, 0)); + sd_item->set_tooltip_text(_("Increase to scatter sprayed objects")); + sd_item->set_custom_numeric_menu_data(values, labels); + sd_item->set_focus_widget(desktop->canvas); + _sd_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::standard_deviation_value_changed)); + add(*sd_item); + sd_item->set_sensitive(true); + } + + { + /* Mean */ + std::vector<Glib::ustring> labels = {_("(default)"), "", "", "", "", "", "", _("(maximum mean)")}; + std::vector<double> values = { 0, 5, 10, 20, 30, 50, 70, 100}; + auto mean_val = prefs->getDouble("/tools/spray/mean", 0); + _mean_adj = Gtk::Adjustment::create(mean_val, 0, 100, 1.0, 10.0); + auto mean_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-mean", _("Focus:"), _mean_adj, 1, 0)); + mean_item->set_tooltip_text(_("0 to spray a spot; increase to enlarge the ring radius")); + mean_item->set_custom_numeric_menu_data(values, labels); + mean_item->set_focus_widget(desktop->canvas); + _mean_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::mean_value_changed)); + add(*mean_item); + mean_item->set_sensitive(true); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Over No Transparent */ + { + _over_no_transparent = add_toggle_button(_("Apply over no transparent areas"), + _("Apply over no transparent areas")); + _over_no_transparent->set_icon_name(INKSCAPE_ICON("object-visible")); + _over_no_transparent->set_active(prefs->getBool("/tools/spray/over_no_transparent", true)); + _over_no_transparent->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _over_no_transparent, + "/tools/spray/over_no_transparent")); + } + + /* Over Transparent */ + { + _over_transparent = add_toggle_button(_("Apply over transparent areas"), + _("Apply over transparent areas")); + _over_transparent->set_icon_name(INKSCAPE_ICON("object-hidden")); + _over_transparent->set_active(prefs->getBool("/tools/spray/over_transparent", true)); + _over_transparent->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _over_transparent, + "/tools/spray/over_transparent")); + } + + /* Pick No Overlap */ + { + _pick_no_overlap = add_toggle_button(_("No overlap between colors"), + _("No overlap between colors")); + _pick_no_overlap->set_icon_name(INKSCAPE_ICON("symbol-bigger")); + _pick_no_overlap->set_active(prefs->getBool("/tools/spray/pick_no_overlap", false)); + _pick_no_overlap->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _pick_no_overlap, + "/tools/spray/pick_no_overlap")); + } + + /* Overlap */ + { + _no_overlap = add_toggle_button(_("Prevent overlapping objects"), + _("Prevent overlapping objects")); + _no_overlap->set_icon_name(INKSCAPE_ICON("distribute-randomize")); + _no_overlap->set_active(prefs->getBool("/tools/spray/no_overlap", false)); + _no_overlap->signal_toggled().connect(sigc::mem_fun(*this, &SprayToolbar::toggle_no_overlap)); + } + + /* Offset */ + { + std::vector<Glib::ustring> labels = {_("(minimum offset)"), "", "", "", _("(default)"), "", "", _("(maximum offset)")}; + std::vector<double> values = { 0, 25, 50, 75, 100, 150, 200, 1000}; + auto offset_val = prefs->getDouble("/tools/spray/offset", 100); + _offset_adj = Gtk::Adjustment::create(offset_val, 0, 1000, 1, 4); + _offset = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-offset", _("Offset %:"), _offset_adj, 0, 0)); + _offset->set_tooltip_text(_("Increase to segregate objects more (value in percent)")); + _offset->set_custom_numeric_menu_data(values, labels); + _offset->set_focus_widget(desktop->canvas); + _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::offset_value_changed)); + add(*_offset); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Picker */ + { + _picker = add_toggle_button(_("Pick color from the drawing. You can use clonetiler trace dialog for advanced effects. In clone mode original fill or stroke colors must be unset."), + _("Pick color from the drawing. You can use clonetiler trace dialog for advanced effects. In clone mode original fill or stroke colors must be unset.")); + _picker->set_icon_name(INKSCAPE_ICON("color-picker")); + _picker->set_active(prefs->getBool("/tools/spray/picker", false)); + _picker->signal_toggled().connect(sigc::mem_fun(*this, &SprayToolbar::toggle_picker)); + } + + /* Pick Fill */ + { + _pick_fill = add_toggle_button(_("Apply picked color to fill"), + _("Apply picked color to fill")); + _pick_fill->set_icon_name(INKSCAPE_ICON("paint-solid")); + _pick_fill->set_active(prefs->getBool("/tools/spray/pick_fill", false)); + _pick_fill->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _pick_fill, + "/tools/spray/pick_fill")); + } + + /* Pick Stroke */ + { + _pick_stroke = add_toggle_button(_("Apply picked color to stroke"), + _("Apply picked color to stroke")); + _pick_stroke->set_icon_name(INKSCAPE_ICON("no-marker")); + _pick_stroke->set_active(prefs->getBool("/tools/spray/pick_stroke", false)); + _pick_stroke->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _pick_stroke, + "/tools/spray/pick_stroke")); + } + + /* Inverse Value Size */ + { + _pick_inverse_value = add_toggle_button(_("Inverted pick value, retaining color in advanced trace mode"), + _("Inverted pick value, retaining color in advanced trace mode")); + _pick_inverse_value->set_icon_name(INKSCAPE_ICON("object-tweak-shrink")); + _pick_inverse_value->set_active(prefs->getBool("/tools/spray/pick_inverse_value", false)); + _pick_inverse_value->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _pick_inverse_value, + "/tools/spray/pick_inverse_value")); + } + + /* Pick from center */ + { + _pick_center = add_toggle_button(_("Pick from center instead of average area."), + _("Pick from center instead of average area.")); + _pick_center->set_icon_name(INKSCAPE_ICON("snap-bounding-box-center")); + _pick_center->set_active(prefs->getBool("/tools/spray/pick_center", true)); + _pick_center->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _pick_center, + "/tools/spray/pick_center")); + } + + gint mode = prefs->getInt("/tools/spray/mode", 1); + _mode_buttons[mode]->set_active(); + show_all(); + init(); +} + +GtkWidget * +SprayToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new SprayToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +SprayToolbar::width_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/width", + _width_adj->get_value()); +} + +void +SprayToolbar::mean_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/mean", + _mean_adj->get_value()); +} + +void +SprayToolbar::standard_deviation_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/standard_deviation", + _sd_adj->get_value()); +} + +void +SprayToolbar::mode_changed(int mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/spray/mode", mode); + init(); +} + +void +SprayToolbar::init(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int mode = prefs->getInt("/tools/spray/mode", 0); + + bool show = true; + if(mode == 3 || mode == 2){ + show = false; + } + _no_overlap->set_visible(show); + _over_no_transparent->set_visible(show); + _over_transparent->set_visible(show); + _pick_no_overlap->set_visible(show); + _pick_stroke->set_visible(show); + _pick_fill->set_visible(show); + _pick_inverse_value->set_visible(show); + _pick_center->set_visible(show); + _picker->set_visible(show); + _offset->set_visible(show); + _pick_fill->set_visible(show); + _pick_stroke->set_visible(show); + _pick_inverse_value->set_visible(show); + _pick_center->set_visible(show); + if(mode == 2){ + show = true; + } + _spray_rotation->set_visible(show); + update_widgets(); +} + +void +SprayToolbar::population_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/population", + _population_adj->get_value()); +} + +void +SprayToolbar::rotation_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/rotation_variation", + _rotation_adj->get_value()); +} + +void +SprayToolbar::update_widgets() +{ + _offset_adj->set_value(100.0); + + bool no_overlap_is_active = _no_overlap->get_active() && _no_overlap->get_visible(); + _offset->set_visible(no_overlap_is_active); + if (_usepressurescale->get_active()) { + _scale_adj->set_value(0.0); + _spray_scale->set_sensitive(false); + } else { + _spray_scale->set_sensitive(true); + } + + bool picker_is_active = _picker->get_active() && _picker->get_visible(); + _pick_fill->set_visible(picker_is_active); + _pick_stroke->set_visible(picker_is_active); + _pick_inverse_value->set_visible(picker_is_active); + _pick_center->set_visible(picker_is_active); +} + +void +SprayToolbar::toggle_no_overlap() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _no_overlap->get_active(); + prefs->setBool("/tools/spray/no_overlap", active); + update_widgets(); +} + +void +SprayToolbar::scale_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/scale_variation", + _scale_adj->get_value()); +} + +void +SprayToolbar::offset_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/offset", + _offset_adj->get_value()); +} + +void +SprayToolbar::toggle_pressure_scale() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _usepressurescale->get_active(); + prefs->setBool("/tools/spray/usepressurescale", active); + if(active){ + prefs->setDouble("/tools/spray/scale_variation", 0); + } + update_widgets(); +} + +void +SprayToolbar::toggle_picker() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _picker->get_active(); + prefs->setBool("/tools/spray/picker", active); + if(active){ + prefs->setBool("/dialogs/clonetiler/dotrace", false); + SPDesktop *dt = _desktop; + if (Inkscape::UI::Dialog::CloneTiler *ct = get_clone_tiler_panel(dt)){ + dt->getContainer()->new_dialog("CloneTiler"); + ct->show_page_trace(); + } + } + update_widgets(); +} + +void +SprayToolbar::on_pref_toggled(Gtk::ToggleToolButton *btn, + const Glib::ustring& path) +{ + auto prefs = Inkscape::Preferences::get(); + bool active = btn->get_active(); + prefs->setBool(path, active); +} + +void +SprayToolbar::set_mode(int mode) +{ + _mode_buttons[mode]->set_active(); +} + +} +} +} + +/* + 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/toolbar/spray-toolbar.h b/src/ui/toolbar/spray-toolbar.h new file mode 100644 index 0000000..4587cf0 --- /dev/null +++ b/src/ui/toolbar/spray-toolbar.h @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SPRAY_TOOLBAR_H +#define SEEN_SPRAY_TOOLBAR_H + +/** + * @file + * Spray aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2015 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class RadioToolButton; +} + +namespace Inkscape { +namespace UI { +class SimplePrefPusher; + +namespace Widget { +class SpinButtonToolItem; +} + +namespace Toolbar { +class SprayToolbar : public Toolbar { +private: + Glib::RefPtr<Gtk::Adjustment> _width_adj; + Glib::RefPtr<Gtk::Adjustment> _mean_adj; + Glib::RefPtr<Gtk::Adjustment> _sd_adj; + Glib::RefPtr<Gtk::Adjustment> _population_adj; + Glib::RefPtr<Gtk::Adjustment> _rotation_adj; + Glib::RefPtr<Gtk::Adjustment> _offset_adj; + Glib::RefPtr<Gtk::Adjustment> _scale_adj; + + std::unique_ptr<SimplePrefPusher> _usepressurewidth_pusher; + std::unique_ptr<SimplePrefPusher> _usepressurepopulation_pusher; + + std::vector<Gtk::RadioToolButton *> _mode_buttons; + UI::Widget::SpinButtonToolItem *_spray_population; + UI::Widget::SpinButtonToolItem *_spray_rotation; + UI::Widget::SpinButtonToolItem *_spray_scale; + Gtk::ToggleToolButton *_usepressurescale; + Gtk::ToggleToolButton *_picker; + Gtk::ToggleToolButton *_pick_center; + Gtk::ToggleToolButton *_pick_inverse_value; + Gtk::ToggleToolButton *_pick_fill; + Gtk::ToggleToolButton *_pick_stroke; + Gtk::ToggleToolButton *_pick_no_overlap; + Gtk::ToggleToolButton *_over_transparent; + Gtk::ToggleToolButton *_over_no_transparent; + Gtk::ToggleToolButton *_no_overlap; + UI::Widget::SpinButtonToolItem *_offset; + + void width_value_changed(); + void mean_value_changed(); + void standard_deviation_value_changed(); + void mode_changed(int mode); + void init(); + void population_value_changed(); + void rotation_value_changed(); + void update_widgets(); + void scale_value_changed(); + void offset_value_changed(); + void on_pref_toggled(Gtk::ToggleToolButton *btn, + const Glib::ustring& path); + void toggle_no_overlap(); + void toggle_pressure_scale(); + void toggle_picker(); + +protected: + SprayToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); + + void set_mode(int mode); +}; +} +} +} + +#endif /* !SEEN_SELECT_TOOLBAR_H */ diff --git a/src/ui/toolbar/star-toolbar.cpp b/src/ui/toolbar/star-toolbar.cpp new file mode 100644 index 0000000..6fc15b6 --- /dev/null +++ b/src/ui/toolbar/star-toolbar.cpp @@ -0,0 +1,569 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Star aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "star-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "selection.h" + +#include "object/sp-star.h" + +#include "ui/icon-names.h" +#include "ui/tools/star-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +static Inkscape::XML::NodeEventVector star_tb_repr_events = +{ + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::StarToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +StarToolbar::StarToolbar(SPDesktop *desktop) : + Toolbar(desktop), + _mode_item(Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>")))), + _repr(nullptr), + _freeze(false) +{ + _mode_item->set_use_markup(true); + add(*_mode_item); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool isFlatSided = prefs->getBool("/tools/shapes/star/isflatsided", false); + + /* Flatsided checkbox */ + { + Gtk::RadioToolButton::Group flat_item_group; + + auto flat_polygon_button = Gtk::manage(new Gtk::RadioToolButton(flat_item_group, _("Polygon"))); + flat_polygon_button->set_tooltip_text(_("Regular polygon (with one handle) instead of a star")); + flat_polygon_button->set_icon_name(INKSCAPE_ICON("draw-polygon")); + _flat_item_buttons.push_back(flat_polygon_button); + + auto flat_star_button = Gtk::manage(new Gtk::RadioToolButton(flat_item_group, _("Star"))); + flat_star_button->set_tooltip_text(_("Star instead of a regular polygon (with one handle)")); + flat_star_button->set_icon_name(INKSCAPE_ICON("draw-star")); + _flat_item_buttons.push_back(flat_star_button); + + _flat_item_buttons[ isFlatSided ? 0 : 1 ]->set_active(); + + int btn_index = 0; + + for (auto btn : _flat_item_buttons) + { + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &StarToolbar::side_mode_changed), btn_index++)); + } + } + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Magnitude */ + { + std::vector<Glib::ustring> labels = {"", + _("triangle/tri-star"), + _("square/quad-star"), + _("pentagon/five-pointed star"), + _("hexagon/six-pointed star"), + "", + "", + "", + "", + ""}; + std::vector<double> values = {2, 3, 4, 5, 6, 7, 8, 10, 12, 20}; + auto magnitude_val = prefs->getDouble("/tools/shapes/star/magnitude", 3); + _magnitude_adj = Gtk::Adjustment::create(magnitude_val, isFlatSided ? 3 : 2, 1024, 1, 5); + _magnitude_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-magnitude", _("Corners:"), _magnitude_adj, 1.0, 0)); + _magnitude_item->set_tooltip_text(_("Number of corners of a polygon or star")); + _magnitude_item->set_custom_numeric_menu_data(values, labels); + _magnitude_item->set_focus_widget(desktop->canvas); + _magnitude_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::magnitude_value_changed)); + _magnitude_item->set_sensitive(true); + add(*_magnitude_item); + } + + /* Spoke ratio */ + { + std::vector<Glib::ustring> labels = {_("thin-ray star"), "", _("pentagram"), _("hexagram"), _("heptagram"), _("octagram"), _("regular polygon")}; + std::vector<double> values = { 0.01, 0.2, 0.382, 0.577, 0.692, 0.765, 1}; + auto prop_val = prefs->getDouble("/tools/shapes/star/proportion", 0.5); + _spoke_adj = Gtk::Adjustment::create(prop_val, 0.01, 1.0, 0.01, 0.1); + _spoke_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-spoke", _("Spoke ratio:"), _spoke_adj)); + // TRANSLATORS: Tip radius of a star is the distance from the center to the farthest handle. + // Base radius is the same for the closest handle. + _spoke_item->set_tooltip_text(_("Base radius to tip radius ratio")); + _spoke_item->set_custom_numeric_menu_data(values, labels); + _spoke_item->set_focus_widget(desktop->canvas); + _spoke_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::proportion_value_changed)); + + add(*_spoke_item); + } + + /* Roundedness */ + { + std::vector<Glib::ustring> labels = {_("stretched"), _("twisted"), _("slightly pinched"), _("NOT rounded"), _("slightly rounded"), + _("visibly rounded"), _("well rounded"), _("amply rounded"), "", _("stretched"), _("blown up")}; + std::vector<double> values = {-1, -0.2, -0.03, 0, 0.05, 0.1, 0.2, 0.3, 0.5, 1, 10}; + auto roundedness_val = prefs->getDouble("/tools/shapes/star/rounded", 0.0); + _roundedness_adj = Gtk::Adjustment::create(roundedness_val, -10.0, 10.0, 0.01, 0.1); + _roundedness_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-roundedness", _("Rounded:"), _roundedness_adj)); + _roundedness_item->set_tooltip_text(_("How rounded are the corners (0 for sharp)")); + _roundedness_item->set_custom_numeric_menu_data(values, labels); + _roundedness_item->set_focus_widget(desktop->canvas); + _roundedness_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::rounded_value_changed)); + _roundedness_item->set_sensitive(true); + add(*_roundedness_item); + } + + /* Randomization */ + { + std::vector<Glib::ustring> labels = {_("NOT randomized"), _("slightly irregular"), _("visibly randomized"), _("strongly randomized"), _("blown up")}; + std::vector<double> values = { 0, 0.01, 0.1, 0.5, 10}; + auto randomized_val = prefs->getDouble("/tools/shapes/star/randomized", 0.0); + _randomization_adj = Gtk::Adjustment::create(randomized_val, -10.0, 10.0, 0.001, 0.01); + _randomization_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-randomized", _("Randomized:"), _randomization_adj, 0.1, 3)); + _randomization_item->set_tooltip_text(_("Scatter randomly the corners and angles")); + _randomization_item->set_custom_numeric_menu_data(values, labels); + _randomization_item->set_focus_widget(desktop->canvas); + _randomization_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::randomized_value_changed)); + _randomization_item->set_sensitive(true); + add(*_randomization_item); + } + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Reset */ + { + _reset_item = Gtk::manage(new Gtk::ToolButton(_("Defaults"))); + _reset_item->set_icon_name(INKSCAPE_ICON("edit-clear")); + _reset_item->set_tooltip_text(_("Reset shape parameters to defaults (use Inkscape Preferences > Tools to change defaults)")); + _reset_item->signal_clicked().connect(sigc::mem_fun(*this, &StarToolbar::defaults)); + _reset_item->set_sensitive(true); + add(*_reset_item); + } + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &StarToolbar::watch_ec)); + + show_all(); + _spoke_item->set_visible(!isFlatSided); +} + +StarToolbar::~StarToolbar() +{ + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } +} + +GtkWidget * +StarToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new StarToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +StarToolbar::side_mode_changed(int mode) +{ + bool flat = (mode == 0); + + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool( "/tools/shapes/star/isflatsided", flat ); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + Inkscape::Selection *selection = _desktop->getSelection(); + bool modmade = false; + + if (_spoke_item) { + _spoke_item->set_visible(!flat); + } + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + if (flat) { + gint sides = (gint)_magnitude_adj->get_value(); + if (sides < 3) { + repr->setAttributeInt("sodipodi:sides", 3); + } + } + repr->setAttribute("inkscape:flatsided", flat ? "true" : "false" ); + + item->updateRepr(); + modmade = true; + } + } + + _magnitude_adj->set_lower(flat ? 3 : 2); + if (flat && _magnitude_adj->get_value() < 3) { + _magnitude_adj->set_value(3); + } + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), flat ? _("Make polygon") : _("Make star"), INKSCAPE_ICON("draw-polygon-star")); + } + + _freeze = false; +} + +void +StarToolbar::magnitude_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + // do not remember prefs if this call is initiated by an undo change, because undoing object + // creation sets bogus values to its attributes before it is deleted + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/shapes/star/magnitude", + (gint)_magnitude_adj->get_value()); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + repr->setAttributeInt("sodipodi:sides", (gint)_magnitude_adj->get_value()); + double arg1 = repr->getAttributeDouble("sodipodi:arg1", 0.5); + repr->setAttributeSvgDouble("sodipodi:arg2", (arg1 + M_PI / (gint)_magnitude_adj->get_value())); + item->updateRepr(); + modmade = true; + } + } + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), _("Star: Change number of corners"), INKSCAPE_ICON("draw-polygon-star")); + } + + _freeze = false; +} + +void +StarToolbar::proportion_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + if (!std::isnan(_spoke_adj->get_value())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/shapes/star/proportion", + _spoke_adj->get_value()); + } + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + + gdouble r1 = repr->getAttributeDouble("sodipodi:r1", 1.0);; + gdouble r2 = repr->getAttributeDouble("sodipodi:r2", 1.0); + + if (r2 < r1) { + repr->setAttributeSvgDouble("sodipodi:r2", r1*_spoke_adj->get_value()); + } else { + repr->setAttributeSvgDouble("sodipodi:r1", r2*_spoke_adj->get_value()); + } + + item->updateRepr(); + modmade = true; + } + } + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), _("Star: Change spoke ratio"), INKSCAPE_ICON("draw-polygon-star")); + } + + _freeze = false; +} + +void +StarToolbar::rounded_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/shapes/star/rounded", (gdouble) _roundedness_adj->get_value()); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + repr->setAttributeSvgDouble("inkscape:rounded", (gdouble) _roundedness_adj->get_value()); + item->updateRepr(); + modmade = true; + } + } + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), _("Star: Change rounding"), INKSCAPE_ICON("draw-polygon-star")); + } + + _freeze = false; +} + +void +StarToolbar::randomized_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/shapes/star/randomized", + (gdouble) _randomization_adj->get_value()); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + repr->setAttributeSvgDouble("inkscape:randomized", (gdouble) _randomization_adj->get_value()); + item->updateRepr(); + modmade = true; + } + } + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), _("Star: Change randomization"), INKSCAPE_ICON("draw-polygon-star")); + } + + _freeze = false; +} + +void +StarToolbar::defaults() +{ + + // FIXME: in this and all other _default functions, set some flag telling the value_changed + // callbacks to lump all the changes for all selected objects in one undo step + + // fixme: make settable in prefs! + gint mag = 5; + gdouble prop = 0.5; + gboolean flat = FALSE; + gdouble randomized = 0; + gdouble rounded = 0; + + _flat_item_buttons[ flat ? 0 : 1 ]->set_active(); + + _spoke_item->set_visible(!flat); + + _magnitude_adj->set_value(mag); + _spoke_adj->set_value(prop); + _roundedness_adj->set_value(rounded); + _randomization_adj->set_value(randomized); +} + +void +StarToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (dynamic_cast<Inkscape::UI::Tools::StarTool const*>(ec) != nullptr) { + _changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &StarToolbar::selection_changed)); + selection_changed(desktop->getSelection()); + } else { + if (_changed) + _changed.disconnect(); + } +} + +/** + * \param selection Should not be NULL. + */ +void +StarToolbar::selection_changed(Inkscape::Selection *selection) +{ + int n_selected = 0; + Inkscape::XML::Node *repr = nullptr; + + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + n_selected++; + repr = item->getRepr(); + } + } + + if (n_selected == 0) { + _mode_item->set_markup(_("<b>New:</b>")); + } else if (n_selected == 1) { + _mode_item->set_markup(_("<b>Change:</b>")); + + if (repr) { + _repr = repr; + Inkscape::GC::anchor(_repr); + _repr->addListener(&star_tb_repr_events, this); + _repr->synthesizeEvents(&star_tb_repr_events, this); + } + } else { + // FIXME: implement averaging of all parameters for multiple selected stars + //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>")); + //gtk_label_set_markup(GTK_LABEL(l), _("<b>Change:</b>")); + } +} + +void +StarToolbar::event_attr_changed(Inkscape::XML::Node *repr, gchar const *name, + gchar const * /*old_value*/, gchar const * /*new_value*/, + bool /*is_interactive*/, gpointer dataPointer) +{ + auto toolbar = reinterpret_cast<StarToolbar *>(dataPointer); + + // quit if run by the _changed callbacks + if (toolbar->_freeze) { + return; + } + + // in turn, prevent callbacks from responding + toolbar->_freeze = true; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool isFlatSided = prefs->getBool("/tools/shapes/star/isflatsided", false); + + if (!strcmp(name, "inkscape:randomized")) { + double randomized = repr->getAttributeDouble("inkscape:randomized", 0.0); + toolbar->_randomization_adj->set_value(randomized); + } else if (!strcmp(name, "inkscape:rounded")) { + double rounded = repr->getAttributeDouble("inkscape:rounded", 0.0); + toolbar->_roundedness_adj->set_value(rounded); + } else if (!strcmp(name, "inkscape:flatsided")) { + char const *flatsides = repr->attribute("inkscape:flatsided"); + if ( flatsides && !strcmp(flatsides,"false") ) { + toolbar->_flat_item_buttons[1]->set_active(); + toolbar->_spoke_item->set_visible(true); + toolbar->_magnitude_adj->set_lower(2); + } else { + toolbar->_flat_item_buttons[0]->set_active(); + toolbar->_spoke_item->set_visible(false); + toolbar->_magnitude_adj->set_lower(3); + } + } else if ((!strcmp(name, "sodipodi:r1") || !strcmp(name, "sodipodi:r2")) && (!isFlatSided) ) { + gdouble r1 = repr->getAttributeDouble("sodipodi:r1", 1.0); + gdouble r2 = repr->getAttributeDouble("sodipodi:r2", 1.0); + + + if (r2 < r1) { + toolbar->_spoke_adj->set_value(r2/r1); + } else { + toolbar->_spoke_adj->set_value(r1/r2); + } + } else if (!strcmp(name, "sodipodi:sides")) { + int sides = repr->getAttributeInt("sodipodi:sides", 0); + toolbar->_magnitude_adj->set_value(sides); + } + + toolbar->_freeze = false; +} + +} +} +} + +/* + 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/toolbar/star-toolbar.h b/src/ui/toolbar/star-toolbar.h new file mode 100644 index 0000000..c44caab --- /dev/null +++ b/src/ui/toolbar/star-toolbar.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_STAR_TOOLBAR_H +#define SEEN_STAR_TOOLBAR_H + +/** + * @file + * Star aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class RadioToolButton; +class ToolButton; +} + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class LabelToolItem; +class SpinButtonToolItem; +} + +namespace Toolbar { +class StarToolbar : public Toolbar { +private: + UI::Widget::LabelToolItem *_mode_item; + std::vector<Gtk::RadioToolButton *> _flat_item_buttons; + UI::Widget::SpinButtonToolItem *_magnitude_item; + UI::Widget::SpinButtonToolItem *_spoke_item; + UI::Widget::SpinButtonToolItem *_roundedness_item; + UI::Widget::SpinButtonToolItem *_randomization_item; + Gtk::ToolButton *_reset_item; + + XML::Node *_repr; + + Glib::RefPtr<Gtk::Adjustment> _magnitude_adj; + Glib::RefPtr<Gtk::Adjustment> _spoke_adj; + Glib::RefPtr<Gtk::Adjustment> _roundedness_adj; + Glib::RefPtr<Gtk::Adjustment> _randomization_adj; + + bool _freeze; + sigc::connection _changed; + + void side_mode_changed(int mode); + void magnitude_value_changed(); + void proportion_value_changed(); + void rounded_value_changed(); + void randomized_value_changed(); + void defaults(); + void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_changed(Inkscape::Selection *selection); + +protected: + StarToolbar(SPDesktop *desktop); + ~StarToolbar() override; + +public: + static GtkWidget * create(SPDesktop *desktop); + + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const *old_value, + gchar const *new_value, + bool is_interactive, + gpointer dataPointer); +}; + +} +} +} + +#endif /* !SEEN_SELECT_TOOLBAR_H */ diff --git a/src/ui/toolbar/text-toolbar.cpp b/src/ui/toolbar/text-toolbar.cpp new file mode 100644 index 0000000..7a6144e --- /dev/null +++ b/src/ui/toolbar/text-toolbar.cpp @@ -0,0 +1,2576 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Text aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 1999-2013 authors + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include "text-toolbar.h" + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "selection-chemistry.h" + +#include "libnrtype/font-lister.h" + +#include "object/sp-flowdiv.h" +#include "object/sp-flowtext.h" +#include "object/sp-root.h" +#include "object/sp-text.h" +#include "object/sp-tspan.h" +#include "object/sp-string.h" + +#include "svg/css-ostringstream.h" +#include "ui/icon-names.h" +#include "ui/tools/select-tool.h" +#include "ui/tools/text-tool.h" +#include "ui/widget/canvas.h" // Focus +#include "ui/widget/combo-box-entry-tool-item.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" +#include "util/units.h" + +#include "widgets/style-utils.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::Util::unit_table; +using Inkscape::UI::Widget::UnitTracker; + +//#define DEBUG_TEXT + +//######################## +//## Text Toolbox ## +//######################## + +// Functions for debugging: +#ifdef DEBUG_TEXT +static void sp_print_font(SPStyle *query) +{ + + + bool family_set = query->font_family.set; + bool style_set = query->font_style.set; + bool fontspec_set = query->font_specification.set; + + std::cout << " Family set? " << family_set + << " Style set? " << style_set + << " FontSpec set? " << fontspec_set + << std::endl; +} + +static void sp_print_fontweight( SPStyle *query ) { + const gchar* names[] = {"100", "200", "300", "400", "500", "600", "700", "800", "900", + "NORMAL", "BOLD", "LIGHTER", "BOLDER", "Out of range"}; + // Missing book = 380 + int index = query->font_weight.computed; + if (index < 0 || index > 13) + index = 13; + std::cout << " Weight: " << names[ index ] + << " (" << query->font_weight.computed << ")" << std::endl; +} + +static void sp_print_fontstyle( SPStyle *query ) { + + const gchar* names[] = {"NORMAL", "ITALIC", "OBLIQUE", "Out of range"}; + int index = query->font_style.computed; + if( index < 0 || index > 3 ) index = 3; + std::cout << " Style: " << names[ index ] << std::endl; + +} +#endif + +static bool is_relative( Unit const *unit ) { + return (unit->abbr == "" || unit->abbr == "em" || unit->abbr == "ex" || unit->abbr == "%"); +} + +static bool is_relative(SPCSSUnit const unit) +{ + return (unit == SP_CSS_UNIT_NONE || unit == SP_CSS_UNIT_EM || unit == SP_CSS_UNIT_EX || + unit == SP_CSS_UNIT_PERCENT); +} + +// Set property for object, but unset all descendents +// Should probably be moved to desktop_style.cpp +static void recursively_set_properties(SPObject *object, SPCSSAttr *css, bool unset_descendents = true) +{ + object->changeCSS (css, "style"); + + SPCSSAttr *css_unset = sp_repr_css_attr_unset_all( css ); + std::vector<SPObject *> children = object->childList(false); + for (auto i: children) { + recursively_set_properties(i, unset_descendents ? css_unset : css); + } + sp_repr_css_attr_unref (css_unset); +} + +/* + * Set the default list of font sizes, scaled to the users preferred unit + */ +static void sp_text_set_sizes(GtkListStore* model_size, int unit) +{ + gtk_list_store_clear(model_size); + + // List of font sizes for dropchange-down menu + int sizes[] = { + 4, 6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28, + 32, 36, 40, 48, 56, 64, 72, 144 + }; + + // Array must be same length as SPCSSUnit in style.h + float ratios[] = {1, 1, 1, 10, 4, 40, 100, 16, 8, 0.16}; + + for(int i : sizes) { + GtkTreeIter iter; + Glib::ustring size = Glib::ustring::format(i / (float)ratios[unit]); + gtk_list_store_append( model_size, &iter ); + gtk_list_store_set( model_size, &iter, 0, size.c_str(), -1 ); + } +} + + +// TODO: possibly share with font-selector by moving most code to font-lister (passing family name) +static void sp_text_toolbox_select_cb( GtkEntry* entry, GtkEntryIconPosition /*position*/, GdkEvent /*event*/, gpointer /*data*/ ) { + + Glib::ustring family = gtk_entry_get_text ( entry ); + //std::cout << "text_toolbox_missing_font_cb: selecting: " << family << std::endl; + + // Get all items with matching font-family set (not inherited!). + std::vector<SPItem*> selectList; + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPDocument *document = desktop->getDocument(); + std::vector<SPItem*> x,y; + std::vector<SPItem*> allList = get_all_items(x, document->getRoot(), desktop, false, false, true, y); + for(std::vector<SPItem*>::const_reverse_iterator i=allList.rbegin();i!=allList.rend(); ++i){ + SPItem *item = *i; + SPStyle *style = item->style; + + if (style) { + + Glib::ustring family_style; + if (style->font_family.set) { + family_style = style->font_family.value(); + //std::cout << " family style from font_family: " << family_style << std::endl; + } + else if (style->font_specification.set) { + family_style = style->font_specification.value(); + //std::cout << " family style from font_spec: " << family_style << std::endl; + } + + if (family_style.compare( family ) == 0 ) { + //std::cout << " found: " << item->getId() << std::endl; + selectList.push_back(item); + } + } + } + + // Update selection + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + //std::cout << " list length: " << g_slist_length ( selectList ) << std::endl; + selection->setList(selectList); +} + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +TextToolbar::TextToolbar(SPDesktop *desktop) + : Toolbar(desktop) + , _freeze(false) + , _text_style_from_prefs(false) + , _outer(true) + , _updating(false) + , _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)) + , _tracker_fs(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)) + , _cusor_numbers(0) +{ + /* Line height unit tracker */ + _tracker->prependUnit(unit_table.getUnit("")); // Ratio + _tracker->addUnit(unit_table.getUnit("%")); + _tracker->addUnit(unit_table.getUnit("em")); + _tracker->addUnit(unit_table.getUnit("ex")); + _tracker->setActiveUnit(unit_table.getUnit("")); + // We change only the display value + _tracker->changeLabel("lines", 0, true); + _tracker_fs->setActiveUnit(unit_table.getUnit("mm")); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /* Font family */ + { + // Font list + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); + fontlister->update_font_list(desktop->getDocument()); + Glib::RefPtr<Gtk::ListStore> store = fontlister->get_font_list(); + GtkListStore* model = store->gobj(); + + _font_family_item = + Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontFamilyAction", + _("Font Family"), + _("Select Font Family (Alt-X to access)"), + GTK_TREE_MODEL(model), + -1, // Entry width + 50, // Extra list width + (gpointer)font_lister_cell_data_func2, // Cell layout + (gpointer)font_lister_separator_func2, + GTK_WIDGET(desktop->getCanvas()->gobj()))); // Focus widget + _font_family_item->popup_enable(); // Enable entry completion + gchar *const info = _("Select all text with this font-family"); + _font_family_item->set_info( info ); // Show selection icon + _font_family_item->set_info_cb( (gpointer)sp_text_toolbox_select_cb ); + + gchar *const warning = _("Font not found on system"); + _font_family_item->set_warning( warning ); // Show icon w/ tooltip if font missing + _font_family_item->set_warning_cb( (gpointer)sp_text_toolbox_select_cb ); + + //ink_comboboxentry_action_set_warning_callback( act, sp_text_fontfamily_select_all ); + _font_family_item->signal_changed().connect( sigc::mem_fun(*this, &TextToolbar::fontfamily_value_changed) ); + add(*_font_family_item); + + // Change style of drop-down from menu to list + auto css_provider = gtk_css_provider_new(); + gtk_css_provider_load_from_data(css_provider, + "#TextFontFamilyAction_combobox {\n" + " -GtkComboBox-appears-as-list: true;\n" + "}\n", + -1, nullptr); + + auto screen = gdk_screen_get_default(); + _font_family_item->focus_on_click(false); + gtk_style_context_add_provider_for_screen(screen, + GTK_STYLE_PROVIDER(css_provider), + GTK_STYLE_PROVIDER_PRIORITY_USER); + } + + /* Font styles */ + { + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); + Glib::RefPtr<Gtk::ListStore> store = fontlister->get_style_list(); + GtkListStore* model_style = store->gobj(); + + _font_style_item = + Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontStyleAction", + _("Font Style"), + _("Font style"), + GTK_TREE_MODEL(model_style), + 12, // Width in characters + 0, // Extra list width + nullptr, // Cell layout + nullptr, // Separator + GTK_WIDGET(desktop->getCanvas()->gobj()))); // Focus widget + + _font_style_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::fontstyle_value_changed)); + _font_style_item->focus_on_click(false); + add(*_font_style_item); + } + + add_separator(); + + /* Font size */ + { + // List of font sizes for drop-down menu + GtkListStore* model_size = gtk_list_store_new( 1, G_TYPE_STRING ); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + + sp_text_set_sizes(model_size, unit); + + auto unit_str = sp_style_get_css_unit_string(unit); + Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", unit_str, ")"); + + _font_size_item = + Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontSizeAction", + _("Font Size"), + tooltip, + GTK_TREE_MODEL(model_size), + 8, // Width in characters + 0, // Extra list width + nullptr, // Cell layout + nullptr, // Separator + GTK_WIDGET(desktop->getCanvas()->gobj()))); // Focus widget + + _font_size_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::fontsize_value_changed)); + _font_size_item->focus_on_click(false); + add(*_font_size_item); + } + /* Font_ size units */ + { + _font_size_units_item = _tracker_fs->create_tool_item(_("Units"), ("")); + _font_size_units_item->signal_changed_after().connect( + sigc::mem_fun(*this, &TextToolbar::fontsize_unit_changed)); + _font_size_units_item->focus_on_click(false); + add(*_font_size_units_item); + } + { + // Drop down menu + std::vector<Glib::ustring> labels = {_("Smaller spacing"), "", "", "", "", C_("Text tool", "Normal"), "", "", "", "", "", _("Larger spacing")}; + std::vector<double> values = { 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 2.0}; + + auto line_height_val = 1.25; + _line_height_adj = Gtk::Adjustment::create(line_height_val, 0.0, 1000.0, 0.1, 1.0); + _line_height_item = + Gtk::manage(new UI::Widget::SpinButtonToolItem("text-line-height", "", _line_height_adj, 0.1, 2)); + _line_height_item->set_tooltip_text(_("Spacing between baselines")); + _line_height_item->set_custom_numeric_menu_data(values, labels); + _line_height_item->set_focus_widget(desktop->getCanvas()); + _line_height_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::lineheight_value_changed)); + //_tracker->addAdjustment(_line_height_adj->gobj()); // (Alex V) Why is this commented out? + _line_height_item->set_sensitive(true); + _line_height_item->set_icon(INKSCAPE_ICON("text_line_spacing")); + add(*_line_height_item); + } + /* Line height units */ + { + _line_height_units_item = _tracker->create_tool_item( _("Units"), ("")); + _line_height_units_item->signal_changed_after().connect(sigc::mem_fun(*this, &TextToolbar::lineheight_unit_changed)); + _line_height_units_item->focus_on_click(false); + add(*_line_height_units_item); + } + + /* Alignment */ + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("Align left"); + row[columns.col_tooltip ] = _("Align left"); + row[columns.col_icon ] = INKSCAPE_ICON("format-justify-left"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Align center"); + row[columns.col_tooltip ] = _("Align center"); + row[columns.col_icon ] = INKSCAPE_ICON("format-justify-center"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Align right"); + row[columns.col_tooltip ] = _("Align right"); + row[columns.col_icon ] = INKSCAPE_ICON("format-justify-right"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Justify"); + row[columns.col_tooltip ] = _("Justify (only flowed text)"); + row[columns.col_icon ] = INKSCAPE_ICON("format-justify-fill"); + row[columns.col_sensitive] = false; + + _align_item = + UI::Widget::ComboToolItem::create(_("Alignment"), // Label + _("Text alignment"), // Tooltip + "Not Used", // Icon + store ); // Tree store + _align_item->use_icon( true ); + _align_item->use_label( false ); + gint mode = prefs->getInt("/tools/text/align_mode", 0); + _align_item->set_active( mode ); + + add(*_align_item); + _align_item->focus_on_click(false); + _align_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::align_mode_changed)); + } + + /* Style - Superscript */ + { + _superscript_item = Gtk::manage(new Gtk::ToggleToolButton()); + _superscript_item->set_label(_("Toggle superscript")); + _superscript_item->set_tooltip_text(_("Toggle superscript")); + _superscript_item->set_icon_name(INKSCAPE_ICON("text_superscript")); + _superscript_item->set_name("text-superscript"); + add(*_superscript_item); + _superscript_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &TextToolbar::script_changed), _superscript_item)); + _superscript_item->set_active(prefs->getBool("/tools/text/super", false)); + } + + /* Style - Subscript */ + { + _subscript_item = Gtk::manage(new Gtk::ToggleToolButton()); + _subscript_item->set_label(_("Toggle subscript")); + _subscript_item->set_tooltip_text(_("Toggle subscript")); + _subscript_item->set_icon_name(INKSCAPE_ICON("text_subscript")); + _subscript_item->set_name("text-subscript"); + add(*_subscript_item); + _subscript_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &TextToolbar::script_changed), _subscript_item)); + _subscript_item->set_active(prefs->getBool("/tools/text/sub", false)); + } + + /* Character positioning popover */ + + auto positioning_item = Gtk::manage(new Gtk::ToolItem); + add(*positioning_item); + + auto positioning_button = Gtk::manage(new Gtk::MenuButton); + positioning_button->set_image_from_icon_name(INKSCAPE_ICON("text_horz_kern")); + positioning_button->set_always_show_image(true); + positioning_button->set_tooltip_text(_("Kerning, word spacing, character positioning")); + positioning_button->set_label(_("Spacing")); + positioning_item->add(*positioning_button); + + auto positioning_popover = Gtk::manage(new Gtk::Popover(*positioning_button)); + positioning_popover->set_modal(false); // Stay open until button clicked again. + positioning_button->set_popover(*positioning_popover); + + auto positioning_grid = Gtk::manage(new Gtk::Grid); + positioning_popover->add(*positioning_grid); + + + /* Letter spacing */ + { + // Drop down menu + std::vector<Glib::ustring> labels = {_("Negative spacing"), "", "", "", C_("Text tool", "Normal"), "", "", "", "", "", "", "", _("Positive spacing")}; + std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0}; + auto letter_spacing_val = prefs->getDouble("/tools/text/letterspacing", 0.0); + _letter_spacing_adj = Gtk::Adjustment::create(letter_spacing_val, -1000.0, 1000.0, 0.01, 0.10); + _letter_spacing_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-letter-spacing", _("Letter:"), _letter_spacing_adj, 0.1, 2)); + _letter_spacing_item->set_tooltip_text(_("Spacing between letters (px)")); + _letter_spacing_item->set_custom_numeric_menu_data(values, labels); + _letter_spacing_item->set_focus_widget(desktop->getCanvas()); + _letter_spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::letterspacing_value_changed)); + _letter_spacing_item->set_sensitive(true); + _letter_spacing_item->set_icon(INKSCAPE_ICON("text_letter_spacing")); + + positioning_grid->attach(*_letter_spacing_item, 0, 0); + } + + /* Word spacing */ + { + // Drop down menu + std::vector<Glib::ustring> labels = {_("Negative spacing"), "", "", "", C_("Text tool", "Normal"), "", "", "", "", "", "", "", _("Positive spacing")}; + std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0}; + auto word_spacing_val = prefs->getDouble("/tools/text/wordspacing", 0.0); + _word_spacing_adj = Gtk::Adjustment::create(word_spacing_val, -1000.0, 1000.0, 0.01, 0.10); + _word_spacing_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-word-spacing", _("Word:"), _word_spacing_adj, 0.1, 2)); + _word_spacing_item->set_tooltip_text(_("Spacing between words (px)")); + _word_spacing_item->set_custom_numeric_menu_data(values, labels); + _word_spacing_item->set_focus_widget(desktop->getCanvas()); + _word_spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::wordspacing_value_changed)); + _word_spacing_item->set_sensitive(true); + _word_spacing_item->set_icon(INKSCAPE_ICON("text_word_spacing")); + + positioning_grid->attach(*_word_spacing_item, 1, 0); + } + + /* Character kerning (horizontal shift) */ + { + // Drop down menu + std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5 }; + auto dx_val = prefs->getDouble("/tools/text/dx", 0.0); + _dx_adj = Gtk::Adjustment::create(dx_val, -1000.0, 1000.0, 0.01, 0.1); + _dx_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-dx", _("Kern:"), _dx_adj, 0.1, 2)); + _dx_item->set_custom_numeric_menu_data(values); + _dx_item->set_tooltip_text(_("Horizontal kerning (px)")); + _dx_item->set_focus_widget(desktop->getCanvas()); + _dx_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::dx_value_changed)); + _dx_item->set_sensitive(true); + _dx_item->set_icon(INKSCAPE_ICON("text_horz_kern")); + + positioning_grid->attach(*_dx_item, 0, 1); + } + + /* Character vertical shift */ + { + // Drop down menu + std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5 }; + auto dy_val = prefs->getDouble("/tools/text/dy", 0.0); + _dy_adj = Gtk::Adjustment::create(dy_val, -1000.0, 1000.0, 0.01, 0.1); + _dy_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-dy", _("Vert:"), _dy_adj, 0.1, 2)); + _dy_item->set_tooltip_text(_("Vertical kerning (px)")); + _dy_item->set_custom_numeric_menu_data(values); + _dy_item->set_focus_widget(desktop->getCanvas()); + _dy_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::dy_value_changed)); + _dy_item->set_sensitive(true); + _dy_item->set_icon(INKSCAPE_ICON("text_vert_kern")); + + positioning_grid->attach(*_dy_item, 1, 1); + } + + /* Character rotation */ + { + std::vector<double> values = { -90, -45, -30, -15, 0, 15, 30, 45, 90, 180 }; + auto rotation_val = prefs->getDouble("/tools/text/rotation", 0.0); + _rotation_adj = Gtk::Adjustment::create(rotation_val, -180.0, 180.0, 0.1, 1.0); + _rotation_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-rotation", _("Rot:"), _rotation_adj, 0.1, 2)); + _rotation_item->set_tooltip_text(_("Character rotation (degrees)")); + _rotation_item->set_custom_numeric_menu_data(values); + _rotation_item->set_focus_widget(desktop->getCanvas()); + _rotation_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::rotation_value_changed)); + _rotation_item->set_sensitive(); + _rotation_item->set_icon(INKSCAPE_ICON("text_rotation")); + + positioning_grid->attach(*_rotation_item, 2, 1); + } + + positioning_grid->show_all(); + + /* Writing mode (Horizontal, Vertical-LR, Vertical-RL) */ + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("Horizontal"); + row[columns.col_tooltip ] = _("Horizontal text"); + row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-horizontal"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Vertical — RL"); + row[columns.col_tooltip ] = _("Vertical text — lines: right to left"); + row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-vertical"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Vertical — LR"); + row[columns.col_tooltip ] = _("Vertical text — lines: left to right"); + row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-vertical-lr"); + row[columns.col_sensitive] = true; + + _writing_mode_item = + UI::Widget::ComboToolItem::create( _("Writing mode"), // Label + _("Block progression"), // Tooltip + "Not Used", // Icon + store ); // Tree store + _writing_mode_item->use_icon(true); + _writing_mode_item->use_label( false ); + gint mode = prefs->getInt("/tools/text/writing_mode", 0); + _writing_mode_item->set_active( mode ); + add(*_writing_mode_item); + _writing_mode_item->focus_on_click(false); + _writing_mode_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::writing_mode_changed)); + } + + + /* Text (glyph) orientation (Auto (mixed), Upright, Sideways) */ + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("Auto"); + row[columns.col_tooltip ] = _("Auto glyph orientation"); + row[columns.col_icon ] = INKSCAPE_ICON("text-orientation-auto"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Upright"); + row[columns.col_tooltip ] = _("Upright glyph orientation"); + row[columns.col_icon ] = INKSCAPE_ICON("text-orientation-upright"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Sideways"); + row[columns.col_tooltip ] = _("Sideways glyph orientation"); + row[columns.col_icon ] = INKSCAPE_ICON("text-orientation-sideways"); + row[columns.col_sensitive] = true; + + _orientation_item = + UI::Widget::ComboToolItem::create(_("Text orientation"), // Label + _("Text (glyph) orientation in vertical text."), // Tooltip + "Not Used", // Icon + store ); // List store + _orientation_item->use_icon(true); + _orientation_item->use_label(false); + gint mode = prefs->getInt("/tools/text/text_orientation", 0); + _orientation_item->set_active( mode ); + _orientation_item->focus_on_click(false); + add(*_orientation_item); + + _orientation_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::orientation_changed)); + } + + // Text direction (predominant direction of horizontal text). + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("LTR"); + row[columns.col_tooltip ] = _("Left to right text"); + row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-horizontal"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("RTL"); + row[columns.col_tooltip ] = _("Right to left text"); + row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-r2l"); + row[columns.col_sensitive] = true; + + _direction_item = + UI::Widget::ComboToolItem::create( _("Text direction"), // Label + _("Text direction for normally horizontal text."), // Tooltip + "Not Used", // Icon + store ); // List store + _direction_item->use_icon(true); + _direction_item->use_label(false); + gint mode = prefs->getInt("/tools/text/text_direction", 0); + _direction_item->set_active( mode ); + _direction_item->focus_on_click(false); + add(*_direction_item); + + _direction_item->signal_changed_after().connect(sigc::mem_fun(*this, &TextToolbar::direction_changed)); + } + + show_all(); + + // we emit a selection change on tool switch to text + desktop->connectEventContextChanged(sigc::mem_fun(*this, &TextToolbar::watch_ec)); +} + +/* + * Set the style, depending on the inner or outer text being selected + */ +void TextToolbar::text_outer_set_style(SPCSSAttr *css) +{ + // Calling sp_desktop_set_style will result in a call to TextTool::_styleSet() which + // will set the style on selected text inside the <text> element. If we want to set + // the style on the outer <text> objects we need to bypass this call. + SPDesktop *desktop = _desktop; + if(_outer) { + // Apply css to parent text objects directly. + for (auto i : desktop->getSelection()->items()) { + SPItem *item = dynamic_cast<SPItem *>(i); + if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) { + // Scale by inverse of accumulated parent transform + SPCSSAttr *css_set = sp_repr_css_attr_new(); + sp_repr_css_merge(css_set, css); + Geom::Affine const local(item->i2doc_affine()); + double const ex(local.descrim()); + if ((ex != 0.0) && (ex != 1.0)) { + sp_css_attr_scale(css_set, 1 / ex); + } + recursively_set_properties(item, css_set); + sp_repr_css_attr_unref(css_set); + } + } + } else { + // Apply css to selected inner objects. + sp_desktop_set_style (desktop, css, true, false); + } +} + +void +TextToolbar::fontfamily_value_changed() +{ +#ifdef DEBUG_TEXT + std::cout << std::endl; + std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" << std::endl; + std::cout << "sp_text_fontfamily_value_changed: " << std::endl; +#endif + + // quit if run by the _changed callbacks + if (_freeze) { +#ifdef DEBUG_TEXT + std::cout << "sp_text_fontfamily_value_changed: frozen... return" << std::endl; + std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n" << std::endl; +#endif + return; + } + _freeze = true; + + Glib::ustring new_family = _font_family_item->get_active_text(); + css_font_family_unquote( new_family ); // Remove quotes around font family names. + + // TODO: Think about how to handle handle multiple selections. While + // the font-family may be the same for all, the styles might be different. + // See: TextEdit::onApply() for example of looping over selected items. + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); +#ifdef DEBUG_TEXT + std::cout << " Old family: " << fontlister->get_font_family() << std::endl; + std::cout << " New family: " << new_family << std::endl; + std::cout << " Old active: " << fontlister->get_font_family_row() << std::endl; + // std::cout << " New active: " << act->active << std::endl; +#endif + if( new_family.compare( fontlister->get_font_family() ) != 0 ) { + // Changed font-family + + if( _font_family_item->get_active() == -1 ) { + // New font-family, not in document, not on system (could be fallback list) + fontlister->insert_font_family( new_family ); + + // This just sets a variable in the ComboBoxEntryAction object... + // shouldn't we also set the actual active row in the combobox? + _font_family_item->set_active(0); // New family is always at top of list. + } + + fontlister->set_font_family( _font_family_item->get_active() ); + // active text set in sp_text_toolbox_selection_changed() + + SPCSSAttr *css = sp_repr_css_attr_new (); + fontlister->fill_css( css ); + + SPDesktop *desktop = _desktop; + if( desktop->getSelection()->isEmpty() ) { + // Update default + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } else { + // If there is a selection, update + sp_desktop_set_style (desktop, css, true, true); // Results in selection change called twice. + DocumentUndo::done(desktop->getDocument(), _("Text: Change font family"), INKSCAPE_ICON("draw-text")); + } + sp_repr_css_attr_unref (css); + } + + // unfreeze + _freeze = false; + +#ifdef DEBUG_TEXT + std::cout << "sp_text_toolbox_fontfamily_changes: exit" << std::endl; + std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" << std::endl; + std::cout << std::endl; +#endif +} + +GtkWidget * +TextToolbar::create(SPDesktop *desktop) +{ + auto tb = Gtk::manage(new TextToolbar(desktop)); + return GTK_WIDGET(tb->gobj()); +} + +void +TextToolbar::fontsize_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + auto active_text = _font_size_item->get_active_text(); + char const *text = active_text.c_str(); + gchar *endptr; + gdouble size = g_strtod( text, &endptr ); + if (endptr == text) { // Conversion failed, non-numeric input. + g_warning( "Conversion of size text to double failed, input: %s\n", text ); + _freeze = false; + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int max_size = prefs->getInt("/dialogs/textandfont/maxFontSize", 10000); // somewhat arbitrary, but text&font preview freezes with too huge fontsizes + + if (size > max_size) + size = max_size; + + // Set css font size. + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream osfs; + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + if (prefs->getBool("/options/font/textOutputPx", true)) { + osfs << sp_style_css_size_units_to_px(size, unit) << sp_style_get_css_unit_string(SP_CSS_UNIT_PX); + } else { + osfs << size << sp_style_get_css_unit_string(unit); + } + sp_repr_css_set_property (css, "font-size", osfs.str().c_str()); + double factor = size / selection_fontsize; + + // Apply font size to selected objects. + text_outer_set_style(css); + + Unit const *unit_lh = _tracker->getActiveUnit(); + g_return_if_fail(unit_lh != nullptr); + if (!is_relative(unit_lh) && _outer) { + double lineheight = _line_height_adj->get_value(); + _freeze = false; + _line_height_adj->set_value(lineheight * factor); + _freeze = true; + } + // If no selected objects, set default. + SPStyle query(_desktop->getDocument()); + int result_numbers = + sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } else { + // Save for undo + sp_desktop_set_style (_desktop, css, true, true); + DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:size", _("Text: Change font size"), INKSCAPE_ICON("draw-text")); + } + + sp_repr_css_attr_unref(css); + + _freeze = false; +} + +void +TextToolbar::fontstyle_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + Glib::ustring new_style = _font_style_item->get_active_text(); + + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); + + if( new_style.compare( fontlister->get_font_style() ) != 0 ) { + + fontlister->set_font_style( new_style ); + // active text set in sp_text_toolbox_seletion_changed() + + SPCSSAttr *css = sp_repr_css_attr_new (); + fontlister->fill_css( css ); + + SPDesktop *desktop = _desktop; + sp_desktop_set_style (desktop, css, true, true); + + + // If no selected objects, set default. + SPStyle query(_desktop->getDocument()); + int result_style = + sp_desktop_query_style (desktop, &query, QUERY_STYLE_PROPERTY_FONTSTYLE); + if (result_style == QUERY_STYLE_NOTHING) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } else { + // Save for undo + DocumentUndo::done(desktop->getDocument(), _("Text: Change font style"), INKSCAPE_ICON("draw-text")); + } + + sp_repr_css_attr_unref (css); + + } + + _freeze = false; +} + +// Handles both Superscripts and Subscripts +void +TextToolbar::script_changed(Gtk::ToggleToolButton *btn) +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + + _freeze = true; + + // Called by Superscript or Subscript button? + auto name = btn->get_name(); + gint prop = (btn == _superscript_item) ? 0 : 1; + +#ifdef DEBUG_TEXT + std::cout << "TextToolbar::script_changed: " << prop << std::endl; +#endif + + // Query baseline + SPStyle query(_desktop->getDocument()); + int result_baseline = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_BASELINES); + + bool setSuper = false; + bool setSub = false; + + if (Inkscape::is_query_style_updateable(result_baseline)) { + // If not set or mixed, turn on superscript or subscript + if( prop == 0 ) { + setSuper = true; + } else { + setSub = true; + } + } else { + // Superscript + gboolean superscriptSet = (query.baseline_shift.set && + query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL && + query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUPER ); + + // Subscript + gboolean subscriptSet = (query.baseline_shift.set && + query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL && + query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUB ); + + setSuper = !superscriptSet && prop == 0; + setSub = !subscriptSet && prop == 1; + } + + // Set css properties + SPCSSAttr *css = sp_repr_css_attr_new (); + if( setSuper || setSub ) { + // Openoffice 2.3 and Adobe use 58%, Microsoft Word 2002 uses 65%, LaTex about 70%. + // 58% looks too small to me, especially if a superscript is placed on a superscript. + // If you make a change here, consider making a change to baseline-shift amount + // in style.cpp. + sp_repr_css_set_property (css, "font-size", "65%"); + } else { + sp_repr_css_set_property (css, "font-size", ""); + } + if( setSuper ) { + sp_repr_css_set_property (css, "baseline-shift", "super"); + } else if( setSub ) { + sp_repr_css_set_property (css, "baseline-shift", "sub"); + } else { + sp_repr_css_set_property (css, "baseline-shift", "baseline"); + } + + // Apply css to selected objects. + SPDesktop *desktop = _desktop; + sp_desktop_set_style (desktop, css, true, false); + + // Save for undo + if(result_baseline != QUERY_STYLE_NOTHING) { + DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:script", _("Text: Change superscript or subscript"), INKSCAPE_ICON("draw-text")); + } + _freeze = false; +} + +void +TextToolbar::align_mode_changed(int mode) +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/text/align_mode", mode); + + SPDesktop *desktop = _desktop; + + // move the x of all texts to preserve the same bbox + Inkscape::Selection *selection = desktop->getSelection(); + auto itemlist= selection->items(); + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + // SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text) { + SPItem *item = i; + + unsigned writing_mode = item->style->writing_mode.value; + // below, variable names suggest horizontal move, but we check the writing direction + // and move in the corresponding axis + Geom::Dim2 axis; + if (writing_mode == SP_CSS_WRITING_MODE_LR_TB || writing_mode == SP_CSS_WRITING_MODE_RL_TB) { + axis = Geom::X; + } else { + axis = Geom::Y; + } + + Geom::OptRect bbox = item->geometricBounds(); + if (!bbox) + continue; + double width = bbox->dimensions()[axis]; + // If you want to align within some frame, other than the text's own bbox, calculate + // the left and right (or top and bottom for tb text) slacks of the text inside that + // frame (currently unused) + double left_slack = 0; + double right_slack = 0; + unsigned old_align = item->style->text_align.value; + double move = 0; + if (old_align == SP_CSS_TEXT_ALIGN_START || old_align == SP_CSS_TEXT_ALIGN_LEFT) { + switch (mode) { + case 0: + move = -left_slack; + break; + case 1: + move = width/2 + (right_slack - left_slack)/2; + break; + case 2: + move = width + right_slack; + break; + } + } else if (old_align == SP_CSS_TEXT_ALIGN_CENTER) { + switch (mode) { + case 0: + move = -width/2 - left_slack; + break; + case 1: + move = (right_slack - left_slack)/2; + break; + case 2: + move = width/2 + right_slack; + break; + } + } else if (old_align == SP_CSS_TEXT_ALIGN_END || old_align == SP_CSS_TEXT_ALIGN_RIGHT) { + switch (mode) { + case 0: + move = -width - left_slack; + break; + case 1: + move = -width/2 + (right_slack - left_slack)/2; + break; + case 2: + move = right_slack; + break; + } + } + Geom::Point XY = SP_TEXT(item)->attributes.firstXY(); + if (axis == Geom::X) { + XY = XY + Geom::Point (move, 0); + } else { + XY = XY + Geom::Point (0, move); + } + SP_TEXT(item)->attributes.setFirstXY(XY); + item->updateRepr(); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + + SPCSSAttr *css = sp_repr_css_attr_new (); + switch (mode) + { + case 0: + { + sp_repr_css_set_property (css, "text-anchor", "start"); + sp_repr_css_set_property (css, "text-align", "start"); + break; + } + case 1: + { + sp_repr_css_set_property (css, "text-anchor", "middle"); + sp_repr_css_set_property (css, "text-align", "center"); + break; + } + + case 2: + { + sp_repr_css_set_property (css, "text-anchor", "end"); + sp_repr_css_set_property (css, "text-align", "end"); + break; + } + + case 3: + { + sp_repr_css_set_property (css, "text-anchor", "start"); + sp_repr_css_set_property (css, "text-align", "justify"); + break; + } + } + + SPStyle query(_desktop->getDocument()); + int result_numbers = + sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + + // If querying returned nothing, update default style. + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_desktop_set_style (desktop, css, true, true); + if (result_numbers != QUERY_STYLE_NOTHING) + { + DocumentUndo::done(_desktop->getDocument(), _("Text: Change alignment"), INKSCAPE_ICON("draw-text")); + } + sp_repr_css_attr_unref (css); + + desktop->getCanvas()->grab_focus(); + + _freeze = false; +} + +void +TextToolbar::writing_mode_changed(int mode) +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + SPCSSAttr *css = sp_repr_css_attr_new (); + switch (mode) + { + case 0: + { + sp_repr_css_set_property (css, "writing-mode", "lr-tb"); + break; + } + + case 1: + { + sp_repr_css_set_property (css, "writing-mode", "tb-rl"); + break; + } + + case 2: + { + sp_repr_css_set_property (css, "writing-mode", "vertical-lr"); + break; + } + } + + SPStyle query(_desktop->getDocument()); + int result_numbers = + sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_WRITINGMODES); + + // If querying returned nothing, update default style. + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_desktop_set_style (_desktop, css, true, true); + if(result_numbers != QUERY_STYLE_NOTHING) + { + DocumentUndo::done(_desktop->getDocument(), _("Text: Change writing mode"), INKSCAPE_ICON("draw-text")); + } + sp_repr_css_attr_unref (css); + + _desktop->getCanvas()->grab_focus(); + + _freeze = false; +} + +void +TextToolbar::orientation_changed(int mode) +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + SPCSSAttr *css = sp_repr_css_attr_new (); + switch (mode) + { + case 0: + { + sp_repr_css_set_property (css, "text-orientation", "auto"); + break; + } + + case 1: + { + sp_repr_css_set_property (css, "text-orientation", "upright"); + break; + } + + case 2: + { + sp_repr_css_set_property (css, "text-orientation", "sideways"); + break; + } + } + + SPStyle query(_desktop->getDocument()); + int result_numbers = + sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_WRITINGMODES); + + // If querying returned nothing, update default style. + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_desktop_set_style (_desktop, css, true, true); + if(result_numbers != QUERY_STYLE_NOTHING) + { + DocumentUndo::done(_desktop->getDocument(), _("Text: Change orientation"), INKSCAPE_ICON("draw-text")); + } + sp_repr_css_attr_unref (css); + _desktop->canvas->grab_focus(); + + _freeze = false; +} + +void +TextToolbar::direction_changed(int mode) +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + SPCSSAttr *css = sp_repr_css_attr_new (); + switch (mode) + { + case 0: + { + sp_repr_css_set_property (css, "direction", "ltr"); + break; + } + + case 1: + { + sp_repr_css_set_property (css, "direction", "rtl"); + break; + } + } + + SPStyle query(_desktop->getDocument()); + int result_numbers = + sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_WRITINGMODES); + + // If querying returned nothing, update default style. + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_desktop_set_style (_desktop, css, true, true); + if(result_numbers != QUERY_STYLE_NOTHING) + { + DocumentUndo::done(_desktop->getDocument(), _("Text: Change direction"), INKSCAPE_ICON("draw-text")); + } + sp_repr_css_attr_unref (css); + + _desktop->getCanvas()->grab_focus(); + + _freeze = false; +} + +void +TextToolbar::lineheight_value_changed() +{ + // quit if run by the _changed callbacks or is not text tool + if (_freeze || !SP_IS_TEXT_CONTEXT(_desktop->event_context)) { + return; + } + + _freeze = true; + SPDesktop *desktop = _desktop; + // Get user selected unit and save as preference + Unit const *unit = _tracker->getActiveUnit(); + // @Tav same disabled unit + g_return_if_fail(unit != nullptr); + + // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit so + // we can save it (allows us to adjust line height value when unit changes). + + // Set css line height. + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream osfs; + if ( is_relative(unit) ) { + osfs << _line_height_adj->get_value() << unit->abbr; + } else { + // Inside SVG file, always use "px" for absolute units. + osfs << Quantity::convert(_line_height_adj->get_value(), unit, "px") << "px"; + } + + sp_repr_css_set_property (css, "line-height", osfs.str().c_str()); + + Inkscape::Selection *selection = desktop->getSelection(); + auto itemlist = selection->items(); + if (_outer) { + // Special else makes this different from other uses of text_outer_set_style + text_outer_set_style(css); + } else { + SPItem *parent = dynamic_cast<SPItem *>(*itemlist.begin()); + SPStyle *parent_style = parent->style; + SPCSSAttr *parent_cssatr = sp_css_attr_from_style(parent_style, SP_STYLE_FLAG_IFSET); + Glib::ustring parent_lineheight = sp_repr_css_property(parent_cssatr, "line-height", "1.25"); + SPCSSAttr *cssfit = sp_repr_css_attr_new(); + sp_repr_css_set_property(cssfit, "line-height", parent_lineheight.c_str()); + double minheight = 0; + if (parent_style) { + minheight = parent_style->line_height.computed; + } + if (minheight) { + for (auto i : parent->childList(false)) { + SPItem *child = dynamic_cast<SPItem *>(i); + if (!child) { + continue; + } + recursively_set_properties(child, cssfit); + } + } + sp_repr_css_set_property(cssfit, "line-height", "0"); + parent->changeCSS(cssfit, "style"); + subselection_wrap_toggle(true); + sp_desktop_set_style(desktop, css, true, true); + subselection_wrap_toggle(false); + sp_repr_css_attr_unref(cssfit); + } + // Only need to save for undo if a text item has been changed. + itemlist = selection->items(); + bool modmade = false; + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + modmade = true; + break; + } + } + + // Save for undo + if (modmade) { + // Call ensureUpToDate() causes rebuild of text layout (with all proper style + // cascading, etc.). For multi-line text with sodipodi::role="line", we must explicitly + // save new <tspan> 'x' and 'y' attribute values by calling updateRepr(). + // Partial fix for bug #1590141. + + desktop->getDocument()->ensureUpToDate(); + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + (i)->updateRepr(); + } + } + if (!_outer) { + prepare_inner(); + } + DocumentUndo::maybeDone(desktop->getDocument(), "ttb:line-height", _("Text: Change line-height"), INKSCAPE_ICON("draw-text")); + } + + // If no selected objects, set default. + SPStyle query(_desktop->getDocument()); + int result_numbers = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_repr_css_attr_unref (css); + + _freeze = false; +} + +void +TextToolbar::lineheight_unit_changed(int /* Not Used */) +{ + // quit if run by the _changed callbacks or is not text tool + if (_freeze || !SP_IS_TEXT_CONTEXT(_desktop->event_context)) { + return; + } + _freeze = true; + + // Get old saved unit + int old_unit = _lineheight_unit; + + // Get user selected unit and save as preference + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit. + SPILength temp_length; + Inkscape::CSSOStringStream temp_stream; + temp_stream << 1 << unit->abbr; + temp_length.read(temp_stream.str().c_str()); + prefs->setInt("/tools/text/lineheight/display_unit", temp_length.unit); + if (old_unit == temp_length.unit) { + _freeze = false; + return; + } else { + _lineheight_unit = temp_length.unit; + } + + // Read current line height value + double line_height = _line_height_adj->get_value(); + SPDesktop *desktop = _desktop; + Inkscape::Selection *selection = desktop->getSelection(); + auto itemlist = selection->items(); + + // Convert between units + double font_size = 0; + double doc_scale = 1; + int count = 0; + bool has_flow = false; + + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + doc_scale = Geom::Affine(i->i2dt_affine()).descrim(); + font_size += i->style->font_size.computed * doc_scale; + ++count; + } + if (flowtext) { + has_flow = true; + } + } + if (count > 0) { + font_size /= count; + } else { + // ideally use default font-size. + font_size = 20; + } + if ((unit->abbr == "" || unit->abbr == "em") && (old_unit == SP_CSS_UNIT_NONE || old_unit == SP_CSS_UNIT_EM)) { + // Do nothing + } else if ((unit->abbr == "" || unit->abbr == "em") && old_unit == SP_CSS_UNIT_EX) { + line_height *= 0.5; + } else if ((unit->abbr) == "ex" && (old_unit == SP_CSS_UNIT_EM || old_unit == SP_CSS_UNIT_NONE)) { + line_height *= 2.0; + } else if ((unit->abbr == "" || unit->abbr == "em") && old_unit == SP_CSS_UNIT_PERCENT) { + line_height /= 100.0; + } else if ((unit->abbr) == "%" && (old_unit == SP_CSS_UNIT_EM || old_unit == SP_CSS_UNIT_NONE)) { + line_height *= 100; + } else if ((unit->abbr) == "ex" && old_unit == SP_CSS_UNIT_PERCENT) { + line_height /= 50.0; + } else if ((unit->abbr) == "%" && old_unit == SP_CSS_UNIT_EX) { + line_height *= 50; + } else if (is_relative(unit)) { + // Convert absolute to relative... for the moment use average font-size + if (old_unit == SP_CSS_UNIT_NONE) old_unit = SP_CSS_UNIT_EM; + line_height = Quantity::convert(line_height, sp_style_get_css_unit_string(old_unit), "px"); + + if (font_size > 0) { + line_height /= font_size; + } + if ((unit->abbr) == "%") { + line_height *= 100; + } else if ((unit->abbr) == "ex") { + line_height *= 2; + } + } else if (old_unit == SP_CSS_UNIT_NONE || old_unit == SP_CSS_UNIT_PERCENT || old_unit == SP_CSS_UNIT_EM || + old_unit == SP_CSS_UNIT_EX) { + // Convert relative to absolute... for the moment use average font-size + if (old_unit == SP_CSS_UNIT_PERCENT) { + line_height /= 100.0; + } else if (old_unit == SP_CSS_UNIT_EX) { + line_height /= 2.0; + } + line_height *= font_size; + line_height = Quantity::convert(line_height, "px", unit); + } else { + // Convert between different absolute units (only used in GUI) + line_height = Quantity::convert(line_height, sp_style_get_css_unit_string(old_unit), unit); + } + // Set css line height. + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream osfs; + // Set css line height. + if ( is_relative(unit) ) { + osfs << line_height << unit->abbr; + } else { + osfs << Quantity::convert(line_height, unit, "px") << "px"; + } + sp_repr_css_set_property (css, "line-height", osfs.str().c_str()); + + // Update GUI with line_height value. + _line_height_adj->set_value(line_height); + // Update "climb rate" The custom action has a step property but no way to set it. + if (unit->abbr == "%") { + _line_height_adj->set_step_increment(1.0); + _line_height_adj->set_page_increment(10.0); + } else { + _line_height_adj->set_step_increment(0.1); + _line_height_adj->set_page_increment(1.0); + } + // Internal function to set line-height which is spacing mode dependent. + SPItem *parent = itemlist.empty() ? nullptr : dynamic_cast<SPItem *>(*itemlist.begin()); + SPStyle *parent_style = nullptr; + if (parent) { + parent_style = parent->style; + } + bool inside = false; + if (_outer) { + if (!selection->singleItem() || !parent_style || parent_style->line_height.computed != 0) { + for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { + if (dynamic_cast<SPText *>(*i) || dynamic_cast<SPFlowtext *>(*i)) { + SPItem *item = *i; + // Scale by inverse of accumulated parent transform + SPCSSAttr *css_set = sp_repr_css_attr_new(); + sp_repr_css_merge(css_set, css); + Geom::Affine const local(item->i2doc_affine()); + double const ex(local.descrim()); + if ((ex != 0.0) && (ex != 1.0)) { + sp_css_attr_scale(css_set, 1 / ex); + } + recursively_set_properties(item, css_set); + sp_repr_css_attr_unref(css_set); + } + } + } else { + inside = true; + } + } + if (!_outer || inside) { + SPCSSAttr *parent_cssatr = sp_css_attr_from_style(parent_style, SP_STYLE_FLAG_IFSET); + Glib::ustring parent_lineheight = sp_repr_css_property(parent_cssatr, "line-height", "1.25"); + SPCSSAttr *cssfit = sp_repr_css_attr_new(); + sp_repr_css_set_property(cssfit, "line-height", parent_lineheight.c_str()); + double minheight = 0; + if (parent_style) { + minheight = parent_style->line_height.computed; + } + if (minheight) { + for (auto i : parent->childList(false)) { + SPItem *child = dynamic_cast<SPItem *>(i); + if (!child) { + continue; + } + recursively_set_properties(child, cssfit); + } + } + sp_repr_css_set_property(cssfit, "line-height", "0"); + parent->changeCSS(cssfit, "style"); + subselection_wrap_toggle(true); + sp_desktop_set_style(desktop, css, true, true); + subselection_wrap_toggle(false); + sp_repr_css_attr_unref(cssfit); + } + itemlist= selection->items(); + // Only need to save for undo if a text item has been changed. + bool modmade = false; + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + modmade = true; + break; + } + } + // Save for undo + if(modmade) { + // Call ensureUpToDate() causes rebuild of text layout (with all proper style + // cascading, etc.). For multi-line text with sodipodi::role="line", we must explicitly + // save new <tspan> 'x' and 'y' attribute values by calling updateRepr(). + // Partial fix for bug #1590141. + + desktop->getDocument()->ensureUpToDate(); + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + (i)->updateRepr(); + } + } + if (_outer) { + prepare_inner(); + } + DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:line-height", _("Text: Change line-height unit"), INKSCAPE_ICON("draw-text")); + } + + // If no selected objects, set default. + SPStyle query(_desktop->getDocument()); + int result_numbers = + sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_repr_css_attr_unref (css); + + _freeze = false; +} + +void TextToolbar::fontsize_unit_changed(int /* Not Used */) +{ + // quit if run by the _changed callbacks + Unit const *unit = _tracker_fs->getActiveUnit(); + g_return_if_fail(unit != nullptr); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit. + SPILength temp_size; + Inkscape::CSSOStringStream temp_size_stream; + temp_size_stream << 1 << unit->abbr; + temp_size.read(temp_size_stream.str().c_str()); + prefs->setInt("/options/font/unitType", temp_size.unit); + selection_changed(_desktop->selection); +} + +void +TextToolbar::wordspacing_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + // At the moment this handles only numerical values (i.e. no em unit). + // Set css word-spacing + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream osfs; + osfs << _word_spacing_adj->get_value() << "px"; // For now always use px + sp_repr_css_set_property (css, "word-spacing", osfs.str().c_str()); + text_outer_set_style(css); + + // If no selected objects, set default. + SPStyle query(_desktop->getDocument()); + int result_numbers = + sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } else { + // Save for undo + DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:word-spacing", _("Text: Change word-spacing"), INKSCAPE_ICON("draw-text")); + } + + sp_repr_css_attr_unref (css); + + _freeze = false; +} + +void +TextToolbar::letterspacing_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + // At the moment this handles only numerical values (i.e. no em unit). + // Set css letter-spacing + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream osfs; + osfs << _letter_spacing_adj->get_value() << "px"; // For now always use px + sp_repr_css_set_property (css, "letter-spacing", osfs.str().c_str()); + text_outer_set_style(css); + + // If no selected objects, set default. + SPStyle query(_desktop->getDocument()); + int result_numbers = + sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + else + { + // Save for undo + DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:letter-spacing", _("Text: Change letter-spacing"), INKSCAPE_ICON("draw-text")); + } + + sp_repr_css_attr_unref (css); + + _freeze = false; +} + +void +TextToolbar::dx_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + gdouble new_dx = _dx_adj->get_value(); + bool modmade = false; + + if( SP_IS_TEXT_CONTEXT(_desktop->event_context) ) { + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_desktop->event_context); + if( tc ) { + unsigned char_index = -1; + TextTagAttributes *attributes = + text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index ); + if( attributes ) { + double old_dx = attributes->getDx( char_index ); + double delta_dx = new_dx - old_dx; + sp_te_adjust_dx( tc->text, tc->text_sel_start, tc->text_sel_end, _desktop, delta_dx ); + modmade = true; + } + } + } + + if(modmade) { + // Save for undo + DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:dx", _("Text: Change dx (kern)"), INKSCAPE_ICON("draw-text")); + } + _freeze = false; +} + +void +TextToolbar::dy_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + gdouble new_dy = _dy_adj->get_value(); + bool modmade = false; + + if( SP_IS_TEXT_CONTEXT(_desktop->event_context) ) { + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_desktop->event_context); + if( tc ) { + unsigned char_index = -1; + TextTagAttributes *attributes = + text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index ); + if( attributes ) { + double old_dy = attributes->getDy( char_index ); + double delta_dy = new_dy - old_dy; + sp_te_adjust_dy( tc->text, tc->text_sel_start, tc->text_sel_end, _desktop, delta_dy ); + modmade = true; + } + } + } + + if(modmade) { + // Save for undo + DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:dy", _("Text: Change dy"), INKSCAPE_ICON("draw-text")); + } + + _freeze = false; +} + +void +TextToolbar::rotation_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + gdouble new_degrees = _rotation_adj->get_value(); + + bool modmade = false; + if( SP_IS_TEXT_CONTEXT(_desktop->event_context) ) { + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_desktop->event_context); + if( tc ) { + unsigned char_index = -1; + TextTagAttributes *attributes = + text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index ); + if( attributes ) { + double old_degrees = attributes->getRotate( char_index ); + double delta_deg = new_degrees - old_degrees; + sp_te_adjust_rotation( tc->text, tc->text_sel_start, tc->text_sel_end, _desktop, delta_deg ); + modmade = true; + } + } + } + + // Save for undo + if(modmade) { + DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:rotate", _("Text: Change rotate"), INKSCAPE_ICON("draw-text")); + } + + _freeze = false; +} + +void TextToolbar::selection_modified_select_tool(Inkscape::Selection *selection, guint flags) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double factor = prefs->getDouble("/options/font/scaleLineHeightFromFontSIze", 1.0); + if (factor != 1.0) { + Unit const *unit_lh = _tracker->getActiveUnit(); + g_return_if_fail(unit_lh != nullptr); + if (!is_relative(unit_lh) && _outer) { + double lineheight = _line_height_adj->get_value(); + bool is_freeze = _freeze; + _freeze = false; + _line_height_adj->set_value(lineheight * factor); + _freeze = is_freeze; + } + prefs->setDouble("/options/font/scaleLineHeightFromFontSIze", 1.0); + } +} + +void TextToolbar::selection_changed(Inkscape::Selection *selection) // don't bother to update font list if subsel + // changed +{ +#ifdef DEBUG_TEXT + static int count = 0; + ++count; + std::cout << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << "sp_text_toolbox_selection_changed: start " << count << std::endl; +#endif + + // quit if run by the _changed callbacks + if (_freeze) { + +#ifdef DEBUG_TEXT + std::cout << " Frozen, returning" << std::endl; + std::cout << "sp_text_toolbox_selection_changed: exit " << count << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << std::endl; +#endif + return; + } + _freeze = true; + + // selection defined as argument but not used, argh!!! + SPDesktop *desktop = _desktop; + SPDocument *document = _desktop->getDocument(); + selection = desktop->getSelection(); + auto itemlist = selection->items(); + +#ifdef DEBUG_TEXT + for(auto i : itemlist) { + const gchar* id = i->getId(); + std::cout << " " << id << std::endl; + } + Glib::ustring selected_text = sp_text_get_selected_text(_desktop->event_context); + std::cout << " Selected text: |" << selected_text << "|" << std::endl; +#endif + + // Only flowed text can be justified, only normal text can be kerned... + // Find out if we have flowed text now so we can use it several places + gboolean isFlow = false; + std::vector<SPItem *> to_work; + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + to_work.push_back(i); + } + if (flowtext || + (text && text->style && text->style->shape_inside.set)) { + isFlow = true; + } + } + bool outside = false; + if (selection && to_work.size() == 0) { + outside = true; + } + + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->selection_update(); + // Update font list, but only if widget already created. + if (_font_family_item->get_combobox() != nullptr) { + _font_family_item->set_active_text(fontlister->get_font_family().c_str(), fontlister->get_font_family_row()); + _font_style_item->set_active_text(fontlister->get_font_style().c_str()); + } + + /* + * Query from current selection: + * Font family (font-family) + * Style (font-weight, font-style, font-stretch, font-variant, font-align) + * Numbers (font-size, letter-spacing, word-spacing, line-height, text-anchor, writing-mode) + * Font specification (Inkscape private attribute) + */ + SPStyle query(document); + SPStyle query_fallback(document); + int result_family = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTFAMILY); + int result_style = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTSTYLE); + int result_baseline = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_BASELINES); + int result_wmode = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_WRITINGMODES); + + // Calling sp_desktop_query_style will result in a call to TextTool::_styleQueried(). + // This returns the style of the selected text inside the <text> element... which + // is often the style of one or more <tspan>s. If we want the style of the outer + // <text> objects then we need to bypass the call to TextTool::_styleQueried(). + // The desktop selection never includes the elements inside the <text> element. + int result_numbers = 0; + int result_numbers_fallback = 0; + if (!outside) { + if (_outer && this->_sub_active_item) { + std::vector<SPItem *> qactive{ this->_sub_active_item }; + SPItem *parent = dynamic_cast<SPItem *>(this->_sub_active_item->parent); + std::vector<SPItem *> qparent{ parent }; + result_numbers = + sp_desktop_query_style_from_list(qactive, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + result_numbers_fallback = + sp_desktop_query_style_from_list(qparent, &query_fallback, QUERY_STYLE_PROPERTY_FONTNUMBERS); + } else if (_outer) { + result_numbers = sp_desktop_query_style_from_list(to_work, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + } else { + result_numbers = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + } + } else { + result_numbers = + sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + } + + /* + * If no text in selection (querying returned nothing), read the style from + * the /tools/text preferences (default style for new texts). Return if + * tool bar already set to these preferences. + */ + if (result_family == QUERY_STYLE_NOTHING || + result_style == QUERY_STYLE_NOTHING || + result_numbers == QUERY_STYLE_NOTHING || + result_wmode == QUERY_STYLE_NOTHING ) { + // There are no texts in selection, read from preferences. + query.readFromPrefs("/tools/text"); +#ifdef DEBUG_TEXT + std::cout << " read style from prefs:" << std::endl; + sp_print_font( &query ); +#endif + if (_text_style_from_prefs) { + // Do not reset the toolbar style from prefs if we already did it last time + _freeze = false; +#ifdef DEBUG_TEXT + std::cout << " text_style_from_prefs: toolbar already set" << std:: endl; + std::cout << "sp_text_toolbox_selection_changed: exit " << count << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << std::endl; +#endif + return; + } + + // To ensure the value of the combobox is properly set on start-up, only mark + // the prefs set if the combobox has already been constructed. + if( _font_family_item->get_combobox() != nullptr ) { + _text_style_from_prefs = true; + } + } else { + _text_style_from_prefs = false; + } + + // If we have valid query data for text (font-family, font-specification) set toolbar accordingly. + { + // Size (average of text selected) + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + double size = 0; + if (!size && _cusor_numbers != QUERY_STYLE_NOTHING) { + size = sp_style_css_size_px_to_units(_query_cursor.font_size.computed, unit); + } + if (!size && result_numbers != QUERY_STYLE_NOTHING) { + size = sp_style_css_size_px_to_units(query.font_size.computed, unit); + } + if (!size && result_numbers_fallback != QUERY_STYLE_NOTHING) { + size = sp_style_css_size_px_to_units(query_fallback.font_size.computed, unit); + } + if (!size && _text_style_from_prefs) { + size = sp_style_css_size_px_to_units(query.font_size.computed, unit); + } + + auto unit_str = sp_style_get_css_unit_string(unit); + Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", unit_str, ")"); + + _font_size_item->set_tooltip(tooltip.c_str()); + + Inkscape::CSSOStringStream os; + // We dot want to parse values just show + + _tracker_fs->setActiveUnitByAbbr(sp_style_get_css_unit_string(unit)); + int rounded_size = std::round(size); + if (std::abs((size - rounded_size)/size) < 0.0001) { + // We use rounded_size to avoid rounding errors when, say, converting stored 'px' values to displayed 'pt' values. + os << rounded_size; + selection_fontsize = rounded_size; + } else { + os << size; + selection_fontsize = size; + } + + // Freeze to ignore callbacks. + //g_object_freeze_notify( G_OBJECT( fontSizeAction->combobox ) ); + sp_text_set_sizes(GTK_LIST_STORE(_font_size_item->get_model()), unit); + //g_object_thaw_notify( G_OBJECT( fontSizeAction->combobox ) ); + + _font_size_item->set_active_text( os.str().c_str() ); + + // Superscript + gboolean superscriptSet = + ((result_baseline == QUERY_STYLE_SINGLE || result_baseline == QUERY_STYLE_MULTIPLE_SAME ) && + query.baseline_shift.set && + query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL && + query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUPER ); + + _superscript_item->set_active(superscriptSet); + + // Subscript + gboolean subscriptSet = + ((result_baseline == QUERY_STYLE_SINGLE || result_baseline == QUERY_STYLE_MULTIPLE_SAME ) && + query.baseline_shift.set && + query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL && + query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUB ); + + _subscript_item->set_active(subscriptSet); + + // Alignment + + // Note: SVG 1.1 doesn't include text-align, SVG 1.2 Tiny doesn't include text-align="justify" + // text-align="justify" was a draft SVG 1.2 item (along with flowed text). + // Only flowed text can be left and right justified at the same time. + // Disable button if we don't have flowed text. + + Glib::RefPtr<Gtk::ListStore> store = _align_item->get_store(); + Gtk::TreeModel::Row row = *(store->get_iter("3")); // Justify entry + UI::Widget::ComboToolItemColumns columns; + row[columns.col_sensitive] = isFlow; + + int activeButton = 0; + if (query.text_align.computed == SP_CSS_TEXT_ALIGN_JUSTIFY) + { + activeButton = 3; + } else { + // This should take 'direction' into account + if (query.text_anchor.computed == SP_CSS_TEXT_ANCHOR_START) activeButton = 0; + if (query.text_anchor.computed == SP_CSS_TEXT_ANCHOR_MIDDLE) activeButton = 1; + if (query.text_anchor.computed == SP_CSS_TEXT_ANCHOR_END) activeButton = 2; + } + _align_item->set_active( activeButton ); + + double height = 0; + gint line_height_unit = 0; + + if (!height && _cusor_numbers != QUERY_STYLE_NOTHING) { + height = _query_cursor.line_height.value; + line_height_unit = _query_cursor.line_height.unit; + } + + if (!height && result_numbers != QUERY_STYLE_NOTHING) { + height = query.line_height.value; + line_height_unit = query.line_height.unit; + } + + if (!height && result_numbers_fallback != QUERY_STYLE_NOTHING) { + height = query_fallback.line_height.value; + line_height_unit = query_fallback.line_height.unit; + } + + if (!height && _text_style_from_prefs) { + height = query.line_height.value; + line_height_unit = query.line_height.unit; + } + + if (line_height_unit == SP_CSS_UNIT_PERCENT) { + height *= 100.0; // Inkscape store % as fraction in .value + } + + // We dot want to parse values just show + if (!is_relative(SPCSSUnit(line_height_unit))) { + gint curunit = prefs->getInt("/tools/text/lineheight/display_unit", 1); + // For backwards comaptibility + if (is_relative(SPCSSUnit(curunit))) { + prefs->setInt("/tools/text/lineheight/display_unit", 1); + curunit = 1; + } + height = Quantity::convert(height, "px", sp_style_get_css_unit_string(curunit)); + line_height_unit = curunit; + } + _line_height_adj->set_value(height); + + + // Update "climb rate" + if (line_height_unit == SP_CSS_UNIT_PERCENT) { + _line_height_adj->set_step_increment(1.0); + _line_height_adj->set_page_increment(10.0); + } else { + _line_height_adj->set_step_increment(0.1); + _line_height_adj->set_page_increment(1.0); + } + + if( line_height_unit == SP_CSS_UNIT_NONE ) { + // Function 'sp_style_get_css_unit_string' returns 'px' for unit none. + // We need to avoid this. + _tracker->setActiveUnitByAbbr(""); + } else { + _tracker->setActiveUnitByAbbr(sp_style_get_css_unit_string(line_height_unit)); + } + + // Save unit so we can do conversions between new/old units. + _lineheight_unit = line_height_unit; + // Word spacing + double wordSpacing; + if (query.word_spacing.normal) wordSpacing = 0.0; + else wordSpacing = query.word_spacing.computed; // Assume no units (change in desktop-style.cpp) + + _word_spacing_adj->set_value(wordSpacing); + + // Letter spacing + double letterSpacing; + if (query.letter_spacing.normal) letterSpacing = 0.0; + else letterSpacing = query.letter_spacing.computed; // Assume no units (change in desktop-style.cpp) + + _letter_spacing_adj->set_value(letterSpacing); + + // Writing mode + int activeButton2 = 0; + if (query.writing_mode.computed == SP_CSS_WRITING_MODE_LR_TB) activeButton2 = 0; + if (query.writing_mode.computed == SP_CSS_WRITING_MODE_TB_RL) activeButton2 = 1; + if (query.writing_mode.computed == SP_CSS_WRITING_MODE_TB_LR) activeButton2 = 2; + + _writing_mode_item->set_active( activeButton2 ); + + // Orientation + int activeButton3 = 0; + if (query.text_orientation.computed == SP_CSS_TEXT_ORIENTATION_MIXED ) activeButton3 = 0; + if (query.text_orientation.computed == SP_CSS_TEXT_ORIENTATION_UPRIGHT ) activeButton3 = 1; + if (query.text_orientation.computed == SP_CSS_TEXT_ORIENTATION_SIDEWAYS) activeButton3 = 2; + + _orientation_item->set_active( activeButton3 ); + + // Disable text orientation for horizontal text... + _orientation_item->set_sensitive( activeButton2 != 0 ); + + // Direction + int activeButton4 = 0; + if (query.direction.computed == SP_CSS_DIRECTION_LTR ) activeButton4 = 0; + if (query.direction.computed == SP_CSS_DIRECTION_RTL ) activeButton4 = 1; + _direction_item->set_active( activeButton4 ); + } + +#ifdef DEBUG_TEXT + std::cout << " GUI: fontfamily.value: " << query.font_family.value() << std::endl; + std::cout << " GUI: font_size.computed: " << query.font_size.computed << std::endl; + std::cout << " GUI: font_weight.computed: " << query.font_weight.computed << std::endl; + std::cout << " GUI: font_style.computed: " << query.font_style.computed << std::endl; + std::cout << " GUI: text_anchor.computed: " << query.text_anchor.computed << std::endl; + std::cout << " GUI: text_align.computed: " << query.text_align.computed << std::endl; + std::cout << " GUI: line_height.computed: " << query.line_height.computed + << " line_height.value: " << query.line_height.value + << " line_height.unit: " << query.line_height.unit << std::endl; + std::cout << " GUI: word_spacing.computed: " << query.word_spacing.computed + << " word_spacing.value: " << query.word_spacing.value + << " word_spacing.unit: " << query.word_spacing.unit << std::endl; + std::cout << " GUI: letter_spacing.computed: " << query.letter_spacing.computed + << " letter_spacing.value: " << query.letter_spacing.value + << " letter_spacing.unit: " << query.letter_spacing.unit << std::endl; + std::cout << " GUI: writing_mode.computed: " << query.writing_mode.computed << std::endl; +#endif + + // Kerning (xshift), yshift, rotation. NB: These are not CSS attributes. + if( SP_IS_TEXT_CONTEXT(_desktop->event_context) ) { + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_desktop->event_context); + if( tc ) { + unsigned char_index = -1; + TextTagAttributes *attributes = + text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index ); + if( attributes ) { + + // Dx + double dx = attributes->getDx( char_index ); + _dx_adj->set_value(dx); + + // Dy + double dy = attributes->getDy( char_index ); + _dy_adj->set_value(dy); + + // Rotation + double rotation = attributes->getRotate( char_index ); + /* SVG value is between 0 and 360 but we're using -180 to 180 in widget */ + if( rotation > 180.0 ) rotation -= 360.0; + _rotation_adj->set_value(rotation); + +#ifdef DEBUG_TEXT + std::cout << " GUI: Dx: " << dx << std::endl; + std::cout << " GUI: Dy: " << dy << std::endl; + std::cout << " GUI: Rotation: " << rotation << std::endl; +#endif + } + } + } + + { + // Set these here as we don't always have kerning/rotating attributes + _dx_item->set_sensitive(!isFlow); + _dy_item->set_sensitive(!isFlow); + _rotation_item->set_sensitive(!isFlow); + } + +#ifdef DEBUG_TEXT + std::cout << "sp_text_toolbox_selection_changed: exit " << count << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << std::endl; +#endif + + _freeze = false; +} + +void +TextToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) { + bool is_text_toolbar = SP_IS_TEXT_CONTEXT(ec); + bool is_select_toolbar = !is_text_toolbar && SP_IS_SELECT_CONTEXT(ec); + if (is_text_toolbar) { + // Watch selection + // Ensure FontLister is updated here first.................. + c_selection_changed = + desktop->getSelection()->connectChangedFirst(sigc::mem_fun(*this, &TextToolbar::selection_changed)); + c_selection_modified = desktop->getSelection()->connectModifiedFirst(sigc::mem_fun(*this, &TextToolbar::selection_modified)); + c_subselection_changed = desktop->connect_text_cursor_moved([=](void* sender, Inkscape::UI::Tools::TextTool* tool){ + subselection_changed(tool); + }); + this->_sub_active_item = nullptr; + this->_cusor_numbers = 0; + selection_changed(desktop->getSelection()); + } else if (is_select_toolbar) { + c_selection_modified_select_tool = desktop->getSelection()->connectModifiedFirst( + sigc::mem_fun(*this, &TextToolbar::selection_modified_select_tool)); + } + + + if (!is_text_toolbar) { + c_selection_changed.disconnect(); + c_selection_modified.disconnect(); + c_subselection_changed.disconnect(); + } + + if (!is_select_toolbar) { + c_selection_modified_select_tool.disconnect(); + } +} + +void +TextToolbar::selection_modified(Inkscape::Selection *selection, guint /*flags*/) +{ + this->_sub_active_item = nullptr; + selection_changed(selection); + +} + +void TextToolbar::subselection_wrap_toggle(bool start) +{ + if (SP_IS_TEXT_CONTEXT(_desktop->event_context)) { + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_desktop->event_context); + if (tc) { + _updating = true; + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + if (layout) { + Inkscape::Text::Layout::iterator start_selection = tc->text_sel_start; + Inkscape::Text::Layout::iterator end_selection = tc->text_sel_end; + tc->text_sel_start = wrap_start; + tc->text_sel_end = wrap_end; + wrap_start = start_selection; + wrap_end = end_selection; + } + _updating = start; + } + } +} + +/* +* This function parses the just created line height in one or more lines of a text subselection. +* It can describe 2 kinds of input because when we store a text element we apply a fallback that change +* structure. This visually is not reflected but user maybe want to change a part of this subselection +* once the fallback is created, so we need more complex logic here to fill the gap. +* Basically, we have a line height changed in the new wrapper element/s between wrap_start and wrap_end. +* These variables store starting iterator of first char in line and last char in line in a subselection. +* These elements are styled well but we can have orphaned text nodes before and after the subselection. +* So, normally 3 elements are inside a container as direct child of a text element. +* We need to apply the container style to the optional first and last text nodes, +* wrapping into a new element that gets the container style (this is not part to the sub-selection). +* After wrapping, we unindent all children of the container and remove the container. +* +*/ +void TextToolbar::prepare_inner() +{ + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_desktop->event_context); + if (!tc) { + return; + } + Inkscape::Text::Layout *layout = const_cast<Inkscape::Text::Layout *>(te_get_layout(tc->text)); + if (!layout) { + return; + } + SPDocument *doc = _desktop->getDocument(); + SPObject *spobject = dynamic_cast<SPObject *>(tc->text); + SPItem *spitem = dynamic_cast<SPItem *>(tc->text); + SPText *text = dynamic_cast<SPText *>(tc->text); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(tc->text); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + if (!spobject) { + return; + } + + // We check for external files with text nodes direct children of text element + // and wrap it into a tspan elements as inkscape do. + if (text) { + bool changed = false; + std::vector<SPObject *> childs = spitem->childList(false); + for (auto child : childs) { + SPString *spstring = dynamic_cast<SPString *>(child); + if (spstring) { + Glib::ustring content = spstring->string; + if (content != "\n") { + Inkscape::XML::Node *rstring = xml_doc->createTextNode(content.c_str()); + Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan"); + //Inkscape::XML::Node *rnl = xml_doc->createTextNode("\n"); + rtspan->setAttribute("sodipodi:role", "line"); + rtspan->addChild(rstring, nullptr); + text->getRepr()->addChild(rtspan, child->getRepr()); + Inkscape::GC::release(rstring); + Inkscape::GC::release(rtspan); + text->getRepr()->removeChild(spstring->getRepr()); + changed = true; + } + } + } + if (changed) { + // proper rebuild happens later, + // this just updates layout to use now, avoids use after free + text->rebuildLayout(); + } + } + + std::vector<SPObject *> containers; + { + // populate `containers` with objects that will be modified. + + // Temporarily remove the shape so Layout calculates + // the position of wrap_end and wrap_start, even if + // one of these are hidden because the previous line height was changed + if (text) { + text->hide_shape_inside(); + } else if (flowtext) { + flowtext->fix_overflow_flowregion(false); + } + SPObject *rawptr_start = nullptr; + SPObject *rawptr_end = nullptr; + layout->validateIterator(&wrap_start); + layout->validateIterator(&wrap_end); + layout->getSourceOfCharacter(wrap_start, &rawptr_start); + layout->getSourceOfCharacter(wrap_end, &rawptr_end); + if (text) { + text->show_shape_inside(); + } else if (flowtext) { + flowtext->fix_overflow_flowregion(true); + } + if (!rawptr_start || !rawptr_end) { + return; + } + + // Loop through parents of start and end till we reach + // first children of the text element. + // Get all objects between start and end (inclusive) + SPObject *start = rawptr_start; + SPObject *end = rawptr_end; + while (start->parent != spobject) { + start = start->parent; + } + while (end->parent != spobject) { + end = end->parent; + } + + while (start && start != end) { + containers.push_back(start); + start = start->getNext(); + } + if (start) { + containers.push_back(start); + } + } + + for (auto container : containers) { + Inkscape::XML::Node *prevchild = container->getRepr(); + std::vector<SPObject*> childs = container->childList(false); + for (auto child : childs) { + SPString *spstring = dynamic_cast<SPString *>(child); + SPFlowtspan *flowtspan = dynamic_cast<SPFlowtspan *>(child); + SPTSpan *tspan = dynamic_cast<SPTSpan *>(child); + // we need to upper all flowtspans to container level + // to do this we need to change the element from flowspan to flowpara + if (flowtspan) { + Inkscape::XML::Node *flowpara = xml_doc->createElement("svg:flowPara"); + std::vector<SPObject*> fts_childs = flowtspan->childList(false); + bool hascontent = false; + // we need to move the contents to the new created element + // maybe we can move directly but it is safer for me to duplicate, + // inject into the new element and delete original + for (auto fts_child : fts_childs) { + // is this check necessary? + if (fts_child) { + Inkscape::XML::Node *fts_child_node = fts_child->getRepr()->duplicate(xml_doc); + flowtspan->getRepr()->removeChild(fts_child->getRepr()); + flowpara->addChild(fts_child_node, nullptr); + Inkscape::GC::release(fts_child_node); + hascontent = true; + } + } + // if no contents we dont want to add + if (hascontent) { + flowpara->setAttribute("style", flowtspan->getRepr()->attribute("style")); + spobject->getRepr()->addChild(flowpara, prevchild); + Inkscape::GC::release(flowpara); + prevchild = flowpara; + } + container->getRepr()->removeChild(flowtspan->getRepr()); + } else if (tspan) { + if (child->childList(false).size()) { + child->getRepr()->setAttribute("sodipodi:role", "line"); + // maybe we need to move unindent function here + // to be the same as other here + prevchild = unindent_node(child->getRepr(), prevchild); + } else { + // if no contents we dont want to add + container->getRepr()->removeChild(child->getRepr()); + } + } else if (spstring) { + // we are on a text node, we act different if in a text or flowtext. + // wrap a duplicate of the element and unindent after the prevchild + // and finally delete original + Inkscape::XML::Node *string_node = xml_doc->createTextNode(spstring->string.c_str()); + if (text) { + Inkscape::XML::Node *tspan_node = xml_doc->createElement("svg:tspan"); + tspan_node->setAttribute("style", container->getRepr()->attribute("style")); + tspan_node->addChild(string_node, nullptr); + tspan_node->setAttribute("sodipodi:role", "line"); + text->getRepr()->addChild(tspan_node, prevchild); + Inkscape::GC::release(string_node); + Inkscape::GC::release(tspan_node); + prevchild = tspan_node; + } else if (flowtext) { + Inkscape::XML::Node *flowpara_node = xml_doc->createElement("svg:flowPara"); + flowpara_node->setAttribute("style", container->getRepr()->attribute("style")); + flowpara_node->addChild(string_node, nullptr); + flowtext->getRepr()->addChild(flowpara_node, prevchild); + Inkscape::GC::release(string_node); + Inkscape::GC::release(flowpara_node); + prevchild = flowpara_node; + } + container->getRepr()->removeChild(spstring->getRepr()); + } + } + tc->text->getRepr()->removeChild(container->getRepr()); + } +} + +Inkscape::XML::Node *TextToolbar::unindent_node(Inkscape::XML::Node *repr, Inkscape::XML::Node *prevchild) +{ + g_assert(repr != nullptr); + + Inkscape::XML::Node *parent = repr->parent(); + if (parent) { + Inkscape::XML::Node *grandparent = parent->parent(); + if (grandparent) { + SPDocument *doc = _desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *newrepr = repr->duplicate(xml_doc); + parent->removeChild(repr); + grandparent->addChild(newrepr, prevchild); + Inkscape::GC::release(newrepr); + newrepr->setAttribute("sodipodi:role", "line"); + return newrepr; + } + } + std::cout << "error on TextToolbar.cpp::2433" << std::endl; + return repr; +} + +void TextToolbar::subselection_changed(Inkscape::UI::Tools::TextTool* tc) +{ +#ifdef DEBUG_TEXT + std::cout << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << "subselection_changed: start " << std::endl; +#endif + // quit if run by the _changed callbacks + this->_sub_active_item = nullptr; + if (_updating) { + return; + } + if (tc) { + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + if (layout) { + Inkscape::Text::Layout::iterator start = layout->begin(); + Inkscape::Text::Layout::iterator end = layout->end(); + Inkscape::Text::Layout::iterator start_selection = tc->text_sel_start; + Inkscape::Text::Layout::iterator end_selection = tc->text_sel_end; +#ifdef DEBUG_TEXT + std::cout << " GUI: Start of text: " << layout->iteratorToCharIndex(start) << std::endl; + std::cout << " GUI: End of text: " << layout->iteratorToCharIndex(end) << std::endl; + std::cout << " GUI: Start of selection: " << layout->iteratorToCharIndex(start_selection) << std::endl; + std::cout << " GUI: End of selection: " << layout->iteratorToCharIndex(end_selection) << std::endl; + std::cout << " GUI: Loop Subelements: " << std::endl; + std::cout << " ::::::::::::::::::::::::::::::::::::::::::::: " << std::endl; +#endif + gint startline = layout->paragraphIndex(start_selection); + if (start_selection == end_selection) { + this->_outer = true; + gint counter = 0; + for (auto child : tc->text->childList(false)) { + SPItem *item = dynamic_cast<SPItem *>(child); + if (item && counter == startline) { + this->_sub_active_item = item; + int origin_selection = layout->iteratorToCharIndex(start_selection); + Inkscape::Text::Layout::iterator next = layout->charIndexToIterator(origin_selection + 1); + Inkscape::Text::Layout::iterator prev = layout->charIndexToIterator(origin_selection - 1); + //TODO: find a better way to init + _updating = true; + SPStyle query(_desktop->getDocument()); + _query_cursor = query; + Inkscape::Text::Layout::iterator start_line = tc->text_sel_start; + start_line.thisStartOfLine(); + if (tc->text_sel_start == start_line) { + tc->text_sel_start = next; + } else { + tc->text_sel_start = prev; + } + _cusor_numbers = sp_desktop_query_style(_desktop, &_query_cursor, QUERY_STYLE_PROPERTY_FONTNUMBERS); + tc->text_sel_start = start_selection; + wrap_start = tc->text_sel_start; + wrap_end = tc->text_sel_end; + wrap_start.thisStartOfLine(); + wrap_end.thisEndOfLine(); + _updating = false; + break; + } + ++counter; + } + selection_changed(nullptr); + } else if ((start_selection == start && end_selection == end) || + (start_selection == end && end_selection == start)) { + // full subselection + _cusor_numbers = 0; + this->_outer = true; + selection_changed(nullptr); + } else { + _cusor_numbers = 0; + this->_outer = false; + wrap_start = tc->text_sel_start; + wrap_end = tc->text_sel_end; + if (tc->text_sel_start > tc->text_sel_end) { + wrap_start.thisEndOfLine(); + wrap_end.thisStartOfLine(); + } else { + wrap_start.thisStartOfLine(); + wrap_end.thisEndOfLine(); + } + selection_changed(nullptr); + } + } + } +#ifdef DEBUG_TEXT + std::cout << "subselection_changed: exit " << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << std::endl; +#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/toolbar/text-toolbar.h b/src/ui/toolbar/text-toolbar.h new file mode 100644 index 0000000..b91166e --- /dev/null +++ b/src/ui/toolbar/text-toolbar.h @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_TEXT_TOOLBAR_H +#define SEEN_TEXT_TOOLBAR_H + +/** + * @file + * Text aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-item.h" +#include "object/sp-object.h" +#include "toolbar.h" +#include "text-editing.h" +#include "style.h" +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> +#include <gtkmm/popover.h> +#include <gtkmm/separatortoolitem.h> +#include <sigc++/connection.h> + +class SPDesktop; + +namespace Gtk { +class ComboBoxText; +class ToggleToolButton; +} + +namespace Inkscape { +class Selection; + +namespace UI { +namespace Tools { +class ToolBase; +class TextTool; +} + +namespace Widget { +class ComboBoxEntryToolItem; +class ComboToolItem; +class SpinButtonToolItem; +class UnitTracker; +} + +namespace Toolbar { +class TextToolbar : public Toolbar { +private: + bool _freeze; + bool _text_style_from_prefs; + UI::Widget::UnitTracker *_tracker; + UI::Widget::UnitTracker *_tracker_fs; + + UI::Widget::ComboBoxEntryToolItem *_font_family_item; + UI::Widget::ComboBoxEntryToolItem *_font_size_item; + UI::Widget::ComboToolItem *_font_size_units_item; + UI::Widget::ComboBoxEntryToolItem *_font_style_item; + UI::Widget::ComboToolItem *_line_height_units_item; + UI::Widget::SpinButtonToolItem *_line_height_item; + Gtk::ToggleToolButton *_superscript_item; + Gtk::ToggleToolButton *_subscript_item; + + UI::Widget::ComboToolItem *_align_item; + UI::Widget::ComboToolItem *_writing_mode_item; + UI::Widget::ComboToolItem *_orientation_item; + UI::Widget::ComboToolItem *_direction_item; + + UI::Widget::SpinButtonToolItem *_word_spacing_item; + UI::Widget::SpinButtonToolItem *_letter_spacing_item; + UI::Widget::SpinButtonToolItem *_dx_item; + UI::Widget::SpinButtonToolItem *_dy_item; + UI::Widget::SpinButtonToolItem *_rotation_item; + + Glib::RefPtr<Gtk::Adjustment> _line_height_adj; + Glib::RefPtr<Gtk::Adjustment> _word_spacing_adj; + Glib::RefPtr<Gtk::Adjustment> _letter_spacing_adj; + Glib::RefPtr<Gtk::Adjustment> _dx_adj; + Glib::RefPtr<Gtk::Adjustment> _dy_adj; + Glib::RefPtr<Gtk::Adjustment> _rotation_adj; + bool _outer; + SPItem *_sub_active_item; + int _lineheight_unit; + Inkscape::Text::Layout::iterator wrap_start; + Inkscape::Text::Layout::iterator wrap_end; + bool _updating; + int _cusor_numbers; + SPStyle _query_cursor; + double selection_fontsize; + sigc::connection c_selection_changed; + sigc::connection c_selection_modified; + sigc::connection c_selection_modified_select_tool; + sigc::connection c_subselection_changed; + void text_outer_set_style(SPCSSAttr *css); + void fontfamily_value_changed(); + void fontsize_value_changed(); + void subselection_wrap_toggle(bool start); + void fontstyle_value_changed(); + void script_changed(Gtk::ToggleToolButton *btn); + void align_mode_changed(int mode); + void writing_mode_changed(int mode); + void orientation_changed(int mode); + void direction_changed(int mode); + void lineheight_value_changed(); + void lineheight_unit_changed(int not_used); + void wordspacing_value_changed(); + void letterspacing_value_changed(); + void dx_value_changed(); + void dy_value_changed(); + void prepare_inner(); + void focus_text(); + void rotation_value_changed(); + void fontsize_unit_changed(int not_used); + void selection_changed(Inkscape::Selection *selection); + void selection_modified(Inkscape::Selection *selection, guint flags); + void selection_modified_select_tool(Inkscape::Selection *selection, guint flags); + void subselection_changed(Inkscape::UI::Tools::TextTool* texttool); + void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void set_sizes(int unit); + Inkscape::XML::Node *unindent_node(Inkscape::XML::Node *repr, Inkscape::XML::Node *before); + + protected: + TextToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; +} +} +} + +#endif /* !SEEN_TEXT_TOOLBAR_H */ diff --git a/src/ui/toolbar/toolbar.cpp b/src/ui/toolbar/toolbar.cpp new file mode 100644 index 0000000..c15a4ca --- /dev/null +++ b/src/ui/toolbar/toolbar.cpp @@ -0,0 +1,84 @@ +// 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 "toolbar.h" + +#include <gtkmm/label.h> +#include <gtkmm/separatortoolitem.h> +#include <gtkmm/toggletoolbutton.h> + +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +Gtk::ToolItem * +Toolbar::add_label(const Glib::ustring &label_text) +{ + auto ti = Gtk::manage(new Gtk::ToolItem()); + + // For now, we always enable mnemonic + auto label = Gtk::manage(new Gtk::Label(label_text, true)); + + ti->add(*label); + add(*ti); + + return ti; +} + +/** + * \brief Add a toggle toolbutton to the toolbar + * + * \param[in] label_text The text to display in the toolbar + * \param[in] tooltip_text The tooltip text for the toolitem + * + * \returns The toggle button + */ +Gtk::ToggleToolButton * +Toolbar::add_toggle_button(const Glib::ustring &label_text, + const Glib::ustring &tooltip_text) +{ + auto btn = Gtk::manage(new Gtk::ToggleToolButton(label_text)); + btn->set_tooltip_text(tooltip_text); + add(*btn); + return btn; +} + +/** + * \brief Add a separator line to the toolbar + * + * \details This is just a convenience wrapper for the + * standard GtkMM functionality + */ +void +Toolbar::add_separator() +{ + add(* Gtk::manage(new Gtk::SeparatorToolItem())); +} + +GtkWidget * +Toolbar::create(SPDesktop *desktop) +{ + auto toolbar = Gtk::manage(new Toolbar(desktop)); + return GTK_WIDGET(toolbar->gobj()); +} +} +} +} +/* + 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/toolbar/toolbar.h b/src/ui/toolbar/toolbar.h new file mode 100644 index 0000000..bbbd7f0 --- /dev/null +++ b/src/ui/toolbar/toolbar.h @@ -0,0 +1,66 @@ +// 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. + */ +#ifndef SEEN_TOOLBAR_H +#define SEEN_TOOLBAR_H + +#include <gtkmm/toolbar.h> + +class SPDesktop; + +namespace Gtk { + class Label; + class ToggleToolButton; +} + +namespace Inkscape { +namespace UI { +namespace Toolbar { +/** + * \brief An abstract definition for a toolbar within Inkscape + * + * \detail This is basically the same as a Gtk::Toolbar but contains a + * few convenience functions. All toolbars must define a "create" + * function that adds all the required tool-items and returns the + * toolbar as a GtkWidget + */ +class Toolbar : public Gtk::Toolbar { +protected: + SPDesktop *_desktop; + + /** + * \brief A default constructor that just assigns the desktop + */ + Toolbar(SPDesktop *desktop) + : _desktop(desktop) + {} + + Gtk::ToolItem * add_label(const Glib::ustring &label_text); + Gtk::ToggleToolButton * add_toggle_button(const Glib::ustring &label_text, + const Glib::ustring &tooltip_text); + void add_separator(); + +protected: + static GtkWidget * create(SPDesktop *desktop); +}; +} +} +} + +#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/toolbar/tweak-toolbar.cpp b/src/ui/toolbar/tweak-toolbar.cpp new file mode 100644 index 0000000..ed840cd --- /dev/null +++ b/src/ui/toolbar/tweak-toolbar.cpp @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Tweak aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tweak-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" + +#include "ui/icon-names.h" +#include "ui/tools/tweak-tool.h" +#include "ui/widget/canvas.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/spin-button-tool-item.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { +TweakToolbar::TweakToolbar(SPDesktop *desktop) + : Toolbar(desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /* Width */ + { + std::vector<Glib::ustring> labels = {_("(pinch tweak)"), "", "", "", _("(default)"), "", "", "", "", _("(broad tweak)")}; + std::vector<double> values = { 1, 3, 5, 10, 15, 20, 30, 50, 75, 100}; + + auto width_val = prefs->getDouble("/tools/tweak/width", 15); + _width_adj = Gtk::Adjustment::create(width_val * 100, 1, 100, 1.0, 10.0); + _width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("tweak-width", _("Width:"), _width_adj, 0.01, 0)); + _width_item->set_tooltip_text(_("The width of the tweak area (relative to the visible canvas area)")); + _width_item->set_custom_numeric_menu_data(values, labels); + _width_item->set_focus_widget(desktop->canvas); + _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::width_value_changed)); + add(*_width_item); + _width_item->set_sensitive(true); + } + + // Force + { + std::vector<Glib::ustring> labels = {_("(minimum force)"), "", "", _("(default)"), "", "", "", _("(maximum force)")}; + std::vector<double> values = { 1, 5, 10, 20, 30, 50, 70, 100}; + auto force_val = prefs->getDouble("/tools/tweak/force", 20); + _force_adj = Gtk::Adjustment::create(force_val * 100, 1, 100, 1.0, 10.0); + _force_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("tweak-force", _("Force:"), _force_adj, 0.01, 0)); + _force_item->set_tooltip_text(_("The force of the tweak action")); + _force_item->set_custom_numeric_menu_data(values, labels); + _force_item->set_focus_widget(desktop->canvas); + _force_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::force_value_changed)); + add(*_force_item); + _force_item->set_sensitive(true); + } + + /* Use Pressure button */ + { + _pressure_item = add_toggle_button(_("Pressure"), + _("Use the pressure of the input device to alter the force of tweak action")); + _pressure_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _pressure_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::pressure_state_changed)); + _pressure_item->set_active(prefs->getBool("/tools/tweak/usepressure", true)); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Mode */ + { + add_label(_("Mode:")); + Gtk::RadioToolButton::Group mode_group; + + auto mode_move_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Move mode"))); + mode_move_btn->set_tooltip_text(_("Move objects in any direction")); + mode_move_btn->set_icon_name(INKSCAPE_ICON("object-tweak-push")); + _mode_buttons.push_back(mode_move_btn); + + auto mode_inout_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Move in/out mode"))); + mode_inout_btn->set_tooltip_text(_("Move objects towards cursor; with Shift from cursor")); + mode_inout_btn->set_icon_name(INKSCAPE_ICON("object-tweak-attract")); + _mode_buttons.push_back(mode_inout_btn); + + auto mode_jitter_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Move jitter mode"))); + mode_jitter_btn->set_tooltip_text(_("Move objects in random directions")); + mode_jitter_btn->set_icon_name(INKSCAPE_ICON("object-tweak-randomize")); + _mode_buttons.push_back(mode_jitter_btn); + + auto mode_scale_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Scale mode"))); + mode_scale_btn->set_tooltip_text(_("Shrink objects, with Shift enlarge")); + mode_scale_btn->set_icon_name(INKSCAPE_ICON("object-tweak-shrink")); + _mode_buttons.push_back(mode_scale_btn); + + auto mode_rotate_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Rotate mode"))); + mode_rotate_btn->set_tooltip_text(_("Rotate objects, with Shift counterclockwise")); + mode_rotate_btn->set_icon_name(INKSCAPE_ICON("object-tweak-rotate")); + _mode_buttons.push_back(mode_rotate_btn); + + auto mode_dupdel_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Duplicate/delete mode"))); + mode_dupdel_btn->set_tooltip_text(_("Duplicate objects, with Shift delete")); + mode_dupdel_btn->set_icon_name(INKSCAPE_ICON("object-tweak-duplicate")); + _mode_buttons.push_back(mode_dupdel_btn); + + auto mode_push_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Push mode"))); + mode_push_btn->set_tooltip_text(_("Push parts of paths in any direction")); + mode_push_btn->set_icon_name(INKSCAPE_ICON("path-tweak-push")); + _mode_buttons.push_back(mode_push_btn); + + auto mode_shrinkgrow_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Shrink/grow mode"))); + mode_shrinkgrow_btn->set_tooltip_text(_("Shrink (inset) parts of paths; with Shift grow (outset)")); + mode_shrinkgrow_btn->set_icon_name(INKSCAPE_ICON("path-tweak-shrink")); + _mode_buttons.push_back(mode_shrinkgrow_btn); + + auto mode_attrep_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Attract/repel mode"))); + mode_attrep_btn->set_tooltip_text(_("Attract parts of paths towards cursor; with Shift from cursor")); + mode_attrep_btn->set_icon_name(INKSCAPE_ICON("path-tweak-attract")); + _mode_buttons.push_back(mode_attrep_btn); + + auto mode_roughen_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Roughen mode"))); + mode_roughen_btn->set_tooltip_text(_("Roughen parts of paths")); + mode_roughen_btn->set_icon_name(INKSCAPE_ICON("path-tweak-roughen")); + _mode_buttons.push_back(mode_roughen_btn); + + auto mode_colpaint_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Color paint mode"))); + mode_colpaint_btn->set_tooltip_text(_("Paint the tool's color upon selected objects")); + mode_colpaint_btn->set_icon_name(INKSCAPE_ICON("object-tweak-paint")); + _mode_buttons.push_back(mode_colpaint_btn); + + auto mode_coljitter_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Color jitter mode"))); + mode_coljitter_btn->set_tooltip_text(_("Jitter the colors of selected objects")); + mode_coljitter_btn->set_icon_name(INKSCAPE_ICON("object-tweak-jitter-color")); + _mode_buttons.push_back(mode_coljitter_btn); + + auto mode_blur_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Blur mode"))); + mode_blur_btn->set_tooltip_text(_("Blur selected objects more; with Shift, blur less")); + mode_blur_btn->set_icon_name(INKSCAPE_ICON("object-tweak-blur")); + _mode_buttons.push_back(mode_blur_btn); + + int btn_idx = 0; + + for (auto btn : _mode_buttons) { + btn->set_sensitive(); + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &TweakToolbar::mode_changed), btn_idx++)); + } + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + guint mode = prefs->getInt("/tools/tweak/mode", 0); + + /* Fidelity */ + { + std::vector<Glib::ustring> labels = {_("(rough, simplified)"), "", "", _("(default)"), "", "", _("(fine, but many nodes)")}; + std::vector<double> values = { 10, 25, 35, 50, 60, 80, 100}; + + auto fidelity_val = prefs->getDouble("/tools/tweak/fidelity", 50); + _fidelity_adj = Gtk::Adjustment::create(fidelity_val * 100, 1, 100, 1.0, 10.0); + _fidelity_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("tweak-fidelity", _("Fidelity:"), _fidelity_adj, 0.01, 0)); + _fidelity_item->set_tooltip_text(_("Low fidelity simplifies paths; high fidelity preserves path features but may generate a lot of new nodes")); + _fidelity_item->set_custom_numeric_menu_data(values, labels); + _fidelity_item->set_focus_widget(desktop->canvas); + _fidelity_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::fidelity_value_changed)); + add(*_fidelity_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + _channels_label = Gtk::manage(new UI::Widget::LabelToolItem(_("Channels:"))); + _channels_label->set_use_markup(true); + add(*_channels_label); + } + + { + //TRANSLATORS: "H" here stands for hue + _doh_item = add_toggle_button(C_("Hue", "H"), + _("In color mode, act on object's hue")); + _doh_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_doh)); + _doh_item->set_active(prefs->getBool("/tools/tweak/doh", true)); + } + { + //TRANSLATORS: "S" here stands for saturation + _dos_item = add_toggle_button(C_("Saturation", "S"), + _("In color mode, act on object's saturation")); + _dos_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_dos)); + _dos_item->set_active(prefs->getBool("/tools/tweak/dos", true)); + } + { + //TRANSLATORS: "S" here stands for saturation + _dol_item = add_toggle_button(C_("Lightness", "L"), + _("In color mode, act on object's lightness")); + _dol_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_dol)); + _dol_item->set_active(prefs->getBool("/tools/tweak/dol", true)); + } + { + //TRANSLATORS: "O" here stands for opacity + _doo_item = add_toggle_button(C_("Opacity", "O"), + _("In color mode, act on object's opacity")); + _doo_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_doo)); + _doo_item->set_active(prefs->getBool("/tools/tweak/doo", true)); + } + + _mode_buttons[mode]->set_active(); + show_all(); + + // Elements must be hidden after show_all() is called + if (mode == Inkscape::UI::Tools::TWEAK_MODE_COLORPAINT || mode == Inkscape::UI::Tools::TWEAK_MODE_COLORJITTER) { + _fidelity_item->set_visible(false); + } else { + _channels_label->set_visible(false); + _doh_item->set_visible(false); + _dos_item->set_visible(false); + _dol_item->set_visible(false); + _doo_item->set_visible(false); + } +} + +void +TweakToolbar::set_mode(int mode) +{ + _mode_buttons[mode]->set_active(); +} + +GtkWidget * +TweakToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new TweakToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +TweakToolbar::width_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/tweak/width", + _width_adj->get_value() * 0.01 ); +} + +void +TweakToolbar::force_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/tweak/force", + _force_adj->get_value() * 0.01 ); +} + +void +TweakToolbar::mode_changed(int mode) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/tweak/mode", mode); + + bool flag = ((mode == Inkscape::UI::Tools::TWEAK_MODE_COLORPAINT) || + (mode == Inkscape::UI::Tools::TWEAK_MODE_COLORJITTER)); + + _doh_item->set_visible(flag); + _dos_item->set_visible(flag); + _dol_item->set_visible(flag); + _doo_item->set_visible(flag); + _channels_label->set_visible(flag); + + if (_fidelity_item) { + _fidelity_item->set_visible(!flag); + } +} + +void +TweakToolbar::fidelity_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/tweak/fidelity", + _fidelity_adj->get_value() * 0.01 ); +} + +void +TweakToolbar::pressure_state_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/tweak/usepressure", _pressure_item->get_active()); +} + +void +TweakToolbar::toggle_doh() { + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/tweak/doh", _doh_item->get_active()); +} + +void +TweakToolbar::toggle_dos() { + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/tweak/dos", _dos_item->get_active()); +} + +void +TweakToolbar::toggle_dol() { + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/tweak/dol", _dol_item->get_active()); +} + +void +TweakToolbar::toggle_doo() { + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/tweak/doo", _doo_item->get_active()); +} + +} +} +} + +/* + 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/toolbar/tweak-toolbar.h b/src/ui/toolbar/tweak-toolbar.h new file mode 100644 index 0000000..cd1c7d0 --- /dev/null +++ b/src/ui/toolbar/tweak-toolbar.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_TWEAK_TOOLBAR_H +#define SEEN_TWEAK_TOOLBAR_H + +/** + * @file + * Tweak aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +class SPDesktop; + +namespace Gtk { +class RadioToolButton; +} + +namespace Inkscape { +namespace UI { +namespace Widget { +class LabelToolItem; +class SpinButtonToolItem; +} + +namespace Toolbar { +class TweakToolbar : public Toolbar { +private: + UI::Widget::SpinButtonToolItem *_width_item; + UI::Widget::SpinButtonToolItem *_force_item; + UI::Widget::SpinButtonToolItem *_fidelity_item; + + Gtk::ToggleToolButton *_pressure_item; + + Glib::RefPtr<Gtk::Adjustment> _width_adj; + Glib::RefPtr<Gtk::Adjustment> _force_adj; + Glib::RefPtr<Gtk::Adjustment> _fidelity_adj; + + std::vector<Gtk::RadioToolButton *> _mode_buttons; + + UI::Widget::LabelToolItem *_channels_label; + Gtk::ToggleToolButton *_doh_item; + Gtk::ToggleToolButton *_dos_item; + Gtk::ToggleToolButton *_dol_item; + Gtk::ToggleToolButton *_doo_item; + + void width_value_changed(); + void force_value_changed(); + void mode_changed(int mode); + void fidelity_value_changed(); + void pressure_state_changed(); + void toggle_doh(); + void toggle_dos(); + void toggle_dol(); + void toggle_doo(); + +protected: + TweakToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); + + void set_mode(int mode); +}; +} +} +} + +#endif /* !SEEN_SELECT_TOOLBAR_H */ diff --git a/src/ui/toolbar/zoom-toolbar.cpp b/src/ui/toolbar/zoom-toolbar.cpp new file mode 100644 index 0000000..56422cc --- /dev/null +++ b/src/ui/toolbar/zoom-toolbar.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Zoom aux toolbar: Temp until we convert all toolbars to ui files with Gio::Actions. + */ +/* Authors: + * Tavmjong Bah <tavmjong@free.fr> + + * Copyright (C) 2019 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> + +#include "zoom-toolbar.h" + +#include "desktop.h" +#include "io/resource.h" + +using Inkscape::IO::Resource::UIS; + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +GtkWidget * +ZoomToolbar::create(SPDesktop *desktop) +{ + Glib::ustring zoom_toolbar_builder_file = get_filename(UIS, "toolbar-zoom.ui"); + auto builder = Gtk::Builder::create(); + try + { + builder->add_from_file(zoom_toolbar_builder_file); + } + catch (const Glib::Error& ex) + { + std::cerr << "ZoomToolbar: " << zoom_toolbar_builder_file << " file not read! " << ex.what().raw() << std::endl; + } + + Gtk::Toolbar* toolbar = nullptr; + builder->get_widget("zoom-toolbar", toolbar); + if (!toolbar) { + std::cerr << "InkscapeWindow: Failed to load zoom toolbar!" << std::endl; + return nullptr; + } + + toolbar->reference(); // Or it will be deleted when builder is destroyed since we haven't added + // it to a container yet. This probably causes a memory leak but we'll + // fix it when all toolbars are converted to use Gio::Actions. + + return GTK_WIDGET(toolbar->gobj()); +} +} +} +} + +/* + 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/toolbar/zoom-toolbar.h b/src/ui/toolbar/zoom-toolbar.h new file mode 100644 index 0000000..e3cfd29 --- /dev/null +++ b/src/ui/toolbar/zoom-toolbar.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_ZOOM_TOOLBAR_H +#define SEEN_ZOOM_TOOLBAR_H + +/** + * @file + * Zoom aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +/** + * \brief A toolbar for controlling the zoom + */ +class ZoomToolbar { +protected: + ZoomToolbar(SPDesktop *desktop) {}; + +public: + static GtkWidget * create(SPDesktop *desktop); +}; +} +} +} + +#endif /* !SEEN_ZOOM_TOOLBAR_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/tools/arc-tool.cpp b/src/ui/tools/arc-tool.cpp new file mode 100644 index 0000000..12143bf --- /dev/null +++ b/src/ui/tools/arc-tool.cpp @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Ellipse drawing context. + */ +/* Authors: + * Mitsuru Oka + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <johan@shouraizou.nl> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000-2006 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <glibmm/i18n.h> +#include <gdk/gdkkeysyms.h> + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "preferences.h" +#include "selection.h" +#include "snap.h" + +#include "include/macros.h" + +#include "object/sp-ellipse.h" +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/modifiers.h" +#include "ui/tools/arc-tool.h" +#include "ui/shape-editor.h" +#include "ui/tools/tool-base.h" + +#include "xml/repr.h" +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +ArcTool::ArcTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/shapes/arc", "arc.svg") + , arc(nullptr) +{ + Inkscape::Selection *selection = desktop->getSelection(); + + this->shape_editor = new ShapeEditor(desktop); + + SPItem *item = desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = selection->connectChanged( + sigc::mem_fun(this, &ArcTool::selection_changed) + ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +ArcTool::~ArcTool() +{ + ungrabCanvasEvents(); + this->finishItem(); + this->sel_changed_connection.disconnect(); + + this->enableGrDrag(false); + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->arc) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + */ +void ArcTool::selection_changed(Inkscape::Selection* selection) { + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); +} + + +bool ArcTool::item_handler(SPItem* item, GdkEvent* event) { + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + this->setup_for_drag_start(event); + } + break; + // motion and release are always on root (why?) + default: + break; + } + + return ToolBase::item_handler(item, event); +} + +bool ArcTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = _desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + bool handled = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + dragging = true; + + this->center = this->setup_for_drag_start(event); + + /* Snap center */ + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE); + + grabCanvasEvents(); + + handled = true; + m.unSetup(); + } + break; + case GDK_MOTION_NOTIFY: + if (dragging && (event->motion.state & GDK_BUTTON1_MASK)) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->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 draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + + this->drag(motion_dt, event->motion.state); + + gobble_motion_events(GDK_BUTTON1_MASK); + + handled = true; + } else if (!this->sp_event_context_knot_mouseover()){ + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + if (event->button.button == 1) { + dragging = false; + this->discard_delayed_snap_event(); + + if (!this->within_tolerance) { + // we've been dragging, finish the arc + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + handled = true; + } + ungrabCanvasEvents(); + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + if (!dragging) { + sp_event_show_modifier_tip(this->defaultMessageContext(), event, + _("<b>Ctrl</b>: make circle or integer-ratio ellipse, snap arc/segment angle"), + _("<b>Shift</b>: draw around the starting point"), + nullptr); + } + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("arc-rx"); + handled = true; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + this->discard_delayed_snap_event(); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + handled = true; + } + break; + + case GDK_KEY_space: + if (dragging) { + ungrabCanvasEvents(); + dragging = false; + this->discard_delayed_snap_event(); + + if (!this->within_tolerance) { + // we've been dragging, finish the arc + this->finishItem(); + } + // do not return true, so that space would work switching to selector + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + handled = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (event->key.keyval) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!handled) { + handled = ToolBase::root_handler(event); + } + + return handled; +} + +void ArcTool::drag(Geom::Point pt, guint state) { + if (!this->arc) { + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "arc"); + + // Set style + sp_desktop_apply_style_tool(_desktop, repr, "/tools/shapes/arc", false); + + auto layer = currentLayer(); + this->arc = SP_GENERICELLIPSE(layer->appendChildRepr(repr)); + Inkscape::GC::release(repr); + this->arc->transform = layer->i2doc_affine().inverse(); + this->arc->updateRepr(); + } + + auto confine = Modifiers::Modifier::get(Modifiers::Type::TRANS_CONFINE)->active(state); + // Third is weirdly wrong, surely incrememnts should do something else. + auto circle_edge = Modifiers::Modifier::get(Modifiers::Type::TRANS_INCREMENT)->active(state); + + Geom::Rect r = Inkscape::snap_rectangular_box(_desktop, this->arc, pt, this->center, state); + + Geom::Point dir = r.dimensions() / 2; + + + if (circle_edge) { + /* With Alt let the ellipse pass through the mouse pointer */ + Geom::Point c = r.midpoint(); + + if (!confine) { + if (fabs(dir[Geom::X]) > 1E-6 && fabs(dir[Geom::Y]) > 1E-6) { + Geom::Affine const i2d ( (this->arc)->i2dt_affine() ); + Geom::Point new_dir = pt * i2d - c; + new_dir[Geom::X] *= dir[Geom::Y] / dir[Geom::X]; + double lambda = new_dir.length() / dir[Geom::Y]; + r = Geom::Rect (c - lambda*dir, c + lambda*dir); + } + } else { + /* with Alt+Ctrl (without Shift) we generate a perfect circle + with diameter click point <--> mouse pointer */ + double l = dir.length(); + Geom::Point d (l, l); + r = Geom::Rect (c - d, c + d); + } + } + + this->arc->position_set( + r.midpoint()[Geom::X], r.midpoint()[Geom::Y], + r.dimensions()[Geom::X] / 2, r.dimensions()[Geom::Y] / 2); + + double rdimx = r.dimensions()[Geom::X]; + double rdimy = r.dimensions()[Geom::Y]; + + Inkscape::Util::Quantity rdimx_q = Inkscape::Util::Quantity(rdimx, "px"); + Inkscape::Util::Quantity rdimy_q = Inkscape::Util::Quantity(rdimy, "px"); + Glib::ustring xs = rdimx_q.string(_desktop->namedview->display_units); + Glib::ustring ys = rdimy_q.string(_desktop->namedview->display_units); + + if (state & GDK_CONTROL_MASK) { + int ratio_x, ratio_y; + bool is_golden_ratio = false; + + if (fabs (rdimx) > fabs (rdimy)) { + if (fabs(rdimx / rdimy - goldenratio) < 1e-6) { + is_golden_ratio = true; + } + + ratio_x = (int) rint (rdimx / rdimy); + ratio_y = 1; + } else { + if (fabs(rdimy / rdimx - goldenratio) < 1e-6) { + is_golden_ratio = true; + } + + ratio_x = 1; + ratio_y = (int) rint (rdimy / rdimx); + } + + if (!is_golden_ratio) { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Ellipse</b>: %s × %s (constrained to ratio %d:%d); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str(), ratio_x, ratio_y); + } else { + if (ratio_y == 1) { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Ellipse</b>: %s × %s (constrained to golden ratio 1.618 : 1); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Ellipse</b>: %s × %s (constrained to golden ratio 1 : 1.618); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } + } + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Ellipse</b>: %s × %s; with <b>Ctrl</b> to make circle, integer-ratio, or golden-ratio ellipse; with <b>Shift</b> to draw around the starting point"), xs.c_str(), ys.c_str()); + } +} + +void ArcTool::finishItem() { + this->message_context->clear(); + + if (this->arc != nullptr) { + if (this->arc->rx.computed == 0 || this->arc->ry.computed == 0) { + this->cancel(); // Don't allow the creating of zero sized arc, for example when the start and and point snap to the snap grid point + return; + } + + this->arc->updateRepr(); + this->arc->doWriteTransform(this->arc->transform, nullptr, true); + + _desktop->getSelection()->set(this->arc); + + DocumentUndo::done(_desktop->getDocument(), _("Create ellipse"), INKSCAPE_ICON("draw-ellipse")); + + this->arc = nullptr; + } +} + +void ArcTool::cancel() { + _desktop->getSelection()->clear(); + ungrabCanvasEvents(); + + if (this->arc != nullptr) { + this->arc->deleteObject(); + this->arc = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + DocumentUndo::cancel(_desktop->getDocument()); +} + +} +} +} + + +/* + 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/tools/arc-tool.h b/src/ui/tools/arc-tool.h new file mode 100644 index 0000000..312f943 --- /dev/null +++ b/src/ui/tools/arc-tool.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_ARC_CONTEXT_H +#define SEEN_ARC_CONTEXT_H + +/* + * Ellipse drawing context + * + * Authors: + * Mitsuru Oka + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2000-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> + +#include <2geom/point.h> +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" + +class SPItem; +class SPGenericEllipse; + +namespace Inkscape { + class Selection; +} + +#define SP_ARC_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ArcTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_ARC_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::ArcTool*>(obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ArcTool : public ToolBase { +public: + ArcTool(SPDesktop *desktop); + ~ArcTool() override; + + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; +private: + SPGenericEllipse *arc; + + Geom::Point center; + + sigc::connection sel_changed_connection; + + void selection_changed(Inkscape::Selection* selection); + + void drag(Geom::Point pt, guint state); + void finishItem(); + void cancel(); +}; + +} +} +} + +#endif /* !SEEN_ARC_CONTEXT_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 : diff --git a/src/ui/tools/box3d-tool.cpp b/src/ui/tools/box3d-tool.cpp new file mode 100644 index 0000000..3efe20f --- /dev/null +++ b/src/ui/tools/box3d-tool.cpp @@ -0,0 +1,566 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 3D box drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2007 Maximilian Albert <Anhalter42@gmx.de> + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2000-2005 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "perspective-line.h" +#include "selection-chemistry.h" +#include "selection.h" + +#include "include/macros.h" + +#include "object/box3d-side.h" +#include "object/box3d.h" +#include "object/sp-defs.h" +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/shape-editor.h" +#include "ui/tools/box3d-tool.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +Box3dTool::Box3dTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/shapes/3dbox", "box.svg") + , _vpdrag(nullptr) + , box3d(nullptr) + , ctrl_dragged(false) + , extruded(false) +{ + this->shape_editor = new ShapeEditor(_desktop); + + SPItem *item = desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = desktop->getSelection()->connectChanged( + sigc::mem_fun(this, &Box3dTool::selection_changed) + ); + + this->_vpdrag = new Box3D::VPDrag(desktop->getDocument()); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +Box3dTool::~Box3dTool() { + ungrabCanvasEvents(); + this->finishItem(); + this->sel_changed_connection.disconnect(); + + this->enableGrDrag(false); + + delete (this->_vpdrag); + this->_vpdrag = nullptr; + + delete this->shape_editor; + this->shape_editor = nullptr; +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + */ +void Box3dTool::selection_changed(Inkscape::Selection* selection) { + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); + + if (selection->perspList().size() == 1) { + // selecting a single box changes the current perspective + _desktop->doc()->setCurrentPersp3D(selection->perspList().front()); + } +} + +/* Create a default perspective in document defs if none is present (which can happen, among other + * circumstances, after 'vacuum defs' or when a pre-0.46 file is opened). + */ +static void sp_box3d_context_ensure_persp_in_defs(SPDocument *document) { + SPDefs *defs = document->getDefs(); + + bool has_persp = false; + for (auto& child: defs->children) { + if (SP_IS_PERSP3D(&child)) { + has_persp = true; + break; + } + } + + if (!has_persp) { + document->setCurrentPersp3D(Persp3D::create_xml_element (document)); + } +} + +bool Box3dTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if ( event->button.button == 1) { + this->setup_for_drag_start(event); + //ret = TRUE; + } + break; + // motion and release are always on root (why?) + default: + break; + } + +// if (((ToolBaseClass *) sp_box3d_context_parent_class)->item_handler) { +// ret = ((ToolBaseClass *) sp_box3d_context_parent_class)->item_handler(event_context, item, event); +// } + // CPPIFY: ret is always overwritten... + ret = ToolBase::item_handler(item, event); + + return ret; +} + +bool Box3dTool::root_handler(GdkEvent* event) { + static bool dragging; + + SPDocument *document = _desktop->getDocument(); + auto const y_dir = _desktop->yaxisdir(); + Inkscape::Selection *selection = _desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + Persp3D *cur_persp = document->getCurrentPersp3D(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + gint ret = FALSE; + switch (event->type) { + case GDK_BUTTON_PRESS: + if ( event->button.button == 1) { + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point button_dt(_desktop->w2d(button_w)); + + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + // remember clicked box3d, *not* disregarding groups (since a 3D box is a group), honoring Alt + this->item_to_select = sp_event_context_find_item (_desktop, button_w, event->button.state & GDK_MOD1_MASK, event->button.state & GDK_CONTROL_MASK); + + dragging = true; + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop, true, this->box3d); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + this->center = button_dt; + + this->drag_origin = button_dt; + this->drag_ptB = button_dt; + this->drag_ptC = button_dt; + + // This can happen after saving when the last remaining perspective was purged and must be recreated. + if (!cur_persp) { + sp_box3d_context_ensure_persp_in_defs(document); + cur_persp = document->getCurrentPersp3D(); + } + + /* Projective preimages of clicked point under current perspective */ + this->drag_origin_proj = cur_persp->perspective_impl->tmat.preimage (button_dt, 0, Proj::Z); + this->drag_ptB_proj = this->drag_origin_proj; + this->drag_ptC_proj = this->drag_origin_proj; + this->drag_ptC_proj.normalize(); + this->drag_ptC_proj[Proj::Z] = 0.25; + + grabCanvasEvents(); + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && ( event->motion.state & GDK_BUTTON1_MASK )) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->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 draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop, true, this->box3d); + m.freeSnapReturnByRef(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + this->ctrl_dragged = event->motion.state & GDK_CONTROL_MASK; + + if ((event->motion.state & GDK_SHIFT_MASK) && !this->extruded && this->box3d) { + // once shift is pressed, set this->extruded + this->extruded = true; + } + + if (!this->extruded) { + this->drag_ptB = motion_dt; + this->drag_ptC = motion_dt; + + this->drag_ptB_proj = cur_persp->perspective_impl->tmat.preimage (motion_dt, 0, Proj::Z); + this->drag_ptC_proj = this->drag_ptB_proj; + this->drag_ptC_proj.normalize(); + this->drag_ptC_proj[Proj::Z] = 0.25; + } else { + // Without Ctrl, motion of the extruded corner is constrained to the + // perspective line from drag_ptB to vanishing point Y. + if (!this->ctrl_dragged) { + /* snapping */ + Box3D::PerspectiveLine pline (this->drag_ptB, Proj::Z, document->getCurrentPersp3D()); + this->drag_ptC = pline.closest_to (motion_dt); + + this->drag_ptB_proj.normalize(); + this->drag_ptC_proj = cur_persp->perspective_impl->tmat.preimage (this->drag_ptC, this->drag_ptB_proj[Proj::X], Proj::X); + } else { + this->drag_ptC = motion_dt; + + this->drag_ptB_proj.normalize(); + this->drag_ptC_proj = cur_persp->perspective_impl->tmat.preimage (motion_dt, this->drag_ptB_proj[Proj::X], Proj::X); + } + + m.freeSnapReturnByRef(this->drag_ptC, Inkscape::SNAPSOURCE_NODE_HANDLE); + } + + m.unSetup(); + + this->drag(event->motion.state); + + ret = TRUE; + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + + if (event->button.button == 1) { + dragging = false; + this->discard_delayed_snap_event(); + + if (!this->within_tolerance) { + // we've been dragging (or switched tools if !box3d), finish the box + if (this->box3d) { + _desktop->getSelection()->set(this->box3d); // Updating the selection will send signals to the box3d-toolbar ... + } + this->finishItem(); // .. but finishItem() will be called from the deconstructor too and shall NOT fire such signals! + } else if (this->item_to_select) { + // no dragging, select clicked box3d if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = nullptr; + ret = TRUE; + ungrabCanvasEvents(); + } + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // prevent the zoom field from activation + if (!MOD__CTRL_ONLY(event)) + ret = TRUE; + break; + + case GDK_KEY_bracketright: + document->getCurrentPersp3D()->rotate_VP (Proj::X, 180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid")); + ret = true; + break; + + case GDK_KEY_bracketleft: + document->getCurrentPersp3D()->rotate_VP (Proj::X, -180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid")); + ret = true; + break; + + case GDK_KEY_parenright: + document->getCurrentPersp3D()->rotate_VP (Proj::Y, 180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid")); + ret = true; + break; + + case GDK_KEY_parenleft: + document->getCurrentPersp3D()->rotate_VP (Proj::Y, -180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid")); + ret = true; + break; + + case GDK_KEY_braceright: + document->getCurrentPersp3D()->rotate_VP (Proj::Z, 180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid")); + ret = true; + break; + + case GDK_KEY_braceleft: + document->getCurrentPersp3D()->rotate_VP (Proj::Z, -180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid")); + ret = true; + break; + + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + _desktop->selection->toGuides(); + ret = true; + } + break; + + case GDK_KEY_p: + case GDK_KEY_P: + if (MOD__SHIFT_ONLY(event)) { + if (document->getCurrentPersp3D()) { + document->getCurrentPersp3D()->print_debugging_info(); + } + ret = true; + } + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("box3d-angle-x"); + ret = TRUE; + } + if (MOD__SHIFT_ONLY(event)) { + Persp3D::toggle_VPs(selection->perspList(), Proj::X); + this->_vpdrag->updateLines(); // FIXME: Shouldn't this be done automatically? + ret = true; + } + break; + + case GDK_KEY_y: + case GDK_KEY_Y: + if (MOD__SHIFT_ONLY(event)) { + Persp3D::toggle_VPs(selection->perspList(), Proj::Y); + this->_vpdrag->updateLines(); // FIXME: Shouldn't this be done automatically? + ret = true; + } + break; + + case GDK_KEY_z: + case GDK_KEY_Z: + if (MOD__SHIFT_ONLY(event)) { + Persp3D::toggle_VPs(selection->perspList(), Proj::Z); + this->_vpdrag->updateLines(); // FIXME: Shouldn't this be done automatically? + ret = true; + } + break; + + case GDK_KEY_Escape: + _desktop->getSelection()->clear(); + //TODO: make dragging escapable by Esc + break; + + case GDK_KEY_space: + if (dragging) { + ungrabCanvasEvents(); + dragging = false; + this->discard_delayed_snap_event(); + if (!this->within_tolerance) { + // we've been dragging (or switched tools if !box3d), finish the box + if (this->box3d) { + _desktop->getSelection()->set(this->box3d); // Updating the selection will send signals to the box3d-toolbar ... + } + this->finishItem(); // .. but finishItem() will be called from the deconstructor too and shall NOT fire such signals! + } + // do not return true, so that space would work switching to selector + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void Box3dTool::drag(guint /*state*/) { + if (!this->box3d) { + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return; + } + + // Create object + SPBox3D *box3d = SPBox3D::createBox3D(currentLayer()); + + // Set style + _desktop->applyCurrentOrToolStyle(box3d, "/tools/shapes/3dbox", false); + + this->box3d = box3d; + + // TODO: Incorporate this in box3d-side.cpp! + for (int i = 0; i < 6; ++i) { + Box3DSide *side = Box3DSide::createBox3DSide(box3d); + + guint desc = Box3D::int_to_face(i); + + Box3D::Axis plane = (Box3D::Axis) (desc & 0x7); + plane = (Box3D::is_plane(plane) ? plane : Box3D::orth_plane_or_axis(plane)); + side->dir1 = Box3D::extract_first_axis_direction(plane); + side->dir2 = Box3D::extract_second_axis_direction(plane); + side->front_or_rear = (Box3D::FrontOrRear) (desc & 0x8); + + // Set style + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + Glib::ustring descr = "/desktop/"; + descr += side->axes_string(); + descr += "/style"; + + Glib::ustring cur_style = prefs->getString(descr); + + bool use_current = prefs->getBool("/tools/shapes/3dbox/usecurrent", false); + + if (use_current && !cur_style.empty()) { + // use last used style + side->setAttribute("style", cur_style); + } else { + // use default style + Glib::ustring tool_path = Glib::ustring::compose("/tools/shapes/3dbox/%1", + side->axes_string()); + _desktop->applyCurrentOrToolStyle(side, tool_path, false); + } + + side->updateRepr(); // calls Box3DSide::write() and updates, e.g., the axes string description + } + + this->box3d->set_z_orders(); + this->box3d->updateRepr(); + + // TODO: It would be nice to show the VPs during dragging, but since there is no selection + // at this point (only after finishing the box), we must do this "manually" + /* this._vpdrag->updateDraggers(); */ + } + + g_assert(this->box3d); + + this->box3d->orig_corner0 = this->drag_origin_proj; + this->box3d->orig_corner7 = this->drag_ptC_proj; + + this->box3d->check_for_swapped_coords(); + + /* we need to call this from here (instead of from SPBox3D::position_set(), for example) + because z-order setting must not interfere with display updates during undo/redo */ + this->box3d->set_z_orders (); + + this->box3d->position_set(); + + // status text + this->message_context->setF(Inkscape::NORMAL_MESSAGE, "%s", _("<b>3D Box</b>; with <b>Shift</b> to extrude along the Z axis")); +} + +void Box3dTool::finishItem() { + this->message_context->clear(); + this->ctrl_dragged = false; + this->extruded = false; + + if (this->box3d != nullptr) { + SPDocument *doc = _desktop->getDocument(); + + if (!doc || !doc->getCurrentPersp3D()) { + return; + } + + this->box3d->orig_corner0 = this->drag_origin_proj; + this->box3d->orig_corner7 = this->drag_ptC_proj; + + this->box3d->updateRepr(); + + this->box3d->relabel_corners(); + + DocumentUndo::done(_desktop->getDocument(), _("Create 3D box"), INKSCAPE_ICON("draw-cuboid")); + + this->box3d = nullptr; + } +} + +} +} +} + +/* + 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/tools/box3d-tool.h b/src/ui/tools/box3d-tool.h new file mode 100644 index 0000000..17bec3a --- /dev/null +++ b/src/ui/tools/box3d-tool.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_BOX3D_CONTEXT_H__ +#define __SP_BOX3D_CONTEXT_H__ + +/* + * 3D box drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2007 Maximilian Albert <Anhalter42@gmx.de> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> + +#include <2geom/point.h> +#include <sigc++/connection.h> + +#include "proj_pt.h" +#include "vanishing-point.h" + +#include "ui/tools/tool-base.h" + +class SPItem; +class SPBox3D; + +namespace Box3D { + struct VPDrag; +} + +namespace Inkscape { + class Selection; +} + +#define SP_BOX3D_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::Box3dTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_BOX3D_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::Box3dTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class Box3dTool : public ToolBase { +public: + Box3dTool(SPDesktop *desktop); + ~Box3dTool() override; + + Box3D::VPDrag *_vpdrag; + + bool root_handler(GdkEvent *event) override; + bool item_handler(SPItem *item, GdkEvent *event) override; + +private: + SPBox3D* box3d; + Geom::Point center; + + /** + * save three corners while dragging: + * 1) the starting point (already done by the event_context) + * 2) drag_ptB --> the opposite corner of the front face (before pressing shift) + * 3) drag_ptC --> the "extruded corner" (which coincides with the mouse pointer location + * if we are ctrl-dragging but is constrained to the perspective line from drag_ptC + * to the vanishing point Y otherwise) + */ + Geom::Point drag_origin; + Geom::Point drag_ptB; + Geom::Point drag_ptC; + + Proj::Pt3 drag_origin_proj; + Proj::Pt3 drag_ptB_proj; + Proj::Pt3 drag_ptC_proj; + + bool ctrl_dragged; /* whether we are ctrl-dragging */ + bool extruded; /* whether shift-dragging already occurred (i.e. the box is already extruded) */ + + sigc::connection sel_changed_connection; + + void selection_changed(Inkscape::Selection* selection); + + void drag(guint state); + void finishItem(); +}; + +} +} +} + +#endif /* __SP_BOX3D_CONTEXT_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/tools/calligraphic-tool.cpp b/src/ui/tools/calligraphic-tool.cpp new file mode 100644 index 0000000..aced95d --- /dev/null +++ b/src/ui/tools/calligraphic-tool.cpp @@ -0,0 +1,1192 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Handwriting-like drawing mode + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * MenTaLguY <mental@rydia.net> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * The original dynadraw code: + * Paul Haeberli <paul@sgi.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2005-2007 bulia byak + * Copyright (C) 2006 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noDYNA_DRAW_VERBOSE + +#include "ui/tools/calligraphic-tool.h" + +#include <cstring> +#include <numeric> +#include <string> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> +#include <gtk/gtk.h> + +#include <2geom/bezier-utils.h> +#include <2geom/circle.h> +#include <2geom/pathvector.h> + +#include "context-fns.h" +#include "desktop-events.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-context.h" +#include "selection.h" + +#include "display/control/canvas-item-bpath.h" +#include "display/control/canvas-item-drawing.h" // ctx +#include "display/curve.h" +#include "display/drawing.h" + +#include "include/macros.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "path/path-util.h" + +#include "svg/svg.h" + +#include "ui/icon-names.h" +#include "ui/tools/freehand-base.h" + +#include "util/units.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::Quantity; +using Inkscape::Util::Unit; +using Inkscape::Util::unit_table; + +#define DDC_RED_RGBA 0xff0000ff + +#define TOLERANCE_CALLIGRAPHIC 0.1 + +#define DYNA_EPSILON 0.5e-6 +#define DYNA_EPSILON_START 0.5e-2 +#define DYNA_VEL_START 1e-5 + +#define DYNA_MIN_WIDTH 1.0e-6 + +namespace Inkscape { +namespace UI { +namespace Tools { + +CalligraphicTool::CalligraphicTool(SPDesktop *desktop) + : DynamicBase(desktop, "/tools/calligraphic", "calligraphy.svg") + , keep_selected(true) + , hatch_spacing(0) + , hatch_spacing_step(0) + , hatch_item(nullptr) + , hatch_livarot_path(nullptr) + , hatch_last_nearest(Geom::Point(0, 0)) + , hatch_last_pointer(Geom::Point(0, 0)) + , hatch_escaped(false) + , just_started_drawing(false) + , trace_bg(false) +{ + this->vel_thin = 0.1; + this->flatness = -0.9; + this->cap_rounding = 0.0; + this->abs_width = false; + + this->accumulated.reset(new SPCurve()); + this->currentcurve.reset(new SPCurve()); + + this->cal1.reset(new SPCurve()); + this->cal2.reset(new SPCurve()); + + currentshape = new Inkscape::CanvasItemBpath(desktop->getCanvasSketch()); + currentshape->set_stroke(0x0); + currentshape->set_fill(DDC_RED_RGBA, SP_WIND_RULE_EVENODD); + + /* fixme: Cannot we cascade it to root more clearly? */ + currentshape->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), desktop)); + + hatch_area = new Inkscape::CanvasItemBpath(desktop->getCanvasControls()); + hatch_area->set_fill(0x0, SP_WIND_RULE_EVENODD); + hatch_area->set_stroke(0x0000007f); + hatch_area->set_pickable(false); + hatch_area->hide(); + + sp_event_context_read(this, "mass"); + sp_event_context_read(this, "wiggle"); + sp_event_context_read(this, "angle"); + sp_event_context_read(this, "width"); + sp_event_context_read(this, "thinning"); + sp_event_context_read(this, "tremor"); + sp_event_context_read(this, "flatness"); + sp_event_context_read(this, "tracebackground"); + sp_event_context_read(this, "usepressure"); + sp_event_context_read(this, "usetilt"); + sp_event_context_read(this, "abs_width"); + sp_event_context_read(this, "keep_selected"); + sp_event_context_read(this, "cap_rounding"); + + this->is_drawing = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/calligraphic/selcue")) { + this->enableSelectionCue(); + } +} + +CalligraphicTool::~CalligraphicTool() +{ + if (hatch_area) { + delete hatch_area; + hatch_area = nullptr; + } + if (currentshape) { + delete currentshape; + currentshape = nullptr; + } +} + +void CalligraphicTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "tracebackground") { + this->trace_bg = val.getBool(); + } else if (path == "keep_selected") { + this->keep_selected = val.getBool(); + } else { + //pass on up to parent class to handle common attributes. + DynamicBase::set(val); + } + + //g_print("DDC: %g %g %g %g\n", ddc->mass, ddc->drag, ddc->angle, ddc->width); +} + +static double +flerp(double f0, double f1, double p) +{ + return f0 + ( f1 - f0 ) * p; +} + +void CalligraphicTool::reset(Geom::Point p) { + this->last = this->cur = this->getNormalizedPoint(p); + + this->vel = Geom::Point(0,0); + this->vel_max = 0; + this->acc = Geom::Point(0,0); + this->ang = Geom::Point(0,0); + this->del = Geom::Point(0,0); +} + +void CalligraphicTool::extinput(GdkEvent *event) { + if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &this->pressure)) { + this->pressure = CLAMP (this->pressure, DDC_MIN_PRESSURE, DDC_MAX_PRESSURE); + } else { + this->pressure = DDC_DEFAULT_PRESSURE; + } + + if (gdk_event_get_axis (event, GDK_AXIS_XTILT, &this->xtilt)) { + this->xtilt = CLAMP (this->xtilt, DDC_MIN_TILT, DDC_MAX_TILT); + } else { + this->xtilt = DDC_DEFAULT_TILT; + } + + if (gdk_event_get_axis (event, GDK_AXIS_YTILT, &this->ytilt)) { + this->ytilt = CLAMP (this->ytilt, DDC_MIN_TILT, DDC_MAX_TILT); + } else { + this->ytilt = DDC_DEFAULT_TILT; + } +} + + +bool CalligraphicTool::apply(Geom::Point p) { + Geom::Point n = this->getNormalizedPoint(p); + + /* Calculate mass and drag */ + double const mass = flerp(1.0, 160.0, this->mass); + double const drag = flerp(0.0, 0.5, this->drag * this->drag); + + /* Calculate force and acceleration */ + Geom::Point force = n - this->cur; + + // If force is below the absolute threshold DYNA_EPSILON, + // or we haven't yet reached DYNA_VEL_START (i.e. at the beginning of stroke) + // _and_ the force is below the (higher) DYNA_EPSILON_START threshold, + // discard this move. + // This prevents flips, blobs, and jerks caused by microscopic tremor of the tablet pen, + // especially bothersome at the start of the stroke where we don't yet have the inertia to + // smooth them out. + if ( Geom::L2(force) < DYNA_EPSILON || (this->vel_max < DYNA_VEL_START && Geom::L2(force) < DYNA_EPSILON_START)) { + return FALSE; + } + + this->acc = force / mass; + + /* Calculate new velocity */ + this->vel += this->acc; + + if (Geom::L2(this->vel) > this->vel_max) + this->vel_max = Geom::L2(this->vel); + + /* Calculate angle of drawing tool */ + + double a1; + if (this->usetilt) { + // 1a. calculate nib angle from input device tilt: + if (this->xtilt == 0 && this->ytilt == 0) { + // to be sure that atan2 in the computation below + // would not crash or return NaN. + a1 = 0; + } else { + Geom::Point dir(-this->xtilt, this->ytilt); + a1 = atan2(dir); + } + } + else { + // 1b. fixed dc->angle (absolutely flat nib): + a1 = ( this->angle / 180.0 ) * M_PI; + } + a1 *= -_desktop->yaxisdir(); + if (this->flatness < 0.0) { + // flips direction. Useful when this->usetilt + // allows simulating both pen and calligraphic brush + a1 *= -1; + } + a1 = fmod(a1, M_PI); + if (a1 > 0.5*M_PI) { + a1 -= M_PI; + } else if (a1 <= -0.5*M_PI) { + a1 += M_PI; + } + + // 2. perpendicular to dc->vel (absolutely non-flat nib): + gdouble const mag_vel = Geom::L2(this->vel); + if ( mag_vel < DYNA_EPSILON ) { + return FALSE; + } + Geom::Point ang2 = Geom::rot90(this->vel) / mag_vel; + + // 3. Average them using flatness parameter: + // calculate angles + double a2 = atan2(ang2); + // flip a2 to force it to be in the same half-circle as a1 + bool flipped = false; + if (fabs (a2-a1) > 0.5*M_PI) { + a2 += M_PI; + flipped = true; + } + // normalize a2 + if (a2 > M_PI) + a2 -= 2*M_PI; + if (a2 < -M_PI) + a2 += 2*M_PI; + // find the flatness-weighted bisector angle, unflip if a2 was flipped + // FIXME: when dc->vel is oscillating around the fixed angle, the new_ang flips back and forth. How to avoid this? + double new_ang = a1 + (1 - fabs(this->flatness)) * (a2 - a1) - (flipped? M_PI : 0); + + // Try to detect a sudden flip when the new angle differs too much from the previous for the + // current velocity; in that case discard this move + double angle_delta = Geom::L2(Geom::Point (cos (new_ang), sin (new_ang)) - this->ang); + if ( angle_delta / Geom::L2(this->vel) > 4000 ) { + return FALSE; + } + + // convert to point + this->ang = Geom::Point (cos (new_ang), sin (new_ang)); + +// g_print ("force %g acc %g vel_max %g vel %g a1 %g a2 %g new_ang %g\n", Geom::L2(force), Geom::L2(dc->acc), dc->vel_max, Geom::L2(dc->vel), a1, a2, new_ang); + + /* Apply drag */ + this->vel *= 1.0 - drag; + + /* Update position */ + this->last = this->cur; + this->cur += this->vel; + + return TRUE; +} + +void CalligraphicTool::brush() { + g_assert( this->npoints >= 0 && this->npoints < SAMPLING_SIZE ); + + // How much velocity thins strokestyle + double vel_thin = flerp (0, 160, this->vel_thin); + + // Influence of pressure on thickness + double pressure_thick = (this->usepressure ? this->pressure : 1.0); + + // get the real brush point, not the same as pointer (affected by hatch tracking and/or mass + // drag) + Geom::Point brush = getViewPoint(this->cur); + Geom::Point brush_w = _desktop->d2w(brush); + + double trace_thick = 1; + if (this->trace_bg) { + // Trace background, use single pixel under brush. + Geom::IntRect area = Geom::IntRect::from_xywh(brush_w.floor(), Geom::IntPoint(1, 1)); + + Inkscape::CanvasItemDrawing *canvas_item_drawing = _desktop->getCanvasDrawing(); + Inkscape::Drawing *drawing = canvas_item_drawing->get_drawing(); + + // Ensure drawing up-to-date. (Is this really necessary?) + drawing->update(); + + // Get average color. + double R, G, B, A; + drawing->average_color(area, R, G, B, A); + + // Convert to thickness. + double max = MAX (MAX (R, G), B); + double min = MIN (MIN (R, G), B); + double L = A * (max + min)/2 + (1 - A); // blend with white bg + trace_thick = 1 - L; + //g_print ("L %g thick %g\n", L, trace_thick); + } + + double width = (pressure_thick * trace_thick - vel_thin * Geom::L2(this->vel)) * this->width; + + double tremble_left = 0, tremble_right = 0; + if (this->tremor > 0) { + // obtain two normally distributed random variables, using polar Box-Muller transform + double x1, x2, w, y1, y2; + do { + x1 = 2.0 * g_random_double_range(0,1) - 1.0; + x2 = 2.0 * g_random_double_range(0,1) - 1.0; + w = x1 * x1 + x2 * x2; + } while ( w >= 1.0 ); + w = sqrt( (-2.0 * log( w ) ) / w ); + y1 = x1 * w; + y2 = x2 * w; + + // deflect both left and right edges randomly and independently, so that: + // (1) dc->tremor=1 corresponds to sigma=1, decreasing dc->tremor narrows the bell curve; + // (2) deflection depends on width, but is upped for small widths for better visual uniformity across widths; + // (3) deflection somewhat depends on speed, to prevent fast strokes looking + // comparatively smooth and slow ones excessively jittery + tremble_left = (y1)*this->tremor * (0.15 + 0.8*width) * (0.35 + 14*Geom::L2(this->vel)); + tremble_right = (y2)*this->tremor * (0.15 + 0.8*width) * (0.35 + 14*Geom::L2(this->vel)); + } + + if ( width < 0.02 * this->width ) { + width = 0.02 * this->width; + } + + double dezoomify_factor = 0.05 * 1000; + if (!this->abs_width) { + dezoomify_factor /= _desktop->current_zoom(); + } + + Geom::Point del_left = dezoomify_factor * (width + tremble_left) * this->ang; + Geom::Point del_right = dezoomify_factor * (width + tremble_right) * this->ang; + + this->point1[this->npoints] = brush + del_left; + this->point2[this->npoints] = brush - del_right; + + this->del = 0.5*(del_left + del_right); + + this->npoints++; +} + +static void +sp_ddc_update_toolbox (SPDesktop *desktop, const gchar *id, double value) +{ + desktop->setToolboxAdjustmentValue (id, value); +} + +void CalligraphicTool::cancel() { + this->dragging = false; + this->is_drawing = false; + + ungrabCanvasEvents(); + + /* Remove all temporary line segments */ + for (auto segment :this->segments) { + delete segment; + } + this->segments.clear(); + + /* reset accumulated curve */ + this->accumulated->reset(); + this->clear_current(); + + if (this->repr) { + this->repr = nullptr; + } +} + +bool CalligraphicTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Unit const *unit = unit_table.getUnit(prefs->getString("/tools/calligraphic/unit")); + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return TRUE; + } + + this->accumulated->reset(); + + if (this->repr) { + this->repr = nullptr; + } + + /* initialize first point */ + this->npoints = 0; + + grabCanvasEvents(); + + ret = TRUE; + + set_high_motion_precision(); + this->is_drawing = true; + this->just_started_drawing = true; + } + break; + case GDK_MOTION_NOTIFY: + { + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + this->extinput(event); + + this->message_context->clear(); + + // for hatching: + double hatch_dist = 0; + Geom::Point hatch_unit_vector(0,0); + Geom::Point nearest(0,0); + Geom::Point pointer(0,0); + Geom::Affine motion_to_curve(Geom::identity()); + + if (event->motion.state & GDK_CONTROL_MASK) { // hatching - sense the item + + SPItem *selected = _desktop->getSelection()->singleItem(); + if (selected && (SP_IS_SHAPE(selected) || SP_IS_TEXT(selected))) { + // One item selected, and it's a path; + // let's try to track it as a guide + + if (selected != this->hatch_item) { + this->hatch_item = selected; + if (this->hatch_livarot_path) + delete this->hatch_livarot_path; + this->hatch_livarot_path = Path_for_item (this->hatch_item, true, true); + if (hatch_livarot_path) { + hatch_livarot_path->ConvertWithBackData(0.01); + } + } + + // calculate pointer point in the guide item's coords + motion_to_curve = selected->dt2i_affine() * selected->i2doc_affine(); + pointer = motion_dt * motion_to_curve; + + // calculate the nearest point on the guide path + std::optional<Path::cut_position> position = get_nearest_position_on_Path(this->hatch_livarot_path, pointer); + if (position) { + nearest = get_point_on_Path(hatch_livarot_path, position->piece, position->t); + + // distance from pointer to nearest + hatch_dist = Geom::L2(pointer - nearest); + // unit-length vector + hatch_unit_vector = (pointer - nearest) / hatch_dist; + + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Guide path selected</b>; start drawing along the guide with <b>Ctrl</b>")); + } + } else { + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Select a guide path</b> to track with <b>Ctrl</b>")); + } + } + + if ( this->is_drawing && (event->motion.state & GDK_BUTTON1_MASK)) { + this->dragging = TRUE; + + if (event->motion.state & GDK_CONTROL_MASK && this->hatch_item) { // hatching + +#define HATCH_VECTOR_ELEMENTS 12 +#define INERTIA_ELEMENTS 24 +#define SPEED_ELEMENTS 12 +#define SPEED_MIN 0.3 +#define SPEED_NORMAL 0.35 +#define INERTIA_FORCE 0.5 + + // speed is the movement of the nearest point along the guide path, divided by + // the movement of the pointer at the same period; it is averaged for the last + // SPEED_ELEMENTS motion events. Normally, as you track the guide path, speed + // is about 1, i.e. the nearest point on the path is moved by about the same + // distance as the pointer. If the speed starts to decrease, we are losing + // contact with the guide; if it drops below SPEED_MIN, we are on our own and + // not attracted to guide anymore. Most often this happens when you have + // tracked to the end of a guide calligraphic stroke and keep moving + // further. We try to handle this situation gracefully: not stick with the + // guide forever but let go of it smoothly and without sharp jerks (non-zero + // mass recommended; with zero mass, jerks are still quite noticeable). + + double speed = 1; + if (Geom::L2(this->hatch_last_nearest) != 0) { + // the distance nearest moved since the last motion event + double nearest_moved = Geom::L2(nearest - this->hatch_last_nearest); + // the distance pointer moved since the last motion event + double pointer_moved = Geom::L2(pointer - this->hatch_last_pointer); + // store them in stacks limited to SPEED_ELEMENTS + this->hatch_nearest_past.push_front(nearest_moved); + if (this->hatch_nearest_past.size() > SPEED_ELEMENTS) + this->hatch_nearest_past.pop_back(); + this->hatch_pointer_past.push_front(pointer_moved); + if (this->hatch_pointer_past.size() > SPEED_ELEMENTS) + this->hatch_pointer_past.pop_back(); + + // If the stacks are full, + if (this->hatch_nearest_past.size() == SPEED_ELEMENTS) { + // calculate the sums of all stored movements + double nearest_sum = std::accumulate (this->hatch_nearest_past.begin(), this->hatch_nearest_past.end(), 0.0); + double pointer_sum = std::accumulate (this->hatch_pointer_past.begin(), this->hatch_pointer_past.end(), 0.0); + // and divide to get the speed + speed = nearest_sum/pointer_sum; + //g_print ("nearest sum %g pointer_sum %g speed %g\n", nearest_sum, pointer_sum, speed); + } + } + + if ( this->hatch_escaped // already escaped, do not reattach + || (speed < SPEED_MIN) // stuck; most likely reached end of traced stroke + || (this->hatch_spacing > 0 && hatch_dist > 50 * this->hatch_spacing) // went too far from the guide + ) { + // We are NOT attracted to the guide! + + //g_print ("\nlast_nearest %g %g nearest %g %g pointer %g %g pos %d %g\n", dc->last_nearest[Geom::X], dc->last_nearest[Geom::Y], nearest[Geom::X], nearest[Geom::Y], pointer[Geom::X], pointer[Geom::Y], position->piece, position->t); + + // Remember hatch_escaped so we don't get + // attracted again until the end of this stroke + this->hatch_escaped = true; + + if (this->inertia_vectors.size() >= INERTIA_ELEMENTS/2) { // move by inertia + Geom::Point moved_past_escape = motion_dt - this->inertia_vectors.front(); + Geom::Point inertia = + this->inertia_vectors.front() - this->inertia_vectors.back(); + + double dot = Geom::dot (moved_past_escape, inertia); + dot /= Geom::L2(moved_past_escape) * Geom::L2(inertia); + + if (dot > 0) { // mouse is still moving in approx the same direction + Geom::Point should_have_moved = + (inertia) * (1/Geom::L2(inertia)) * Geom::L2(moved_past_escape); + motion_dt = this->inertia_vectors.front() + + (INERTIA_FORCE * should_have_moved + (1 - INERTIA_FORCE) * moved_past_escape); + } + } + + } else { + + // Calculate angle cosine of this vector-to-guide and all past vectors + // summed, to detect if we accidentally flipped to the other side of the + // guide + Geom::Point hatch_vector_accumulated = std::accumulate + (this->hatch_vectors.begin(), this->hatch_vectors.end(), Geom::Point(0,0)); + double dot = Geom::dot (pointer - nearest, hatch_vector_accumulated); + dot /= Geom::L2(pointer - nearest) * Geom::L2(hatch_vector_accumulated); + + if (this->hatch_spacing != 0) { // spacing was already set + double target; + if (speed > SPEED_NORMAL) { + // all ok, strictly obey the spacing + target = this->hatch_spacing; + } else { + // looks like we're starting to lose speed, + // so _gradually_ let go attraction to prevent jerks + target = (this->hatch_spacing * speed + hatch_dist * (SPEED_NORMAL - speed))/SPEED_NORMAL; + } + if (!std::isnan(dot) && dot < -0.5) {// flip + target = -target; + } + + // This is the track pointer that we will use instead of the real one + Geom::Point new_pointer = nearest + target * hatch_unit_vector; + + // some limited feedback: allow persistent pulling to slightly change + // the spacing + this->hatch_spacing += (hatch_dist - this->hatch_spacing)/3500; + + // return it to the desktop coords + motion_dt = new_pointer * motion_to_curve.inverse(); + + if (speed >= SPEED_NORMAL) { + this->inertia_vectors.push_front(motion_dt); + if (this->inertia_vectors.size() > INERTIA_ELEMENTS) + this->inertia_vectors.pop_back(); + } + + } else { + // this is the first motion event, set the dist + this->hatch_spacing = hatch_dist; + } + + // remember last points + this->hatch_last_pointer = pointer; + this->hatch_last_nearest = nearest; + + this->hatch_vectors.push_front(pointer - nearest); + if (this->hatch_vectors.size() > HATCH_VECTOR_ELEMENTS) + this->hatch_vectors.pop_back(); + } + + this->message_context->set(Inkscape::NORMAL_MESSAGE, this->hatch_escaped? _("Tracking: <b>connection to guide path lost!</b>") : _("<b>Tracking</b> a guide path")); + + } else { + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> a calligraphic stroke")); + } + + if (this->just_started_drawing) { + this->just_started_drawing = false; + this->reset(motion_dt); + } + + if (!this->apply(motion_dt)) { + ret = TRUE; + break; + } + + if ( this->cur != this->last ) { + this->brush(); + g_assert( this->npoints > 0 ); + this->fit_and_split(false); + } + ret = TRUE; + } + + Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); // Unit circle centered at origin. + + // Draw the hatching circle if necessary + if (event->motion.state & GDK_CONTROL_MASK) { + if (this->hatch_spacing == 0 && hatch_dist != 0) { + // Haven't set spacing yet: gray, center free, update radius live + + Geom::Point c = _desktop->w2d(motion_w); + Geom::Affine const sm (Geom::Scale(hatch_dist, hatch_dist) * Geom::Translate(c)); + path *= sm; + + hatch_area->set_bpath(path, true); + hatch_area->set_stroke(0x7f7f7fff); + hatch_area->show(); + + } else if (this->dragging && !this->hatch_escaped && hatch_dist != 0) { + // Tracking: green, center snapped, fixed radius + + Geom::Point c = motion_dt; + Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c)); + path *= sm; + + hatch_area->set_bpath(path, true); + hatch_area->set_stroke(0x00FF00ff); + hatch_area->show(); + + } else if (this->dragging && this->hatch_escaped && hatch_dist != 0) { + // Tracking escaped: red, center free, fixed radius + + Geom::Point c = motion_dt; + Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c)); + path *= sm; + + hatch_area->set_bpath(path, true); + hatch_area->set_stroke(0xff0000ff); + hatch_area->show(); + + } else { + // Not drawing but spacing set: gray, center snapped, fixed radius + + Geom::Point c = (nearest + this->hatch_spacing * hatch_unit_vector) * motion_to_curve.inverse(); + if (!std::isnan(c[Geom::X]) && !std::isnan(c[Geom::Y]) && this->hatch_spacing!=0) { + Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c)); + path *= sm; + + hatch_area->set_bpath(path, true); + hatch_area->set_stroke(0x7f7f7fff); + hatch_area->show(); + } + } + } else { + hatch_area->hide(); + } + } + break; + + + case GDK_BUTTON_RELEASE: + { + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + + ungrabCanvasEvents(); + + set_high_motion_precision(false); + this->is_drawing = false; + + if (this->dragging && event->button.button == 1) { + this->dragging = FALSE; + + this->apply(motion_dt); + + /* Remove all temporary line segments */ + for (auto segment : this->segments) { + delete segment; + } + this->segments.clear(); + + /* Create object */ + this->fit_and_split(true); + if (this->accumulate()) + this->set_to_accumulated(event->button.state & GDK_SHIFT_MASK, event->button.state & GDK_MOD1_MASK); // performs document_done + else + g_warning ("Failed to create path: invalid data in dc->cal1 or dc->cal2"); + + /* reset accumulated curve */ + this->accumulated->reset(); + + this->clear_current(); + if (this->repr) { + this->repr = nullptr; + } + + if (!this->hatch_pointer_past.empty()) this->hatch_pointer_past.clear(); + if (!this->hatch_nearest_past.empty()) this->hatch_nearest_past.clear(); + if (!this->inertia_vectors.empty()) this->inertia_vectors.clear(); + if (!this->hatch_vectors.empty()) this->hatch_vectors.clear(); + this->hatch_last_nearest = Geom::Point(0,0); + this->hatch_last_pointer = Geom::Point(0,0); + this->hatch_escaped = false; + this->hatch_item = nullptr; + this->hatch_livarot_path = nullptr; + this->just_started_drawing = false; + + if (this->hatch_spacing != 0 && !this->keep_selected) { + // we do not select the newly drawn path, so increase spacing by step + if (this->hatch_spacing_step == 0) { + this->hatch_spacing_step = this->hatch_spacing; + } + this->hatch_spacing += this->hatch_spacing_step; + } + + this->message_context->clear(); + ret = TRUE; + } else if (!this->dragging + && event->button.button == 1 + && Inkscape::have_viable_layer(_desktop, defaultMessageContext())) + { + spdc_create_single_dot(this, _desktop->w2d(motion_w), "/tools/calligraphic", event->button.state); + ret = TRUE; + } + break; + } + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + if (!MOD__CTRL_ONLY(event)) { + this->angle += 5.0; + if (this->angle > 90.0) + this->angle = 90.0; + sp_ddc_update_toolbox(_desktop, "calligraphy-angle", this->angle); + ret = TRUE; + } + break; + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + if (!MOD__CTRL_ONLY(event)) { + this->angle -= 5.0; + if (this->angle < -90.0) + this->angle = -90.0; + sp_ddc_update_toolbox(_desktop, "calligraphy-angle", this->angle); + ret = TRUE; + } + break; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (!MOD__CTRL_ONLY(event)) { + this->width = Quantity::convert(this->width, "px", unit) + 0.01; + if (this->width > 1.0) + this->width = 1.0; + sp_ddc_update_toolbox (_desktop, "calligraphy-width", this->width * 100); // the same spinbutton is for alt+x + ret = TRUE; + } + break; + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (!MOD__CTRL_ONLY(event)) { + this->width = Quantity::convert(this->width, "px", unit) - 0.01; + if (this->width < 0.00001) + this->width = 0.00001; + sp_ddc_update_toolbox(_desktop, "calligraphy-width", this->width * 100); + ret = TRUE; + } + break; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + this->width = 0.00001; + sp_ddc_update_toolbox(_desktop, "calligraphy-width", this->width * 100); + ret = TRUE; + break; + case GDK_KEY_End: + case GDK_KEY_KP_End: + this->width = 1.0; + sp_ddc_update_toolbox(_desktop, "calligraphy-width", this->width * 100); + ret = TRUE; + break; + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("calligraphy-width"); + ret = TRUE; + } + break; + case GDK_KEY_Escape: + if (this->is_drawing) { + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + case GDK_KEY_z: + case GDK_KEY_Z: + if (MOD__CTRL_ONLY(event) && this->is_drawing) { + // if drawing, cancel, otherwise pass it up for undo + this->cancel(); + ret = TRUE; + } + break; + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + this->message_context->clear(); + this->hatch_spacing = 0; + this->hatch_spacing_step = 0; + break; + default: + break; + } + break; + + default: + break; + } + + if (!ret) { +// if ((SP_EVENT_CONTEXT_CLASS(sp_dyna_draw_context_parent_class))->root_handler) { +// ret = (SP_EVENT_CONTEXT_CLASS(sp_dyna_draw_context_parent_class))->root_handler(event_context, event); +// } + ret = DynamicBase::root_handler(event); + } + + return ret; +} + + +void CalligraphicTool::clear_current() { + /* reset bpath */ + this->currentshape->set_bpath(nullptr); + + /* reset curve */ + this->currentcurve->reset(); + this->cal1->reset(); + this->cal2->reset(); + + /* reset points */ + this->npoints = 0; +} + +void CalligraphicTool::set_to_accumulated(bool unionize, bool subtract) { + if (!this->accumulated->is_empty()) { + if (!this->repr) { + /* Create object */ + Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + /* Set style */ + sp_desktop_apply_style_tool(_desktop, repr, "/tools/calligraphic", false); + + this->repr = repr; + + auto layer = currentLayer(); + SPItem *item=SP_ITEM(layer->appendChildRepr(this->repr)); + Inkscape::GC::release(this->repr); + item->transform = layer->i2doc_affine().inverse(); + item->updateRepr(); + } + + Geom::PathVector pathv = this->accumulated->get_pathvector() * _desktop->dt2doc(); + this->repr->setAttribute("d", sp_svg_write_path(pathv)); + + if (unionize) { + _desktop->getSelection()->add(this->repr); + _desktop->getSelection()->pathUnion(true); + } else if (subtract) { + _desktop->getSelection()->add(this->repr); + _desktop->getSelection()->pathDiff(true); + } else { + if (this->keep_selected) { + _desktop->getSelection()->set(this->repr); + } + } + + // Now we need to write the transform information. + // First, find out whether our repr is still linked to a valid object. In this case, + // we need to write the transform data only for this element. + // Either there was no boolean op or it failed. + SPItem *result = SP_ITEM(_desktop->doc()->getObjectByRepr(this->repr)); + + if (result == nullptr) { + // The boolean operation succeeded. + // Now we fetch the single item, that has been set as selected by the boolean op. + // This is its result. + result = _desktop->getSelection()->singleItem(); + } + result->doWriteTransform(result->transform, nullptr, true); + } else { + if (this->repr) { + sp_repr_unparent(this->repr); + } + + this->repr = nullptr; + } + + DocumentUndo::done(_desktop->getDocument(), _("Draw calligraphic stroke"), INKSCAPE_ICON("draw-calligraphic")); +} + +static void +add_cap(SPCurve &curve, + Geom::Point const &from, + Geom::Point const &to, + double rounding) +{ + if (Geom::L2( to - from ) > DYNA_EPSILON) { + Geom::Point vel = rounding * Geom::rot90( to - from ) / sqrt(2.0); + double mag = Geom::L2(vel); + + Geom::Point v = mag * Geom::rot90( to - from ) / Geom::L2( to - from ); + curve.curveto(from + v, to + v, to); + } +} + +bool CalligraphicTool::accumulate() { + if ( + this->cal1->is_empty() || + this->cal2->is_empty() || + (this->cal1->get_segment_count() <= 0) || + this->cal1->first_path()->closed() + ) { + + this->cal1->reset(); + this->cal2->reset(); + + return false; // failure + } + + auto rev_cal2 = this->cal2->create_reverse(); + + if ((rev_cal2->get_segment_count() <= 0) || rev_cal2->first_path()->closed()) { + this->cal1->reset(); + this->cal2->reset(); + + return false; // failure + } + + Geom::Curve const * dc_cal1_firstseg = this->cal1->first_segment(); + Geom::Curve const * rev_cal2_firstseg = rev_cal2->first_segment(); + Geom::Curve const * dc_cal1_lastseg = this->cal1->last_segment(); + Geom::Curve const * rev_cal2_lastseg = rev_cal2->last_segment(); + + this->accumulated->reset(); /* Is this required ?? */ + + this->accumulated->append(*cal1); + + add_cap(*accumulated, dc_cal1_lastseg->finalPoint(), rev_cal2_firstseg->initialPoint(), cap_rounding); + + this->accumulated->append(*rev_cal2, true); + + add_cap(*accumulated, rev_cal2_lastseg->finalPoint(), dc_cal1_firstseg->initialPoint(), cap_rounding); + + this->accumulated->closepath(); + + this->cal1->reset(); + this->cal2->reset(); + + return true; // success +} + +static double square(double const x) +{ + return x * x; +} + +void CalligraphicTool::fit_and_split(bool release) { + double const tolerance_sq = square(_desktop->w2d().descrim() * TOLERANCE_CALLIGRAPHIC); + +#ifdef DYNA_DRAW_VERBOSE + g_print("[F&S:R=%c]", release?'T':'F'); +#endif + + if (!( this->npoints > 0 && this->npoints < SAMPLING_SIZE )) { + return; // just clicked + } + + if ( this->npoints == SAMPLING_SIZE - 1 || release ) { +#define BEZIER_SIZE 4 +#define BEZIER_MAX_BEZIERS 8 +#define BEZIER_MAX_LENGTH ( BEZIER_SIZE * BEZIER_MAX_BEZIERS ) + +#ifdef DYNA_DRAW_VERBOSE + g_print("[F&S:#] dc->npoints:%d, release:%s\n", + this->npoints, release ? "TRUE" : "FALSE"); +#endif + + /* Current calligraphic */ + if ( this->cal1->is_empty() || this->cal2->is_empty() ) { + /* dc->npoints > 0 */ + /* g_print("calligraphics(1|2) reset\n"); */ + this->cal1->reset(); + this->cal2->reset(); + + this->cal1->moveto(this->point1[0]); + this->cal2->moveto(this->point2[0]); + } + + Geom::Point b1[BEZIER_MAX_LENGTH]; + gint const nb1 = Geom::bezier_fit_cubic_r(b1, this->point1, this->npoints, + tolerance_sq, BEZIER_MAX_BEZIERS); + g_assert( nb1 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b1)) ); + + Geom::Point b2[BEZIER_MAX_LENGTH]; + gint const nb2 = Geom::bezier_fit_cubic_r(b2, this->point2, this->npoints, + tolerance_sq, BEZIER_MAX_BEZIERS); + g_assert( nb2 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b2)) ); + + if ( nb1 != -1 && nb2 != -1 ) { + /* Fit and draw and reset state */ +#ifdef DYNA_DRAW_VERBOSE + g_print("nb1:%d nb2:%d\n", nb1, nb2); +#endif + /* CanvasShape */ + if (! release) { + this->currentcurve->reset(); + this->currentcurve->moveto(b1[0]); + for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) { + this->currentcurve->curveto(bp1[1], bp1[2], bp1[3]); + } + this->currentcurve->lineto(b2[BEZIER_SIZE*(nb2-1) + 3]); + for (Geom::Point *bp2 = b2 + BEZIER_SIZE * ( nb2 - 1 ); bp2 >= b2; bp2 -= BEZIER_SIZE) { + this->currentcurve->curveto(bp2[2], bp2[1], bp2[0]); + } + // FIXME: dc->segments is always NULL at this point?? + if (this->segments.empty()) { // first segment + add_cap(*currentcurve, b2[0], b1[0], cap_rounding); + } + this->currentcurve->closepath(); + currentshape->set_bpath(currentcurve.get(), true); + } + + /* Current calligraphic */ + for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) { + this->cal1->curveto(bp1[1], bp1[2], bp1[3]); + } + for (Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) { + this->cal2->curveto(bp2[1], bp2[2], bp2[3]); + } + } else { + /* fixme: ??? */ +#ifdef DYNA_DRAW_VERBOSE + g_print("[fit_and_split] failed to fit-cubic.\n"); +#endif + this->draw_temporary_box(); + + for (gint i = 1; i < this->npoints; i++) { + this->cal1->lineto(this->point1[i]); + } + for (gint i = 1; i < this->npoints; i++) { + this->cal2->lineto(this->point2[i]); + } + } + + /* Fit and draw and copy last point */ +#ifdef DYNA_DRAW_VERBOSE + g_print("[%d]Yup\n", this->npoints); +#endif + if (!release) { + g_assert(!this->currentcurve->is_empty()); + + guint32 fillColor = sp_desktop_get_color_tool(_desktop, "/tools/calligraphic", true); + double opacity = sp_desktop_get_master_opacity_tool(_desktop, "/tools/calligraphic"); + double fillOpacity = sp_desktop_get_opacity_tool(_desktop, "/tools/calligraphic", true); + guint fill = (fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*fillOpacity); + + auto cbp = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), currentcurve.get(), true); + cbp->set_fill(fill, SP_WIND_RULE_EVENODD); + cbp->set_stroke(0x0); + + /* fixme: Cannot we cascade it to root more clearly? */ + cbp->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), _desktop)); + + this->segments.push_back(cbp); + } + + this->point1[0] = this->point1[this->npoints - 1]; + this->point2[0] = this->point2[this->npoints - 1]; + this->npoints = 1; + } else { + this->draw_temporary_box(); + } +} + +void CalligraphicTool::draw_temporary_box() { + this->currentcurve->reset(); + + this->currentcurve->moveto(this->point2[this->npoints-1]); + + for (gint i = this->npoints-2; i >= 0; i--) { + this->currentcurve->lineto(this->point2[i]); + } + + for (gint i = 0; i < this->npoints; i++) { + this->currentcurve->lineto(this->point1[i]); + } + + if (this->npoints >= 2) { + add_cap(*currentcurve, point1[npoints - 1], point2[npoints - 1], cap_rounding); + } + + this->currentcurve->closepath(); + currentshape->set_bpath(currentcurve.get(), true); +} + +} +} +} + + +/* + 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/tools/calligraphic-tool.h b/src/ui/tools/calligraphic-tool.h new file mode 100644 index 0000000..8f43a4f --- /dev/null +++ b/src/ui/tools/calligraphic-tool.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_DYNA_DRAW_CONTEXT_H_SEEN +#define SP_DYNA_DRAW_CONTEXT_H_SEEN + +/* + * Handwriting-like drawing mode + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * The original dynadraw code: + * Paul Haeberli <paul@sgi.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <list> +#include <string> + +#include <2geom/point.h> + +#include "ui/tools/dynamic-base.h" + +class SPItem; +class Path; + +#define DDC_MIN_PRESSURE 0.0 +#define DDC_MAX_PRESSURE 1.0 +#define DDC_DEFAULT_PRESSURE 1.0 + +#define DDC_MIN_TILT -1.0 +#define DDC_MAX_TILT 1.0 +#define DDC_DEFAULT_TILT 0.0 + +namespace Inkscape { + +class CanvasItemBpath; + +namespace UI { +namespace Tools { + +class CalligraphicTool : public DynamicBase { +public: + CalligraphicTool(SPDesktop *desktop); + ~CalligraphicTool() override; + + void set(const Inkscape::Preferences::Entry &val) override; + bool root_handler(GdkEvent *event) override; + +private: + /** newly created object remain selected */ + bool keep_selected; + + double hatch_spacing; + double hatch_spacing_step; + SPItem *hatch_item; + Path *hatch_livarot_path; + std::list<double> hatch_nearest_past; + std::list<double> hatch_pointer_past; + std::list<Geom::Point> inertia_vectors; + Geom::Point hatch_last_nearest, hatch_last_pointer; + std::list<Geom::Point> hatch_vectors; + bool hatch_escaped; + Inkscape::CanvasItemBpath *hatch_area = nullptr; + bool just_started_drawing; + bool trace_bg; + + void clear_current(); + void set_to_accumulated(bool unionize, bool subtract); + bool accumulate(); + void fit_and_split(bool release); + void draw_temporary_box(); + void cancel(); + void brush(); + bool apply(Geom::Point p); + void extinput(GdkEvent *event); + void reset(Geom::Point p); +}; + +} +} +} + +#endif // SP_DYNA_DRAW_CONTEXT_H_SEEN + +/* + 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/tools/connector-tool.cpp b/src/ui/tools/connector-tool.cpp new file mode 100644 index 0000000..48302bd --- /dev/null +++ b/src/ui/tools/connector-tool.cpp @@ -0,0 +1,1383 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Connector creation tool + * + * Authors: + * Michael Wybrow <mjwybrow@users.sourceforge.net> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * Martin Owens <doctormo@gmail.com> + * + * Copyright (C) 2005-2008 Michael Wybrow + * Copyright (C) 2009 Monash University + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * TODO: + * o Show a visual indicator for objects with the 'avoid' property set. + * o Allow user to change a object between a path and connector through + * the interface. + * o Create an interface for setting markers (arrow heads). + * o Better distinguish between paths and connectors to prevent problems + * in the node tool and paths accidentally being turned into connectors + * in the connector tool. Perhaps have a way to convert between. + * o Only call libavoid's updateEndPoint as required. Currently we do it + * for both endpoints, even if only one is moving. + * o Deal sanely with connectors with both endpoints attached to the + * same connection point, and drawing of connectors attaching + * overlapping shapes (currently tries to adjust connector to be + * outside both bounding boxes). + * o Fix many special cases related to connectors updating, + * e.g., copying a couple of shapes and a connector that are + * attached to each other. + * e.g., detach connector when it is moved or transformed in + * one of the other contexts. + * o Cope with shapes whose ids change when they have attached + * connectors. + * o During dragging motion, gobble up to and use the final motion event. + * Gobbling away all duplicates after the current can occasionally result + * in the path lagging behind the mouse cursor if it is no longer being + * dragged. + * o Fix up libavoid's representation after undo actions. It doesn't see + * any transform signals and hence doesn't know shapes have moved back to + * there earlier positions. + * + * ---------------------------------------------------------------------------- + * + * Notes: + * + * Much of the way connectors work for user-defined points has been + * changed so that it no longer defines special attributes to record + * the points. Instead it uses single node paths to define points + * who are then separate objects that can be fixed on the canvas, + * grouped into objects and take full advantage of all transform, snap + * and align functionality of all other objects. + * + * I think that the style change between polyline and orthogonal + * would be much clearer with two buttons (radio behaviour -- just + * one is true). + * + * The other tools show a label change from "New:" to "Change:" + * depending on whether an object is selected. We could consider + * this but there may not be space. + * + * Likewise for the avoid/ignore shapes buttons. These should be + * inactive when a shape is not selected in the connector context. + * + */ + +#include "connector-tool.h" + +#include <string> +#include <cstring> + +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> +#include <gdk/gdkkeysyms.h> + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-context.h" +#include "message-stack.h" +#include "selection.h" +#include "snap.h" + +#include "display/control/canvas-item-bpath.h" +#include "display/control/canvas-item-ctrl.h" +#include "display/curve.h" + +#include "3rdparty/adaptagrams/libavoid/router.h" + +#include "object/sp-conn-end.h" +#include "object/sp-flowtext.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "object/sp-text.h" +#include "object/sp-use.h" +#include "object/sp-symbol.h" + +#include "ui/icon-names.h" +#include "ui/knot/knot.h" +#include "ui/widget/canvas.h" // Enter events + +#include "svg/svg.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void cc_clear_active_knots(SPKnotList k); + +static void shape_event_attr_deleted(Inkscape::XML::Node *repr, + Inkscape::XML::Node *child, Inkscape::XML::Node *ref, gpointer data); + +static void shape_event_attr_changed(Inkscape::XML::Node *repr, gchar const *name, + gchar const *old_value, gchar const *new_value, bool is_interactive, + gpointer data); + +static void cc_select_handle(SPKnot* knot); +static void cc_deselect_handle(SPKnot* knot); +static bool cc_item_is_shape(SPItem *item); + +/*static Geom::Point connector_drag_origin_w(0, 0); +static bool connector_within_tolerance = false;*/ + +static Inkscape::XML::NodeEventVector shape_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_added */ + shape_event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +static Inkscape::XML::NodeEventVector layer_repr_events = { + nullptr, /* child_added */ + shape_event_attr_deleted, + nullptr, /* child_added */ + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +ConnectorTool::ConnectorTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/connector", "connector.svg") + , selection(nullptr) + , npoints(0) + , state(SP_CONNECTOR_CONTEXT_IDLE) + , red_bpath(nullptr) + , red_curve(nullptr) + , red_color(0xff00007f) + , green_curve(nullptr) + , newconn(nullptr) + , newConnRef(nullptr) + , curvature(0.0) + , isOrthogonal(false) + , active_shape(nullptr) + , active_shape_repr(nullptr) + , active_shape_layer_repr(nullptr) + , active_conn(nullptr) + , active_conn_repr(nullptr) + , active_handle(nullptr) + , selected_handle(nullptr) + , clickeditem(nullptr) + , clickedhandle(nullptr) + , shref(nullptr) + , sub_shref(nullptr) + , ehref(nullptr) + , sub_ehref(nullptr) +{ + this->selection = desktop->getSelection(); + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = this->selection->connectChanged( + sigc::mem_fun(this, &ConnectorTool::_selectionChanged) + ); + + /* Create red bpath */ + red_bpath = new Inkscape::CanvasItemBpath(desktop->getCanvasSketch()); + red_bpath->set_stroke(red_color); + red_bpath->set_fill(0x0, SP_WIND_RULE_NONZERO); + + /* Create red curve */ + this->red_curve = std::make_unique<SPCurve>(); + + /* Create green curve */ + green_curve = std::make_unique<SPCurve>(); + + // Notice the initial selection. + //cc_selection_changed(this->selection, (gpointer) this); + this->_selectionChanged(this->selection); + + this->within_tolerance = false; + + sp_event_context_read(this, "curvature"); + sp_event_context_read(this, "orthogonal"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/connector/selcue", false)) { + this->enableSelectionCue(); + } + + // Make sure we see all enter events for canvas items, + // even if a mouse button is depressed. + desktop->getCanvas()->set_all_enter_events(true); +} + +ConnectorTool::~ConnectorTool() +{ + this->_finish(); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + + if (this->selection) { + this->selection = nullptr; + } + + this->cc_clear_active_shape(); + this->cc_clear_active_conn(); + + // Restore the default event generating behaviour. + _desktop->getCanvas()->set_all_enter_events(false); + + this->sel_changed_connection.disconnect(); + + for (auto &i : this->endpt_handle) { + if (i) { + knot_unref(i); + i = nullptr; + } + } + + if (this->shref) { + g_free(this->shref); + this->shref = nullptr; + } + + if (this->ehref) { + g_free(this->shref); + this->shref = nullptr; + } + + g_assert(this->newConnRef == nullptr); +} + +void ConnectorTool::set(const Inkscape::Preferences::Entry &val) +{ + /* fixme: Proper error handling for non-numeric data. Use a locale-independent function like + * g_ascii_strtod (or a thin wrapper that does the right thing for invalid values inf/nan). */ + Glib::ustring name = val.getEntryName(); + + if (name == "curvature") { + this->curvature = val.getDoubleLimited(); // prevents NaN and +/-Inf from messing up + } else if (name == "orthogonal") { + this->isOrthogonal = val.getBool(); + } +} + +//----------------------------------------------------------------------------- + + +void ConnectorTool::cc_clear_active_shape() +{ + if (this->active_shape == nullptr) { + return; + } + g_assert( this->active_shape_repr ); + g_assert( this->active_shape_layer_repr ); + + this->active_shape = nullptr; + + if (this->active_shape_repr) { + sp_repr_remove_listener_by_data(this->active_shape_repr, this); + Inkscape::GC::release(this->active_shape_repr); + this->active_shape_repr = nullptr; + + sp_repr_remove_listener_by_data(this->active_shape_layer_repr, this); + Inkscape::GC::release(this->active_shape_layer_repr); + this->active_shape_layer_repr = nullptr; + } + + cc_clear_active_knots(this->knots); +} + +static void cc_clear_active_knots(SPKnotList k) +{ + // Hide the connection points if they exist. + if (k.size()) { + for (auto & it : k) { + it.first->hide(); + } + } +} + +void ConnectorTool::cc_clear_active_conn() +{ + if (this->active_conn == nullptr) { + return; + } + g_assert( this->active_conn_repr ); + + this->active_conn = nullptr; + + if (this->active_conn_repr) { + sp_repr_remove_listener_by_data(this->active_conn_repr, this); + Inkscape::GC::release(this->active_conn_repr); + this->active_conn_repr = nullptr; + } + + // Hide the endpoint handles. + for (auto & i : this->endpt_handle) { + if (i) { + i->hide(); + } + } +} + + +bool ConnectorTool::_ptHandleTest(Geom::Point& p, gchar **href, gchar **subhref) +{ + if (this->active_handle && (this->knots.find(this->active_handle) != this->knots.end())) { + p = this->active_handle->pos; + *href = g_strdup_printf("#%s", this->active_handle->owner->getId()); + if(this->active_handle->sub_owner) { + auto id = this->active_handle->sub_owner->getAttribute("id"); + if(id) { + *subhref = g_strdup_printf("#%s", id); + } + } else { + *subhref = nullptr; + } + return true; + } + *href = nullptr; + *subhref = nullptr; + return false; +} + +static void cc_select_handle(SPKnot* knot) +{ + knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE); + knot->setSize(11); // Should be odd. + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0x0000ffff, 0x0000ffff, 0x0000ffff, 0x0000ffff); + knot->updateCtrl(); +} + +static void cc_deselect_handle(SPKnot* knot) +{ + knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE); + knot->setSize(9); // Should be odd. + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff); + knot->updateCtrl(); +} + +bool ConnectorTool::item_handler(SPItem* item, GdkEvent* event) +{ + bool ret = false; + + Geom::Point p(event->button.x, event->button.y); + + switch (event->type) { + case GDK_BUTTON_RELEASE: + if (event->button.button == 1) { + if ((this->state == SP_CONNECTOR_CONTEXT_DRAGGING) && this->within_tolerance) { + this->_resetColors(); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + } + + if (this->state != SP_CONNECTOR_CONTEXT_IDLE) { + // Doing something else like rerouting. + break; + } + + // find out clicked item, honoring Alt + SPItem *item = sp_event_context_find_item(_desktop, p, event->button.state & GDK_MOD1_MASK, FALSE); + + if (event->button.state & GDK_SHIFT_MASK) { + this->selection->toggle(item); + } else { + this->selection->set(item); + /* When selecting a new item, do not allow showing + connection points on connectors. (yet?) + */ + + if (item != this->active_shape && !cc_item_is_connector(item)) { + this->_setActiveShape(item); + } + } + + ret = true; + } + break; + + case GDK_MOTION_NOTIFY: { + auto last_pos = Geom::Point(event->motion.x, event->motion.y); + SPItem *item = _desktop->getItemAtPoint(last_pos, false); + if (cc_item_is_shape(item)) { + this->_setActiveShape(item); + } + ret = false; + break; + } + default: + break; + } + + return ret; +} + +bool ConnectorTool::root_handler(GdkEvent* event) +{ + bool ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = this->_handleButtonPress(event->button); + break; + + case GDK_MOTION_NOTIFY: + ret = this->_handleMotionNotify(event->motion); + break; + + case GDK_BUTTON_RELEASE: + ret = this->_handleButtonRelease(event->button); + break; + + case GDK_KEY_PRESS: + ret = this->_handleKeyPress(get_latin_keyval (&event->key)); + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + + +bool ConnectorTool::_handleButtonPress(GdkEventButton const &bevent) +{ + Geom::Point const event_w(bevent.x, bevent.y); + /* Find desktop coordinates */ + Geom::Point p = _desktop->w2d(event_w); + + bool ret = false; + + if ( bevent.button == 1 ) { + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return true; + } + + Geom::Point const event_w(bevent.x, bevent.y); + + this->xp = bevent.x; + this->yp = bevent.y; + this->within_tolerance = true; + + Geom::Point const event_dt = _desktop->w2d(event_w); + + SnapManager &m = _desktop->namedview->snap_manager; + + switch (this->state) { + case SP_CONNECTOR_CONTEXT_STOP: + + /* This is allowed, if we just canceled curve */ + case SP_CONNECTOR_CONTEXT_IDLE: + { + if ( this->npoints == 0 ) { + this->cc_clear_active_conn(); + + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new connector")); + + /* Set start anchor */ + /* Create green anchor */ + Geom::Point p = event_dt; + + // Test whether we clicked on a connection point + bool found = this->_ptHandleTest(p, &this->shref, &this->sub_shref); + + if (!found) { + // This is the first point, so just snap it to the grid + // as there's no other points to go off. + m.setup(_desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + } + this->_setInitialPoint(p); + + } + this->state = SP_CONNECTOR_CONTEXT_DRAGGING; + ret = true; + break; + } + case SP_CONNECTOR_CONTEXT_DRAGGING: + { + // This is the second click of a connector creation. + m.setup(_desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + + this->_setSubsequentPoint(p); + this->_finishSegment(p); + + this->_ptHandleTest(p, &this->ehref, &this->sub_ehref); + if (this->npoints != 0) { + this->_finish(); + } + this->cc_set_active_conn(this->newconn); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + ret = true; + break; + } + case SP_CONNECTOR_CONTEXT_CLOSE: + { + g_warning("Button down in CLOSE state"); + break; + } + default: + break; + } + } else if (bevent.button == 3) { + if (this->state == SP_CONNECTOR_CONTEXT_REROUTING) { + // A context menu is going to be triggered here, + // so end the rerouting operation. + this->_reroutingFinish(&p); + + this->state = SP_CONNECTOR_CONTEXT_IDLE; + + // Don't set ret to TRUE, so we drop through to the + // parent handler which will open the context menu. + } else if (this->npoints != 0) { + this->_finish(); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + ret = true; + } + } + return ret; +} + +bool ConnectorTool::_handleMotionNotify(GdkEventMotion const &mevent) +{ + bool ret = false; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (mevent.state & GDK_BUTTON2_MASK || mevent.state & GDK_BUTTON3_MASK) { + // allow middle-button scrolling + return false; + } + + Geom::Point const event_w(mevent.x, mevent.y); + + if (this->within_tolerance) { + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + if ( ( abs( (gint) mevent.x - this->xp ) < this->tolerance ) && + ( abs( (gint) mevent.y - this->yp ) < this->tolerance ) ) { + return false; // 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) + this->within_tolerance = false; + + /* Find desktop coordinates */ + Geom::Point p = _desktop->w2d(event_w); + + SnapManager &m = _desktop->namedview->snap_manager; + + switch (this->state) { + case SP_CONNECTOR_CONTEXT_DRAGGING: + { + gobble_motion_events(mevent.state); + // This is movement during a connector creation. + if ( this->npoints > 0 ) { + m.setup(_desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + this->selection->clear(); + this->_setSubsequentPoint(p); + ret = true; + } + break; + } + case SP_CONNECTOR_CONTEXT_REROUTING: + { + gobble_motion_events(GDK_BUTTON1_MASK); + g_assert( SP_IS_PATH(this->clickeditem)); + + m.setup(_desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + + // Update the hidden path + Geom::Affine i2d ( (this->clickeditem)->i2dt_affine() ); + Geom::Affine d2i = i2d.inverse(); + SPPath *path = SP_PATH(this->clickeditem); + SPCurve *curve = path->curve(); + if (this->clickedhandle == this->endpt_handle[0]) { + Geom::Point o = this->endpt_handle[1]->pos; + curve->stretch_endpoints(p * d2i, o * d2i); + } else { + Geom::Point o = this->endpt_handle[0]->pos; + curve->stretch_endpoints(o * d2i, p * d2i); + } + sp_conn_reroute_path_immediate(path); + + // Copy this to the temporary visible path + this->red_curve = SPCurve::copy(path->curveForEdit()); + this->red_curve->transform(i2d); + + red_bpath->set_bpath(red_curve.get()); + + ret = true; + break; + } + case SP_CONNECTOR_CONTEXT_STOP: + /* This is perfectly valid */ + break; + default: + if (!this->sp_event_context_knot_mouseover()) { + m.setup(_desktop); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + break; + } + return ret; +} + +bool ConnectorTool::_handleButtonRelease(GdkEventButton const &revent) +{ + bool ret = false; + + if ( revent.button == 1 ) { + SPDocument *doc = _desktop->getDocument(); + SnapManager &m = _desktop->namedview->snap_manager; + + Geom::Point const event_w(revent.x, revent.y); + + /* Find desktop coordinates */ + Geom::Point p = _desktop->w2d(event_w); + + switch (this->state) { + //case SP_CONNECTOR_CONTEXT_POINT: + case SP_CONNECTOR_CONTEXT_DRAGGING: + { + m.setup(_desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + + if (this->within_tolerance) { + this->_finishSegment(p); + return true; + } + // Connector has been created via a drag, end it now. + this->_setSubsequentPoint(p); + this->_finishSegment(p); + // Test whether we clicked on a connection point + this->_ptHandleTest(p, &this->ehref, &this->sub_ehref); + if (this->npoints != 0) { + this->_finish(); + } + this->cc_set_active_conn(this->newconn); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + break; + } + case SP_CONNECTOR_CONTEXT_REROUTING: + { + m.setup(_desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + this->_reroutingFinish(&p); + + doc->ensureUpToDate(); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + return true; + break; + } + case SP_CONNECTOR_CONTEXT_STOP: + /* This is allowed, if we just cancelled curve */ + break; + default: + break; + } + ret = true; + } + return ret; +} + +bool ConnectorTool::_handleKeyPress(guint const keyval) +{ + bool ret = false; + + switch (keyval) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (this->npoints != 0) { + this->_finish(); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + ret = true; + } + break; + case GDK_KEY_Escape: + if (this->state == SP_CONNECTOR_CONTEXT_REROUTING) { + SPDocument *doc = _desktop->getDocument(); + + this->_reroutingFinish(nullptr); + + DocumentUndo::undo(doc); + + this->state = SP_CONNECTOR_CONTEXT_IDLE; + _desktop->messageStack()->flash( Inkscape::NORMAL_MESSAGE, + _("Connector endpoint drag cancelled.")); + ret = true; + } else if (this->npoints != 0) { + // if drawing, cancel, otherwise pass it up for deselecting + this->state = SP_CONNECTOR_CONTEXT_STOP; + this->_resetColors(); + ret = true; + } + break; + default: + break; + } + return ret; +} + +void ConnectorTool::_reroutingFinish(Geom::Point *const p) +{ + SPDocument *doc = _desktop->getDocument(); + + // Clear the temporary path: + this->red_curve->reset(); + red_bpath->set_bpath(nullptr); + + if (p != nullptr) { + // Test whether we clicked on a connection point + gchar *shape_label; + gchar *sub_label; + bool found = this->_ptHandleTest(*p, &shape_label, &sub_label); + + if (found) { + if (this->clickedhandle == this->endpt_handle[0]) { + this->clickeditem->setAttribute("inkscape:connection-start", shape_label); + this->clickeditem->setAttribute("inkscape:connection-start-point", sub_label); + } else { + this->clickeditem->setAttribute("inkscape:connection-end", shape_label); + this->clickeditem->setAttribute("inkscape:connection-end-point", sub_label); + } + g_free(shape_label); + if(sub_label) { + g_free(sub_label); + } + } + } + this->clickeditem->setHidden(false); + sp_conn_reroute_path_immediate(SP_PATH(this->clickeditem)); + this->clickeditem->updateRepr(); + DocumentUndo::done(doc, _("Reroute connector"), INKSCAPE_ICON("draw-connector")); + this->cc_set_active_conn(this->clickeditem); +} + + +void ConnectorTool::_resetColors() +{ + /* Red */ + this->red_curve->reset(); + red_bpath->set_bpath(nullptr); + + this->green_curve->reset(); + this->npoints = 0; +} + +void ConnectorTool::_setInitialPoint(Geom::Point const p) +{ + g_assert( this->npoints == 0 ); + + this->p[0] = p; + this->p[1] = p; + this->npoints = 2; + red_bpath->set_bpath(nullptr); +} + +void ConnectorTool::_setSubsequentPoint(Geom::Point const p) +{ + g_assert( this->npoints != 0 ); + + Geom::Point o = _desktop->dt2doc(this->p[0]); + Geom::Point d = _desktop->dt2doc(p); + Avoid::Point src(o[Geom::X], o[Geom::Y]); + Avoid::Point dst(d[Geom::X], d[Geom::Y]); + + if (!this->newConnRef) { + Avoid::Router *router = _desktop->getDocument()->getRouter(); + this->newConnRef = new Avoid::ConnRef(router); + this->newConnRef->setEndpoint(Avoid::VertID::src, src); + if (this->isOrthogonal) { + this->newConnRef->setRoutingType(Avoid::ConnType_Orthogonal); + } else { + this->newConnRef->setRoutingType(Avoid::ConnType_PolyLine); + } + } + // Set new endpoint. + this->newConnRef->setEndpoint(Avoid::VertID::tar, dst); + // Immediately generate new routes for connector. + this->newConnRef->makePathInvalid(); + this->newConnRef->router()->processTransaction(); + // Recreate curve from libavoid route. + recreateCurve(red_curve.get(), this->newConnRef, this->curvature); + this->red_curve->transform(_desktop->doc2dt()); + red_bpath->set_bpath(red_curve.get(), true); +} + + +/** + * Concats red, blue and green. + * If any anchors are defined, process these, optionally removing curves from white list + * Invoke _flush_white to write result back to object. + */ +void ConnectorTool::_concatColorsAndFlush() +{ + auto c = std::make_unique<SPCurve>(); + std::swap(c, green_curve); + + this->red_curve->reset(); + red_bpath->set_bpath(nullptr); + + if (c->is_empty()) { + return; + } + + this->_flushWhite(c.get()); +} + + +/* + * Flushes white curve(s) and additional curve into object + * + * No cleaning of colored curves - this has to be done by caller + * No rereading of white data, so if you cannot rely on ::modified, do it in caller + * + */ + +void ConnectorTool::_flushWhite(SPCurve *c) +{ + /* Now we have to go back to item coordinates at last */ + c->transform(_desktop->dt2doc()); + + SPDocument *doc = _desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + if ( c && !c->is_empty() ) { + /* We actually have something to write */ + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + /* Set style */ + sp_desktop_apply_style_tool(_desktop, repr, "/tools/connector", false); + + repr->setAttribute("d", sp_svg_write_path(c->get_pathvector())); + + /* Attach repr */ + auto layer = currentLayer(); + this->newconn = SP_ITEM(layer->appendChildRepr(repr)); + this->newconn->transform = layer->i2doc_affine().inverse(); + + bool connection = false; + this->newconn->setAttribute( "inkscape:connector-type", + this->isOrthogonal ? "orthogonal" : "polyline"); + this->newconn->setAttribute( "inkscape:connector-curvature", + Glib::Ascii::dtostr(this->curvature).c_str()); + if (this->shref) { + connection = true; + this->newconn->setAttribute( "inkscape:connection-start", this->shref); + if(this->sub_shref) { + this->newconn->setAttribute( "inkscape:connection-start-point", this->sub_shref); + } + } + + if (this->ehref) { + connection = true; + this->newconn->setAttribute( "inkscape:connection-end", this->ehref); + if(this->sub_ehref) { + this->newconn->setAttribute( "inkscape:connection-end-point", this->sub_ehref); + } + } + // Process pending updates. + this->newconn->updateRepr(); + doc->ensureUpToDate(); + + if (connection) { + // Adjust endpoints to shape edge. + sp_conn_reroute_path_immediate(SP_PATH(this->newconn)); + this->newconn->updateRepr(); + } + + this->newconn->doWriteTransform(this->newconn->transform, nullptr, true); + + // Only set the selection after we are finished with creating the attributes of + // the connector. Otherwise, the selection change may alter the defaults for + // values like curvature in the connector context, preventing subsequent lookup + // of their original values. + this->selection->set(repr); + Inkscape::GC::release(repr); + } + + DocumentUndo::done(doc, _("Create connector"), INKSCAPE_ICON("draw-connector")); +} + + +void ConnectorTool::_finishSegment(Geom::Point const /*p*/) +{ + if (!this->red_curve->is_empty()) { + green_curve->append_continuous(*red_curve); + + this->p[0] = this->p[3]; + this->p[1] = this->p[4]; + this->npoints = 2; + + this->red_curve->reset(); + } +} + +void ConnectorTool::_finish() +{ + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing connector")); + + this->red_curve->reset(); + this->_concatColorsAndFlush(); + + this->npoints = 0; + + if (this->newConnRef) { + this->newConnRef->router()->deleteConnector(this->newConnRef); + this->newConnRef = nullptr; + } +} + + +static bool cc_generic_knot_handler(GdkEvent *event, SPKnot *knot) +{ + g_assert (knot != nullptr); + + //g_object_ref(knot); + knot_ref(knot); + + ConnectorTool *cc = SP_CONNECTOR_CONTEXT( + knot->desktop->event_context); + + bool consumed = false; + + gchar const *knot_tip = _("Click to join at this point"); + switch (event->type) { + case GDK_ENTER_NOTIFY: + knot->setFlag(SP_KNOT_MOUSEOVER, TRUE); + + cc->active_handle = knot; + if (knot_tip) { + knot->desktop->event_context->defaultMessageContext()->set( + Inkscape::NORMAL_MESSAGE, knot_tip); + } + + consumed = true; + break; + case GDK_LEAVE_NOTIFY: + knot->setFlag(SP_KNOT_MOUSEOVER, FALSE); + + /* FIXME: the following test is a workaround for LP Bug #1273510. + * It seems that a signal is not correctly disconnected, maybe + * something missing in cc_clear_active_conn()? */ + if (cc) { + cc->active_handle = nullptr; + } + + if (knot_tip) { + knot->desktop->event_context->defaultMessageContext()->clear(); + } + + consumed = true; + break; + default: + break; + } + + knot_unref(knot); + + return consumed; +} + + +static bool endpt_handler(GdkEvent *event, ConnectorTool *cc) +{ + //g_assert( SP_IS_CONNECTOR_CONTEXT(cc) ); + + gboolean consumed = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + g_assert( (cc->active_handle == cc->endpt_handle[0]) || + (cc->active_handle == cc->endpt_handle[1]) ); + if (cc->state == SP_CONNECTOR_CONTEXT_IDLE) { + cc->clickeditem = cc->active_conn; + cc->clickedhandle = cc->active_handle; + cc->cc_clear_active_conn(); + cc->state = SP_CONNECTOR_CONTEXT_REROUTING; + + // Disconnect from attached shape + unsigned ind = (cc->active_handle == cc->endpt_handle[0]) ? 0 : 1; + sp_conn_end_detach(cc->clickeditem, ind); + + Geom::Point origin; + if (cc->clickedhandle == cc->endpt_handle[0]) { + origin = cc->endpt_handle[1]->pos; + } else { + origin = cc->endpt_handle[0]->pos; + } + + // Show the red path for dragging. + auto path = static_cast<SPPath const *>(cc->clickeditem); + cc->red_curve = SPCurve::copy(path->curveForEdit()); + Geom::Affine i2d = (cc->clickeditem)->i2dt_affine(); + cc->red_curve->transform(i2d); + cc->red_bpath->set_bpath(cc->red_curve.get(), true); + + cc->clickeditem->setHidden(true); + + // The rest of the interaction rerouting the connector is + // handled by the context root handler. + consumed = TRUE; + } + break; + default: + break; + } + + return consumed; +} + +void ConnectorTool::_activeShapeAddKnot(SPItem* item, SPItem* subitem) +{ + SPKnot *knot = new SPKnot(_desktop, "", Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "CanvasItemCtrl:ConnectorTool:Shape"); + knot->owner = item; + + if (subitem) { + SPUse *use = dynamic_cast<SPUse *>(item); + g_assert(use != nullptr); + knot->sub_owner = subitem; + knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE); + knot->setSize(11); // Must be odd + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff); + + // Set the point to the middle of the sub item + knot->setPosition(subitem->getAvoidRef().getConnectionPointPos() * _desktop->doc2dt(), 0); + } else { + knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE); + knot->setSize(9); // Must be odd + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff); + + // Set the point to the middle of the object + knot->setPosition(item->getAvoidRef().getConnectionPointPos() * _desktop->doc2dt(), 0); + } + + knot->updateCtrl(); + + // We don't want to use the standard knot handler. + knot->_event_connection.disconnect(); + knot->_event_connection = + knot->ctrl->connect_event(sigc::bind(sigc::ptr_fun(cc_generic_knot_handler), knot)); + + knot->show(); + this->knots[knot] = 1; +} + +void ConnectorTool::_setActiveShape(SPItem *item) +{ + g_assert(item != nullptr ); + + if (this->active_shape != item) { + // The active shape has changed + // Rebuild everything + this->active_shape = item; + // Remove existing active shape listeners + if (this->active_shape_repr) { + sp_repr_remove_listener_by_data(this->active_shape_repr, this); + Inkscape::GC::release(this->active_shape_repr); + + sp_repr_remove_listener_by_data(this->active_shape_layer_repr, this); + Inkscape::GC::release(this->active_shape_layer_repr); + } + + // Listen in case the active shape changes + this->active_shape_repr = item->getRepr(); + if (this->active_shape_repr) { + Inkscape::GC::anchor(this->active_shape_repr); + sp_repr_add_listener(this->active_shape_repr, &shape_repr_events, this); + + this->active_shape_layer_repr = this->active_shape_repr->parent(); + Inkscape::GC::anchor(this->active_shape_layer_repr); + sp_repr_add_listener(this->active_shape_layer_repr, &layer_repr_events, this); + } + + cc_clear_active_knots(this->knots); + + // The idea here is to try and add a group's children to solidify + // connection handling. We react to path objects with only one node. + for (auto& child: item->children) { + if(child.getAttribute("inkscape:connector")) { + this->_activeShapeAddKnot((SPItem *) &child, nullptr); + } + } + // Special connector points in a symbol + SPUse *use = dynamic_cast<SPUse *>(item); + if(use) { + SPItem *orig = use->root(); + //SPItem *orig = use->get_original(); + for (auto& child: orig->children) { + if(child.getAttribute("inkscape:connector")) { + this->_activeShapeAddKnot(item, (SPItem *) &child); + } + } + } + // Center point to any object + this->_activeShapeAddKnot(item, nullptr); + + } else { + // Ensure the item's connection_points map + // has been updated + item->document->ensureUpToDate(); + } +} + +void ConnectorTool::cc_set_active_conn(SPItem *item) +{ + g_assert( SP_IS_PATH(item) ); + + const SPCurve *curve = SP_PATH(item)->curveForEdit(); + Geom::Affine i2dt = item->i2dt_affine(); + + if (this->active_conn == item) { + if (curve->is_empty()) { + // Connector is invisible because it is clipped to the boundary of + // two overlapping shapes. + this->endpt_handle[0]->hide(); + this->endpt_handle[1]->hide(); + } else { + // Just adjust handle positions. + Geom::Point startpt = *(curve->first_point()) * i2dt; + this->endpt_handle[0]->setPosition(startpt, 0); + + Geom::Point endpt = *(curve->last_point()) * i2dt; + this->endpt_handle[1]->setPosition(endpt, 0); + } + + return; + } + + this->active_conn = item; + + // Remove existing active conn listeners + if (this->active_conn_repr) { + sp_repr_remove_listener_by_data(this->active_conn_repr, this); + Inkscape::GC::release(this->active_conn_repr); + this->active_conn_repr = nullptr; + } + + // Listen in case the active conn changes + this->active_conn_repr = item->getRepr(); + if (this->active_conn_repr) { + Inkscape::GC::anchor(this->active_conn_repr); + sp_repr_add_listener(this->active_conn_repr, &shape_repr_events, this); + } + + for (int i = 0; i < 2; ++i) { + // Create the handle if it doesn't exist + if ( this->endpt_handle[i] == nullptr ) { + SPKnot *knot = new SPKnot(_desktop, + _("<b>Connector endpoint</b>: drag to reroute or connect to new shapes"), + Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "CanvasItemCtrl:ConnectorTool:Endpoint"); + + knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE); + knot->setSize(7); + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff); + knot->setStroke(0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff); + knot->updateCtrl(); + + // We don't want to use the standard knot handler, + // since we don't want this knot to be draggable. + knot->_event_connection.disconnect(); + knot->_event_connection = + knot->ctrl->connect_event(sigc::bind(sigc::ptr_fun(cc_generic_knot_handler), knot)); + + this->endpt_handle[i] = knot; + } + + // Remove any existing handlers + this->endpt_handler_connection[i].disconnect(); + this->endpt_handler_connection[i] = + this->endpt_handle[i]->ctrl->connect_event(sigc::bind(sigc::ptr_fun(endpt_handler), this)); + } + + if (curve->is_empty()) { + // Connector is invisible because it is clipped to the boundary + // of two overlpapping shapes. So, it doesn't need endpoints. + return; + } + + Geom::Point startpt = *(curve->first_point()) * i2dt; + this->endpt_handle[0]->setPosition(startpt, 0); + + Geom::Point endpt = *(curve->last_point()) * i2dt; + this->endpt_handle[1]->setPosition(endpt, 0); + + this->endpt_handle[0]->show(); + this->endpt_handle[1]->show(); +} + +void cc_create_connection_point(ConnectorTool* cc) +{ + if (cc->active_shape && cc->state == SP_CONNECTOR_CONTEXT_IDLE) { + if (cc->selected_handle) { + cc_deselect_handle( cc->selected_handle ); + } + + SPKnot *knot = new SPKnot(cc->getDesktop(), "", Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, + "CanvasItemCtrl::ConnectorTool:ConnectionPoint"); + + // We do not process events on this knot. + knot->_event_connection.disconnect(); + + cc_select_handle( knot ); + cc->selected_handle = knot; + cc->selected_handle->show(); + cc->state = SP_CONNECTOR_CONTEXT_NEWCONNPOINT; + } +} + +static bool cc_item_is_shape(SPItem *item) +{ + if (auto path = dynamic_cast<SPPath const *>(item)) { + SPCurve const *curve = path->curve(); + if ( curve && !(curve->is_closed()) ) { + // Open paths are connectors. + return false; + } + } else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/connector/ignoretext", true)) { + // Don't count text as a shape we can connect connector to. + return false; + } + } + return true; +} + + +bool cc_item_is_connector(SPItem *item) +{ + if (SP_IS_PATH(item)) { + bool closed = SP_PATH(item)->curveForEdit()->is_closed(); + if (SP_PATH(item)->connEndPair.isAutoRoutingConn() && !closed) { + // To be considered a connector, an object must be a non-closed + // path that is marked with a "inkscape:connector-type" attribute. + return true; + } + } + return false; +} + + +void cc_selection_set_avoid(SPDesktop *desktop, bool const set_avoid) +{ + if (desktop == nullptr) { + return; + } + + SPDocument *document = desktop->getDocument(); + + Inkscape::Selection *selection = desktop->getSelection(); + + + int changes = 0; + + for (SPItem *item: selection->items()) { + char const *value = (set_avoid) ? "true" : nullptr; + + if (cc_item_is_shape(item)) { + item->setAttribute("inkscape:connector-avoid", value); + item->getAvoidRef().handleSettingChange(); + changes++; + } + } + + if (changes == 0) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, + _("Select <b>at least one non-connector object</b>.")); + return; + } + + char *event_desc = (set_avoid) ? + _("Make connectors avoid selected objects") : + _("Make connectors ignore selected objects"); + DocumentUndo::done(document, event_desc, INKSCAPE_ICON("draw-connector")); +} + +void ConnectorTool::_selectionChanged(Inkscape::Selection *selection) +{ + SPItem *item = selection->singleItem(); + if (this->active_conn == item) { + // Nothing to change. + return; + } + + if (item == nullptr) { + this->cc_clear_active_conn(); + return; + } + + if (cc_item_is_connector(item)) { + this->cc_set_active_conn(item); + } +} + +static void shape_event_attr_deleted(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node *child, + Inkscape::XML::Node */*ref*/, gpointer data) +{ + g_assert(data); + ConnectorTool *cc = SP_CONNECTOR_CONTEXT(data); + + if (child == cc->active_shape_repr) { + // The active shape has been deleted. Clear active shape. + cc->cc_clear_active_shape(); + } +} + + +static void shape_event_attr_changed(Inkscape::XML::Node *repr, gchar const *name, + gchar const */*old_value*/, gchar const */*new_value*/, bool /*is_interactive*/, gpointer data) +{ + g_assert(data); + ConnectorTool *cc = SP_CONNECTOR_CONTEXT(data); + + // Look for changes that result in onscreen movement. + if (!strcmp(name, "d") || !strcmp(name, "x") || !strcmp(name, "y") || + !strcmp(name, "width") || !strcmp(name, "height") || + !strcmp(name, "transform")) { + if (repr == cc->active_shape_repr) { + // Active shape has moved. Clear active shape. + cc->cc_clear_active_shape(); + } else if (repr == cc->active_conn_repr) { + // The active conn has been moved. + // Set it again, which just sets new handle positions. + cc->cc_set_active_conn(cc->active_conn); + } + } +} + +} +} +} + + +/* + 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/tools/connector-tool.h b/src/ui/tools/connector-tool.h new file mode 100644 index 0000000..8c35ccb --- /dev/null +++ b/src/ui/tools/connector-tool.h @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CONNECTOR_CONTEXT_H +#define SEEN_CONNECTOR_CONTEXT_H + +/* + * Connector creation tool + * + * Authors: + * Michael Wybrow <mjwybrow@users.sourceforge.net> + * + * Copyright (C) 2005 Michael Wybrow + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <map> +#include <memory> +#include <string> + +#include <2geom/point.h> +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" + +class SPItem; +class SPCurve; +class SPKnot; + +namespace Avoid { + class ConnRef; +} + +namespace Inkscape { + class CanvasItemBpath; + class Selection; + + namespace XML { + class Node; + } +} + +#define SP_CONNECTOR_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ConnectorTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +//#define SP_IS_CONNECTOR_CONTEXT(obj) (dynamic_cast<const ConnectorTool*>((const ToolBase*)obj) != NULL) + +enum { + SP_CONNECTOR_CONTEXT_IDLE, + SP_CONNECTOR_CONTEXT_DRAGGING, + SP_CONNECTOR_CONTEXT_CLOSE, + SP_CONNECTOR_CONTEXT_STOP, + SP_CONNECTOR_CONTEXT_REROUTING, + SP_CONNECTOR_CONTEXT_NEWCONNPOINT +}; + +typedef std::map<SPKnot *, int> SPKnotList; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ConnectorTool : public ToolBase { +public: + ConnectorTool(SPDesktop *desktop); + ~ConnectorTool() override; + + Inkscape::Selection *selection; + Geom::Point p[5]; + + /** \invar npoints in {0, 2}. */ + gint npoints; + unsigned int state : 4; + + // Red curve + Inkscape::CanvasItemBpath *red_bpath; + std::unique_ptr<SPCurve> red_curve; + guint32 red_color; + + // Green curve + std::unique_ptr<SPCurve> green_curve; + + // The new connector + SPItem *newconn; + Avoid::ConnRef *newConnRef; + gdouble curvature; + bool isOrthogonal; + + // The active shape + SPItem *active_shape; + Inkscape::XML::Node *active_shape_repr; + Inkscape::XML::Node *active_shape_layer_repr; + + // Same as above, but for the active connector + SPItem *active_conn; + Inkscape::XML::Node *active_conn_repr; + sigc::connection sel_changed_connection; + + // The activehandle + SPKnot *active_handle; + + // The selected handle, used in editing mode + SPKnot *selected_handle; + + SPItem *clickeditem; + SPKnot *clickedhandle; + + SPKnotList knots; + SPKnot *endpt_handle[2]{}; + sigc::connection endpt_handler_connection[2]; + gchar *shref; + gchar *sub_shref; + gchar *ehref; + gchar *sub_ehref; + + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + + void cc_clear_active_shape(); + void cc_set_active_conn(SPItem *item); + void cc_clear_active_conn(); + +private: + void _selectionChanged(Inkscape::Selection *selection); + + bool _handleButtonPress(GdkEventButton const &bevent); + bool _handleMotionNotify(GdkEventMotion const &mevent); + bool _handleButtonRelease(GdkEventButton const &revent); + bool _handleKeyPress(guint const keyval); + + void _setInitialPoint(Geom::Point const p); + void _setSubsequentPoint(Geom::Point const p); + void _finishSegment(Geom::Point p); + void _resetColors(); + void _finish(); + void _concatColorsAndFlush(); + void _flushWhite(SPCurve *gc); + + void _activeShapeAddKnot(SPItem* item, SPItem* subitem); + void _setActiveShape(SPItem *item); + bool _ptHandleTest(Geom::Point& p, gchar **href, gchar **subhref); + + void _reroutingFinish(Geom::Point *const p); +}; + +void cc_selection_set_avoid(SPDesktop *, bool const set_ignore); +void cc_create_connection_point(ConnectorTool* cc); +void cc_remove_connection_point(ConnectorTool* cc); +bool cc_item_is_connector(SPItem *item); + +} +} +} + +#endif /* !SEEN_CONNECTOR_CONTEXT_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/tools/dropper-tool.cpp b/src/ui/tools/dropper-tool.cpp new file mode 100644 index 0000000..e7ff4fb --- /dev/null +++ b/src/ui/tools/dropper-tool.cpp @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Tool for picking colors from drawing + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Abhishek Sharma + * + * Copyright (C) 1999-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> + +#include <2geom/transforms.h> +#include <2geom/circle.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "message-context.h" +#include "preferences.h" +#include "selection.h" +#include "style.h" +#include "page-manager.h" + +#include "display/curve.h" +#include "display/drawing.h" +#include "display/control/canvas-item-bpath.h" +#include "display/control/canvas-item-drawing.h" + +#include "include/macros.h" + +#include "object/sp-namedview.h" + +#include "svg/svg-color.h" + +#include "ui/cursor-utils.h" +#include "ui/icon-names.h" +#include "ui/tools/dropper-tool.h" +#include "ui/widget/canvas.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +DropperTool::DropperTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/dropper", "dropper-pick-fill.svg") +{ + area = new Inkscape::CanvasItemBpath(desktop->getCanvasControls()); + area->set_stroke(0x0000007f); + area->set_fill(0x0, SP_WIND_RULE_EVENODD); + area->hide(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/dropper/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/dropper/gradientdrag")) { + this->enableGrDrag(); + } +} + +DropperTool::~DropperTool() +{ + this->enableGrDrag(false); + + ungrabCanvasEvents(); + + if (this->area) { + delete this->area; + this->area = nullptr; + } +} + +/** + * Returns the current dropper context color. + * + * - If in dropping mode, returns color from selected objects. + * Ignored if non_dropping set to true. + * - If in dragging mode, returns average color on canvas, depending on radius + * - If in pick mode, alpha is not premultiplied. Alpha is only set if in pick mode + * and setalpha is true. Both values are taken from preferences. + * + * @param invert If true, invert the rgb value + * @param non_dropping If true, use color from canvas, even in dropping mode. + */ +guint32 DropperTool::get_color(bool invert, bool non_dropping) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + int pick = prefs->getInt("/tools/dropper/pick", SP_DROPPER_PICK_VISIBLE); + bool setalpha = prefs->getBool("/tools/dropper/setalpha", true); + + // non_dropping ignores dropping mode and always uses color from canvas. + // Used by the clipboard + double r = non_dropping ? this->non_dropping_R : this->R; + double g = non_dropping ? this->non_dropping_G : this->G; + double b = non_dropping ? this->non_dropping_B : this->B; + double a = non_dropping ? this->non_dropping_A : this->alpha; + + return SP_RGBA32_F_COMPOSE( + fabs(invert - r), + fabs(invert - g), + fabs(invert - b), + (pick == SP_DROPPER_PICK_ACTUAL && setalpha) ? a : 1.0); +} + +bool DropperTool::root_handler(GdkEvent* event) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + int ret = FALSE; + int pick = prefs->getInt("/tools/dropper/pick", SP_DROPPER_PICK_VISIBLE); + + // Decide first what kind of 'mode' we're in. + if (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) { + switch (event->key.keyval) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->stroke = event->type == GDK_KEY_PRESS; + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + this->dropping = event->type == GDK_KEY_PRESS; + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + this->invert = event->type == GDK_KEY_PRESS; + break; + } + } + + // Get color from selected object + // Only if dropping mode enabled and object's color is set. + // Otherwise dropping mode disabled. + if(this->dropping) { + Inkscape::Selection *selection = _desktop->getSelection(); + g_assert(selection); + guint32 apply_color; + bool apply_set = false; + for (auto& obj: selection->objects()) { + if(obj->style) { + double opacity = 1.0; + if(!this->stroke && obj->style->fill.set) { + if(obj->style->fill_opacity.set) { + opacity = SP_SCALE24_TO_FLOAT(obj->style->fill_opacity.value); + } + apply_color = obj->style->fill.value.color.toRGBA32(opacity); + apply_set = true; + } else if(this->stroke && obj->style->stroke.set) { + if(obj->style->stroke_opacity.set) { + opacity = SP_SCALE24_TO_FLOAT(obj->style->stroke_opacity.value); + } + apply_color = obj->style->stroke.value.color.toRGBA32(opacity); + apply_set = true; + } + } + } + if(apply_set) { + this->R = SP_RGBA32_R_F(apply_color); + this->G = SP_RGBA32_G_F(apply_color); + this->B = SP_RGBA32_B_F(apply_color); + this->alpha = SP_RGBA32_A_F(apply_color); + } else { + // This means that having no selection or some other error + // we will default back to normal dropper mode. + this->dropping = false; + } + } + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + this->centre = Geom::Point(event->button.x, event->button.y); + this->dragging = true; + ret = TRUE; + } + + grabCanvasEvents(Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | + Gdk::BUTTON_PRESS_MASK ); + break; + + case GDK_MOTION_NOTIFY: + if (event->motion.state & GDK_BUTTON2_MASK || event->motion.state & GDK_BUTTON3_MASK) { + // pass on middle and right drag + ret = FALSE; + break; + } else { + // otherwise, constantly calculate color no matter if any button pressed or not + + Geom::IntRect pick_area; + if (this->dragging) { + // calculate average + + // radius + double rw = std::min(Geom::L2(Geom::Point(event->button.x, event->button.y) - this->centre), 400.0); + if (rw == 0) { // happens sometimes, little idea why... + break; + } + this->radius = rw; + + Geom::Point const cd = _desktop->w2d(this->centre); + Geom::Affine const w2dt = _desktop->w2d(); + const double scale = rw * w2dt.descrim(); + Geom::Affine const sm( Geom::Scale(scale, scale) * Geom::Translate(cd) ); + + // Show circle on canvas + Geom::PathVector path = Geom::Path(Geom::Circle(0, 0, 1)); // Unit circle centered at origin. + path *= sm; + this->area->set_bpath(path); + this->area->show(); + + /* Get buffer */ + Geom::Rect r(this->centre, this->centre); + r.expandBy(rw); + if (!r.hasZeroArea()) { + pick_area = r.roundOutwards(); + + } + } else { + // pick single pixel + pick_area = Geom::IntRect::from_xywh(floor(event->button.x), floor(event->button.y), 1, 1); + } + + Inkscape::CanvasItemDrawing *canvas_item_drawing = _desktop->getCanvasDrawing(); + Inkscape::Drawing *drawing = canvas_item_drawing->get_drawing(); + + // Ensure drawing up-to-date. (Is this really necessary?) + drawing->update(); + + // Get average color. + double R, G, B, A; + drawing->average_color(pick_area, R, G, B, A); + + if (pick == SP_DROPPER_PICK_VISIBLE) { + // compose with page color + auto bg = _desktop->getDocument()->getPageManager().getDefaultBackgroundColor(); + R = R + bg[0] * (1 - A); + G = G + bg[1] * (1 - A); + B = B + bg[2] * (1 - A); + A = 1.0; + } else { + // un-premultiply color channels + if (A > 0) { + R /= A; + G /= A; + B /= A; + } + } + + if (fabs(A) < 1e-4) { + A = 0; // suppress exponentials, CSS does not allow that + } + + // remember color + if (!this->dropping) { + this->R = R; + this->G = G; + this->B = B; + this->alpha = A; + } + // remember color from canvas, even in dropping mode + // These values are used by the clipboard + this->non_dropping_R = R; + this->non_dropping_G = G; + this->non_dropping_B = B; + this->non_dropping_A = A; + + ret = TRUE; + } + break; + + case GDK_BUTTON_RELEASE: + if (event->button.button == 1) { + this->area->hide(); + this->dragging = false; + + ungrabCanvasEvents(); + + Inkscape::Selection *selection = _desktop->getSelection(); + g_assert(selection); + std::vector<SPItem *> old_selection(selection->items().begin(), selection->items().end()); + if(this->dropping) { + Geom::Point const button_w(event->button.x, event->button.y); + // remember clicked item, disregarding groups, honoring Alt + this->item_to_select = sp_event_context_find_item (_desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + + // Change selected object to object under cursor + if (this->item_to_select) { + std::vector<SPItem *> vec(selection->items().begin(), selection->items().end()); + selection->set(this->item_to_select); + } + } + + auto picked_color = ColorRGBA(this->get_color(this->invert)); + + // One time pick has active signal, call them all and clear. + if (!onetimepick_signal.empty()) + { + onetimepick_signal.emit(&picked_color); + onetimepick_signal.clear(); + // Do this last as it destroys the picker tool. + sp_toggle_dropper(_desktop); + return true; + } + + // do the actual color setting + sp_desktop_set_color(_desktop, picked_color, false, !this->stroke); + + // REJON: set aux. toolbar input to hex color! + if (!(_desktop->getSelection()->isEmpty())) { + DocumentUndo::done(_desktop->getDocument(), _("Set picked color"), INKSCAPE_ICON("color-picker")); + } + if(this->dropping) { + selection->setList(old_selection); + } + + + ret = TRUE; + } + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // prevent the zoom field from activation + if (!MOD__CTRL_ONLY(event)) { + ret = TRUE; + } + break; + case GDK_KEY_Escape: + _desktop->getSelection()->clear(); + break; + } + break; + } + + // set the status message to the right text. + gchar c[64]; + sp_svg_write_color(c, sizeof(c), this->get_color(this->invert)); + + // alpha of color under cursor, to show in the statusbar + // locale-sensitive printf is OK, since this goes to the UI, not into SVG + gchar *alpha = g_strdup_printf(_(" alpha %.3g"), this->alpha); + // where the color is picked, to show in the statusbar + gchar *where = this->dragging ? g_strdup_printf(_(", averaged with radius %d"), (int) this->radius) : g_strdup_printf("%s", _(" under cursor")); + // message, to show in the statusbar + const gchar *message = this->dragging ? _("<b>Release mouse</b> to set color.") : _("<b>Click</b> to set fill, <b>Shift+click</b> to set stroke; <b>drag</b> to average color in area; with <b>Alt</b> to pick inverse color; <b>Ctrl+C</b> to copy the color under mouse to clipboard"); + + this->defaultMessageContext()->setF( + Inkscape::NORMAL_MESSAGE, + "<b>%s%s</b>%s. %s", c, + (pick == SP_DROPPER_PICK_VISIBLE) ? "" : alpha, where, message); + + g_free(where); + g_free(alpha); + + // Set the right cursor for the mode and apply the special Fill color + _cursor_filename = (this->dropping ? (this->stroke ? "dropper-drop-stroke.svg" : "dropper-drop-fill.svg") : + (this->stroke ? "dropper-pick-stroke.svg" : "dropper-pick-fill.svg") ); + + // We do this ourselves to get color correct. + auto display = _desktop->getCanvas()->get_display(); + auto window = _desktop->getCanvas()->get_window(); + auto cursor = load_svg_cursor(display, window, _cursor_filename, get_color(invert)); + window->set_cursor(cursor); + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + + +/* + 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/tools/dropper-tool.h b/src/ui/tools/dropper-tool.h new file mode 100644 index 0000000..4584ae9 --- /dev/null +++ b/src/ui/tools/dropper-tool.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_DROPPER_CONTEXT_H__ +#define __SP_DROPPER_CONTEXT_H__ + +/* + * Tool for picking colors from drawing + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> + +#include "color-rgba.h" +#include "ui/tools/tool-base.h" + +struct SPCanvasItem; + +#define SP_DROPPER_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::DropperTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_DROPPER_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::DropperTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +enum { + SP_DROPPER_PICK_VISIBLE, + SP_DROPPER_PICK_ACTUAL +}; +enum { + DONT_REDRAW_CURSOR, + DRAW_FILL_CURSOR, + DRAW_STROKE_CURSOR +}; + +namespace Inkscape { + +class CanvasItemBpath; + +namespace UI { +namespace Tools { + +class DropperTool : public ToolBase { +public: + DropperTool(SPDesktop *desktop); + ~DropperTool() override; + + guint32 get_color(bool invert = false, bool non_dropping = false); + sigc::signal<void, ColorRGBA *> onetimepick_signal; + +protected: + bool root_handler(GdkEvent *event) override; + +private: + // Stored color. + double R = 0.0; + double G = 0.0; + double B = 0.0; + double alpha = 0.0; + // Stored color taken from canvas. Used by clipboard. + // Identical to R, G, B, alpha if dropping disabled. + double non_dropping_R = 0.0; + double non_dropping_G = 0.0; + double non_dropping_B = 0.0; + double non_dropping_A = 0.0; + + bool invert = false; ///< Set color to inverse rgb value + bool stroke = false; ///< Set to stroke color. In dropping mode, set from stroke color + bool dropping = false; ///< When true, get color from selected objects instead of canvas + bool dragging = false; ///< When true, get average color for region on canvas, instead of a single point + + double radius = 0.0; ///< Size of region under dragging mode + Inkscape::CanvasItemBpath* area = nullptr; ///< Circle depicting region's borders in dragging mode + Geom::Point centre {0, 0}; ///< Center of region in dragging mode + +}; + +} +} +} + +#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/tools/dynamic-base.cpp b/src/ui/tools/dynamic-base.cpp new file mode 100644 index 0000000..de0ab07 --- /dev/null +++ b/src/ui/tools/dynamic-base.cpp @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Common drawing mode. Base class of Eraser and Calligraphic tools. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/dynamic-base.h" + +#include "message-context.h" +#include "desktop.h" + +#include "display/curve.h" +#include "display/control/canvas-item-bpath.h" + +#include "util/units.h" + +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::Util::unit_table; + +#define MIN_PRESSURE 0.0 +#define MAX_PRESSURE 1.0 +#define DEFAULT_PRESSURE 1.0 + +#define DRAG_MIN 0.0 +#define DRAG_DEFAULT 1.0 +#define DRAG_MAX 1.0 + +namespace Inkscape { +namespace UI { +namespace Tools { + +DynamicBase::DynamicBase(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename) + : ToolBase(desktop, prefs_path, cursor_filename) + , accumulated(nullptr) + , currentshape(nullptr) + , currentcurve(nullptr) + , cal1(nullptr) + , cal2(nullptr) + , point1() + , point2() + , npoints(0) + , repr(nullptr) + , cur(0, 0) + , vel(0, 0) + , vel_max(0) + , acc(0, 0) + , ang(0, 0) + , last(0, 0) + , del(0, 0) + , pressure(DEFAULT_PRESSURE) + , xtilt(0) + , ytilt(0) + , dragging(false) + , usepressure(false) + , usetilt(false) + , mass(0.3) + , drag(DRAG_DEFAULT) + , angle(30.0) + , width(0.2) + , vel_thin(0.1) + , flatness(0.9) + , tremor(0) + , cap_rounding(0) + , is_drawing(false) + , abs_width(false) +{ +} + +DynamicBase::~DynamicBase() { + for (auto segment : segments) { + delete segment; + } + segments.clear(); + + if (this->currentshape) { + delete currentshape; + } +} + +void DynamicBase::set(const Inkscape::Preferences::Entry& value) { + Glib::ustring path = value.getEntryName(); + + // ignore preset modifications + static Glib::ustring const presets_path = getPrefsPath() + "/preset"; + Glib::ustring const &full_path = value.getPath(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Unit const *unit = unit_table.getUnit(prefs->getString("/tools/calligraphic/unit")); + + if (full_path.compare(0, presets_path.size(), presets_path) == 0) { + return; + } + + if (path == "mass") { + this->mass = 0.01 * CLAMP(value.getInt(10), 0, 100); + } else if (path == "wiggle") { + this->drag = CLAMP((1 - 0.01 * value.getInt()), DRAG_MIN, DRAG_MAX); // drag is inverse to wiggle + } else if (path == "angle") { + this->angle = CLAMP(value.getDouble(), -90, 90); + } else if (path == "width") { + this->width = 0.01 * CLAMP(value.getDouble(), Quantity::convert(0.001, unit, "px"), Quantity::convert(100, unit, "px")); + } else if (path == "thinning") { + this->vel_thin = 0.01 * CLAMP(value.getInt(10), -100, 100); + } else if (path == "tremor") { + this->tremor = 0.01 * CLAMP(value.getInt(), 0, 100); + } else if (path == "flatness") { + this->flatness = 0.01 * CLAMP(value.getInt(), -100, 100); + } else if (path == "usepressure") { + this->usepressure = value.getBool(); + } else if (path == "usetilt") { + this->usetilt = value.getBool(); + } else if (path == "abs_width") { + this->abs_width = value.getBool(); + } else if (path == "cap_rounding") { + this->cap_rounding = value.getDouble(); + } +} + +/* Get normalized point */ +Geom::Point DynamicBase::getNormalizedPoint(Geom::Point v) const { + auto drect = _desktop->get_display_area(); + + double const max = drect.maxExtent(); + + return (v - drect.bounds().min()) / max; +} + +/* Get view point */ +Geom::Point DynamicBase::getViewPoint(Geom::Point n) const { + auto drect = _desktop->get_display_area(); + + double const max = drect.maxExtent(); + + return n * max + drect.bounds().min(); +} + +} +} +} + +/* + 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/tools/dynamic-base.h b/src/ui/tools/dynamic-base.h new file mode 100644 index 0000000..ce62166 --- /dev/null +++ b/src/ui/tools/dynamic-base.h @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef COMMON_CONTEXT_H_SEEN +#define COMMON_CONTEXT_H_SEEN + +/* + * Common drawing mode. Base class of Eraser and Calligraphic tools. + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * The original dynadraw code: + * Paul Haeberli <paul@sgi.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2008 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/tool-base.h" + +#include <memory> + +class SPCurve; + +namespace Inkscape { + namespace XML { + class Node; + } +} + +#define SAMPLING_SIZE 8 /* fixme: ?? */ + +namespace Inkscape { + +class CanvasItemBpath; + +namespace UI { +namespace Tools { + +class DynamicBase : public ToolBase { +public: + DynamicBase(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename); + ~DynamicBase() override; + + void set(const Inkscape::Preferences::Entry& val) override; + +protected: + /** accumulated shape which ultimately goes in svg:path */ + std::unique_ptr<SPCurve> accumulated; + + /** canvas items for "committed" segments */ + std::vector<Inkscape::CanvasItemBpath *> segments; + + /** canvas item for red "leading" segment */ + Inkscape::CanvasItemBpath *currentshape; + + /** shape of red "leading" segment */ + std::unique_ptr<SPCurve> currentcurve; + + /** left edge of the stroke; combined to get accumulated */ + std::unique_ptr<SPCurve> cal1; + + /** right edge of the stroke; combined to get accumulated */ + std::unique_ptr<SPCurve> cal2; + + /** left edge points for this segment */ + Geom::Point point1[SAMPLING_SIZE]; + + /** right edge points for this segment */ + Geom::Point point2[SAMPLING_SIZE]; + + /** number of edge points for this segment */ + gint npoints; + + /* repr */ + Inkscape::XML::Node *repr; + + /* common */ + Geom::Point cur; + Geom::Point vel; + double vel_max; + Geom::Point acc; + Geom::Point ang; + Geom::Point last; + Geom::Point del; + + /* extended input data */ + gdouble pressure; + gdouble xtilt; + gdouble ytilt; + + /* attributes */ + bool dragging; /* mouse state: mouse is dragging */ + bool usepressure; + bool usetilt; + double mass, drag; + double angle; + double width; + + double vel_thin; + double flatness; + double tremor; + double cap_rounding; + + bool is_drawing; + + /** uses absolute width independent of zoom */ + bool abs_width; + + Geom::Point getViewPoint(Geom::Point n) const; + Geom::Point getNormalizedPoint(Geom::Point v) const; +}; + +} +} +} + +#endif // COMMON_CONTEXT_H_SEEN + +/* + 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/tools/eraser-tool.cpp b/src/ui/tools/eraser-tool.cpp new file mode 100644 index 0000000..5b9cbab --- /dev/null +++ b/src/ui/tools/eraser-tool.cpp @@ -0,0 +1,1229 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Eraser drawing mode + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Rafael Siejakowski <rs@rs-math.net> + * + * The original dynadraw code: + * Paul Haeberli <paul@sgi.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2005-2007 bulia byak + * Copyright (C) 2006 MenTaLguY + * Copyright (C) 2008 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noERASER_VERBOSE + +#include "eraser-tool.h" + +#include <string> +#include <cstring> +#include <numeric> + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/bezier-utils.h> +#include <2geom/pathvector.h> + +#include "context-fns.h" +#include "desktop-events.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "layer-manager.h" +#include "message-context.h" +#include "message-stack.h" +#include "path-chemistry.h" +#include "rubberband.h" +#include "selection-chemistry.h" +#include "selection.h" + +#include "display/curve.h" +#include "display/control/canvas-item-bpath.h" + +#include "include/macros.h" + +#include "object/sp-clippath.h" +#include "object/sp-image.h" +#include "object/sp-item-group.h" +#include "object/sp-path.h" +#include "object/sp-rect.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-use.h" + +#include "ui/icon-names.h" + +#include "svg/svg.h" + + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +extern EraserToolMode const DEFAULT_ERASER_MODE = EraserToolMode::CUT; + +EraserTool::EraserTool(SPDesktop *desktop) + : DynamicBase(desktop, "/tools/eraser", "eraser.svg") +{ + accumulated.reset(new SPCurve()); + currentcurve.reset(new SPCurve()); + + cal1.reset(new SPCurve()); + cal2.reset(new SPCurve()); + + currentshape = new Inkscape::CanvasItemBpath(desktop->getCanvasSketch()); + currentshape->set_stroke(0x0); + currentshape->set_fill(trace_color_rgba, trace_wind_rule); + + /* fixme: Cannot we cascade it to root more clearly? */ + currentshape->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), desktop)); + + sp_event_context_read(this, "mass"); + sp_event_context_read(this, "wiggle"); + sp_event_context_read(this, "angle"); + sp_event_context_read(this, "width"); + sp_event_context_read(this, "thinning"); + sp_event_context_read(this, "tremor"); + sp_event_context_read(this, "flatness"); + sp_event_context_read(this, "tracebackground"); + sp_event_context_read(this, "usepressure"); + sp_event_context_read(this, "usetilt"); + sp_event_context_read(this, "abs_width"); + sp_event_context_read(this, "cap_rounding"); + + is_drawing = false; + //TODO not sure why get 0.01 if slider width == 0, maybe a double/int problem + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/eraser/selcue", false) != 0) { + enableSelectionCue(); + } + _updateMode(); + + // TODO temp force: + enableSelectionCue(); +} + +EraserTool::~EraserTool() +{ + delete currentshape; + currentshape = nullptr; +} + +/** Reads the current Eraser mode from Preferences and sets `mode` accordingly. */ +void EraserTool::_updateMode() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs) { + return; + } + + int mode_idx = prefs->getInt("/tools/eraser/mode", 1); // Cut mode is default + + // Note: the integer indices must agree with those in EraserToolbar::_modeAsInt() + if (mode_idx == 0) { + mode = EraserToolMode::DELETE; + } else if (mode_idx == 1) { + mode = EraserToolMode::CUT; + } else if (mode_idx == 2) { + mode = EraserToolMode::CLIP; + } else { + g_printerr("Error: invalid mode setting \"%d\" for Eraser tool!", mode_idx); + mode = DEFAULT_ERASER_MODE; + } +} + +// TODO: After switch to C++20, replace this with std::lerp +inline double flerp(double const f0, double const f1, double const p) +{ + return f0 + (f1 - f0) * p; +} + +inline double square(double const x) +{ + return x * x; +} + +void EraserTool::_reset(Geom::Point p) +{ + last = cur = getNormalizedPoint(p); + vel = Geom::Point(0, 0); + vel_max = 0; + acc = Geom::Point(0, 0); + ang = Geom::Point(0, 0); + del = Geom::Point(0, 0); +} + +void EraserTool::_extinput(GdkEvent *event) +{ + if (gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &pressure)) { + pressure = CLAMP(pressure, min_pressure, max_pressure); + } else { + pressure = default_pressure; + } + + if (gdk_event_get_axis(event, GDK_AXIS_XTILT, &xtilt)) { + xtilt = CLAMP(xtilt, min_tilt, max_tilt); + } else { + xtilt = default_tilt; + } + + if (gdk_event_get_axis(event, GDK_AXIS_YTILT, &ytilt)) { + ytilt = CLAMP(ytilt, min_tilt, max_tilt); + } else { + ytilt = default_tilt; + } +} + +bool EraserTool::_apply(Geom::Point p) +{ + /* Calculate force and acceleration */ + Geom::Point n = getNormalizedPoint(p); + Geom::Point force = n - cur; + + // If force is below the absolute threshold `epsilon`, + // or we haven't yet reached `vel_start` (i.e. at the beginning of stroke) + // _and_ the force is below the (higher) `epsilon_start` threshold, + // discard this move. + // This prevents flips, blobs, and jerks caused by microscopic tremor of the tablet pen, + // especially bothersome at the start of the stroke where we don't yet have the inertia to + // smooth them out. + if (Geom::L2(force) < epsilon || (vel_max < vel_start && Geom::L2(force) < epsilon_start)) { + return false; + } + + // Calculate mass + double const m = flerp(1.0, 160.0, mass); + acc = force / m; + vel += acc; // Calculate new velocity + double const speed = Geom::L2(vel); + + if (speed > vel_max) { + vel_max = speed; + } else if (speed < epsilon) { + return false; // return early if movement is insignificant + } + + /* Calculate angle of eraser tool */ + double angle_fixed{0.0}; + if (usetilt) { + // 1a. calculate nib angle from input device tilt: + Geom::Point normal{ytilt, xtilt}; + if (!Geom::is_zero(normal)) { + angle_fixed = Geom::atan2(normal); + } + } else { + // 1b. fixed angle (absolutely flat nib): + angle_fixed = angle * M_PI / 180.0; // convert to radians + } + if (flatness < 0.0) { + // flips direction. Useful when usetilt is true + // allows simulating both pen/charcoal and broad-nibbed pen + angle_fixed *= -1; + } + + // 2. Angle perpendicular to vel (absolutely non-flat nib): + double angle_dynamic = Geom::atan2(Geom::rot90(vel)); + // flip angle_dynamic to force it to be in the same half-circle as angle_fixed + bool flipped = false; + if (fabs(angle_dynamic - angle_fixed) > M_PI_2) { + angle_dynamic += M_PI; + flipped = true; + } + // normalize angle_dynamic + if (angle_dynamic > M_PI) { + angle_dynamic -= 2 * M_PI; + } + if (angle_dynamic < -M_PI) { + angle_dynamic += 2 * M_PI; + } + + // 3. Average them using flatness parameter: + // find the flatness-weighted bisector angle, unflip if angle_dynamic was flipped + // FIXME: when `vel` is oscillating around the fixed angle, the new_ang flips back and forth. + // How to avoid this? + double new_ang = flerp(angle_dynamic, angle_fixed, fabs(flatness)) - (flipped ? M_PI : 0); + + // Try to detect a sudden flip when the new angle differs too much from the previous for the + // current velocity; in that case discard this move + double angle_delta = Geom::L2(Geom::Point(cos(new_ang), sin(new_ang)) - ang); + if (angle_delta / speed > 4000) { + return false; + } + + // convert to point + ang = Geom::Point(cos(new_ang), sin(new_ang)); + + /* Apply drag */ + double const d = flerp(0.0, 0.5, square(drag)); + vel *= 1.0 - d; + + /* Update position */ + last = cur; + cur += vel; + + return true; +} + +void EraserTool::_brush() +{ + g_assert(npoints >= 0 && npoints < SAMPLING_SIZE); + + // How much velocity thins strokestyle + double const vel_thinning = flerp(0, 160, vel_thin); + + // Influence of pressure on thickness + double const pressure_thick = (usepressure ? pressure : 1.0); + + // get the real brush point, not the same as pointer (affected by mass drag) + Geom::Point brush = getViewPoint(cur); + + double const trace_thick = 1; + double const speed = Geom::L2(vel); + double effective_width = (pressure_thick * trace_thick - vel_thinning * speed) * width; + + double tremble_left = 0, tremble_right = 0; + if (tremor > 0) { + // obtain two normally distributed random variables, using polar Box-Muller transform + double y1, y2; + _generateNormalDist2(y1, y2); + + // deflect both left and right edges randomly and independently, so that: + // (1) tremor=1 corresponds to sigma=1, decreasing tremor narrows the bell curve; + // (2) deflection depends on width, but is upped for small widths for better visual uniformity across widths; + // (3) deflection somewhat depends on speed, to prevent fast strokes looking + // comparatively smooth and slow ones excessively jittery + double const width_coefficient = 0.15 + 0.8 * effective_width; + double const speed_coefficient = 0.35 + 14 * speed; + double const total_coefficient = tremor * width_coefficient * speed_coefficient; + + tremble_left = y1 * total_coefficient; + tremble_right = y2 * total_coefficient; + } + + double const min_width = 0.02 * width; + if (effective_width < min_width) { + effective_width = min_width; + } + + double dezoomify_factor = 0.05 * 1000; + if (!abs_width) { + dezoomify_factor /= _desktop->current_zoom(); + } + + Geom::Point del_left = dezoomify_factor * (effective_width + tremble_left) * ang; + Geom::Point del_right = dezoomify_factor * (effective_width + tremble_right) * ang; + + point1[npoints] = brush + del_left; + point2[npoints] = brush - del_right; + + if (nowidth) { + point1[npoints] = Geom::middle_point(point1[npoints], point2[npoints]); + } + del = Geom::middle_point(del_left, del_right); + + npoints++; +} + +void EraserTool::_generateNormalDist2(double &r1, double &r2) +{ + // obtain two normally distributed random variables, using polar Box-Muller transform + double x1, x2, w; + do { + x1 = 2.0 * g_random_double_range(0, 1) - 1.0; + x2 = 2.0 * g_random_double_range(0, 1) - 1.0; + w = square(x1) + square(x2); + } while (w >= 1.0); + w = sqrt(-2.0 * log(w) / w); + r1 = x1 * w; + r2 = x2 * w; +} + +void EraserTool::_cancel() +{ + dragging = false; + is_drawing = false; + ungrabCanvasEvents(); + + _removeTemporarySegments(); + + /* reset accumulated curve */ + accumulated->reset(); + _clearCurrent(); + repr = nullptr; +} + +/** Removes all temporary line segments */ +void EraserTool::_removeTemporarySegments() +{ + for (auto segment : segments) { + delete segment; + } + segments.clear(); +} + +bool EraserTool::root_handler(GdkEvent* event) +{ + bool ret = false; + _updateMode(); + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + if (!Inkscape::have_viable_layer(_desktop, defaultMessageContext())) { + return true; + } + + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point const button_dt(_desktop->w2d(button_w)); + + _reset(button_dt); + _extinput(event); + _apply(button_dt); + accumulated->reset(); + + repr = nullptr; + + if (mode == EraserToolMode::DELETE) { + auto rubberband = Inkscape::Rubberband::get(_desktop); + rubberband->start(_desktop, button_dt); + rubberband->setMode(RUBBERBAND_MODE_TOUCHPATH); + } + /* initialize first point */ + npoints = 0; + + grabCanvasEvents(); + is_drawing = true; + ret = true; + } + break; + + case GDK_MOTION_NOTIFY: { + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + _extinput(event); + + message_context->clear(); + + if (is_drawing && (event->motion.state & GDK_BUTTON1_MASK)) { + dragging = true; + + message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> an eraser stroke")); + + if (!_apply(motion_dt)) { + ret = true; + break; + } + + if (cur != last) { + _brush(); + g_assert(npoints > 0); + _fitAndSplit(false); + } + + ret = true; + } + if (mode == EraserToolMode::DELETE) { + accumulated->reset(); + Inkscape::Rubberband::get(_desktop)->move(motion_dt); + } + break; + } + case GDK_BUTTON_RELEASE: { + if (event->button.button != 1) { + break; + } + + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + + ungrabCanvasEvents(); + + is_drawing = false; + + if (dragging) { + dragging = false; + + _apply(motion_dt); + _removeTemporarySegments(); + + /* Create object */ + _fitAndSplit(true); + _accumulate(); + _setToAccumulated(); // performs document_done + + /* reset accumulated curve */ + accumulated->reset(); + + _clearCurrent(); + repr = nullptr; + + message_context->clear(); + ret = true; + } + + if (mode == EraserToolMode::DELETE) { + auto r = Inkscape::Rubberband::get(_desktop); + if (r->is_started()) { + r->stop(); + } + } + + break; + } + case GDK_KEY_PRESS: + ret = _handleKeypress(&event->key); + break; + + case GDK_KEY_RELEASE: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + message_context->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = DynamicBase::root_handler(event); + } + return ret; +} + +/** Analyses and handles a key press event, returns true if processed, false if not. */ +bool EraserTool::_handleKeypress(const GdkEventKey *key) +{ + bool ret = false; + bool just_ctrl = (key->state & GDK_CONTROL_MASK) // Ctrl key is down + && !(key->state & (GDK_MOD1_MASK | GDK_SHIFT_MASK)); // but not Alt or Shift + + bool just_alt = (key->state & GDK_MOD1_MASK) // Alt is down + && !(key->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)); // but not Ctrl or Shift + + switch (get_latin_keyval(key)) { + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (!just_ctrl) { + width += 0.01; + if (width > 1.0) { + width = 1.0; + } + // Alt+X sets focus to this spinbutton as well + _desktop->setToolboxAdjustmentValue("eraser-width", width * 100); + ret = true; + } + break; + + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (!just_ctrl) { + width -= 0.01; + if (width < 0.01) { + width = 0.01; + } + _desktop->setToolboxAdjustmentValue("eraser-width", width * 100); + ret = true; + } + break; + + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + width = 0.01; + _desktop->setToolboxAdjustmentValue("eraser-width", width * 100); + ret = true; + break; + + case GDK_KEY_End: + case GDK_KEY_KP_End: + width = 1.0; + _desktop->setToolboxAdjustmentValue("eraser-width", width * 100); + ret = true; + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (just_alt) { + _desktop->setToolboxFocusTo("eraser-width"); + ret = true; + } + break; + + case GDK_KEY_Escape: + if (mode == EraserToolMode::DELETE) { + Inkscape::Rubberband::get(_desktop)->stop(); + } + if (is_drawing) { + // if drawing, cancel, otherwise pass it up for deselecting + _cancel(); + ret = true; + } + break; + + case GDK_KEY_z: + case GDK_KEY_Z: + if (just_ctrl && is_drawing) { // Ctrl+Z pressed while drawing + _cancel(); + ret = true; + } // if not drawing, pass it up for undo + break; + + default: + break; + } + return ret; +} + +void EraserTool::_clearCurrent() +{ + // reset bpath + currentshape->set_bpath(nullptr); + + // reset curve + currentcurve->reset(); + cal1->reset(); + cal2->reset(); + + // reset points + npoints = 0; +} + +void EraserTool::_setToAccumulated() +{ + bool work_done = false; + SPDocument *document = _desktop->doc(); + + if (!accumulated->is_empty()) { + if (!repr) { + /* Create object */ + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *eraser_repr = xml_doc->createElement("svg:path"); + + /* Set style */ + sp_desktop_apply_style_tool(_desktop, eraser_repr, "/tools/eraser", false); + + repr = eraser_repr; + } + SPObject *top_layer = _desktop->layerManager().currentRoot(); + SPItem *item_repr = SP_ITEM(top_layer->appendChildRepr(repr)); + Inkscape::GC::release(repr); + item_repr->updateRepr(); + Geom::PathVector pathv = accumulated->get_pathvector() * _desktop->dt2doc(); + pathv *= item_repr->i2doc_affine().inverse(); + repr->setAttribute("d", sp_svg_write_path(pathv)); + Geom::OptRect eraser_bbox; + if (repr) { + bool was_selection = false; + Inkscape::Selection *selection = _desktop->getSelection(); + _updateMode(); + SPItem *acid = SP_ITEM(document->getObjectByRepr(repr)); + eraser_bbox = acid->documentVisualBounds(); + std::vector<SPItem *> remaining_items; + std::vector<SPItem *> to_work_on; + if (selection->isEmpty()) { + if (mode == EraserToolMode::CUT || mode == EraserToolMode::CLIP) { + to_work_on = document->getItemsPartiallyInBox(_desktop->dkey, *eraser_bbox, + false, false, false, true); + } else { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop); + to_work_on = document->getItemsAtPoints(_desktop->dkey, r->getPoints()); + } + to_work_on.erase(std::remove(to_work_on.begin(), to_work_on.end(), acid), to_work_on.end()); + } else { + if (mode == EraserToolMode::DELETE) { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop); + auto selected_items = selection->items(); + std::vector<SPItem *> touched = document->getItemsAtPoints(_desktop->dkey, r->getPoints()); + for (auto i : selected_items) { + if (std::find(touched.begin(), touched.end(), i) == touched.end()) { + remaining_items.push_back(i); + } else { + to_work_on.push_back(i); + } + } + } else { + to_work_on.insert(to_work_on.end(), selection->items().begin(), selection->items().end()); + } + was_selection = true; + } + + if (to_work_on.empty()) { + _clearStatusBar(); + } else { + selection->clear(); + if (mode == EraserToolMode::CUT) { + Error status = ALL_GOOD; + for (auto item : to_work_on) { + Error retval = _cutErase(item, eraser_bbox, remaining_items); + if (retval == ALL_GOOD) { + work_done = true; + } else { + status |= retval; + } + } + + status &= ~(NOT_IN_BOUNDS | NON_EXISTENT); // Clear flags not handled at the moment + if (status == ALL_GOOD) { + _clearStatusBar(); + } else { // Something went wrong during the cut operation + if (status & RASTER_IMAGE) { + _setStatusBarMessage(_("Cannot cut out from a bitmap, use <b>Clip</b> mode " + "instead.")); + } else if (status & ERROR_GROUP) { + _setStatusBarMessage(_("Cannot cut out from a group, ungroup the objects " + "first.")); + } else if (status & NO_AREA_PATH) { + _setStatusBarMessage(_("Cannot cut out from a path with zero area, use " + "<b>Clip</b> mode instead.")); + } + } + } else if (mode == EraserToolMode::CLIP) { + if (!nowidth) { + for (auto item : to_work_on) { + _clipErase(item, item_repr->parent, eraser_bbox); + } + if (was_selection) { + remaining_items = to_work_on; + } + work_done = true; + } + _clearStatusBar(); + } else if (mode == EraserToolMode::DELETE) { + for (auto item : to_work_on) { + item->deleteObject(true); + } + work_done = true; + _clearStatusBar(); + } + + if (was_selection && !remaining_items.empty()) { + selection->add(remaining_items.begin(), remaining_items.end()); + } + } + // Remove the eraser stroke itself: + sp_repr_unparent(repr); + repr = nullptr; + } + } else if (repr) { + sp_repr_unparent(repr); + repr = nullptr; + } + + if (work_done) { + DocumentUndo::done(document, _("Draw eraser stroke"), INKSCAPE_ICON("draw-eraser")); + } else { + DocumentUndo::cancel(document); + } +} + +/** + * @brief Erases from a shape by cutting + * @param item - the item to be erased + * @param eraser_bbox - bounding box of the eraser stroke + * @param survivers - items that survived the erase operation will be added to this vector + * @return type of error encountered + */ +EraserTool::Error EraserTool::_cutErase(SPItem* item, Geom::OptRect const &eraser_bbox, + std::vector<SPItem *> &survivers) +{ + // If the item cannot be cut, preserve it + if (Error error = EraserTool::_uncuttableItemType(item)) { + survivers.push_back(item); + return error; + } + + Geom::OptRect bbox = item->documentVisualBounds(); + if (!bbox || !bbox->intersects(eraser_bbox)) { + survivers.push_back(item); + return NOT_IN_BOUNDS; + } + + // If the item is a clone, we check if the original is cuttable before unlinking it + if (SPUse *use = dynamic_cast<SPUse *>(item)) { + int depth = use->cloneDepth(); + if (depth < 0) { + survivers.push_back(item); + return NON_EXISTENT; + } + // We recurse into the chain of uses until we reach the original item + SPItem *original_item = item; + for (int i = 0; i < depth; ++i) { + SPUse *intermediate_clone = dynamic_cast<SPUse *>(original_item); + original_item = intermediate_clone->get_original(); + } + if (Error error = EraserTool::_uncuttableItemType(original_item)) { + survivers.push_back(item); + return error; + } + item = use->unlink(); + } + + _booleanErase(item, survivers); + return ALL_GOOD; +} + +/** Returns error flags for items that cannot be meaningfully erased in CUT mode */ +EraserTool::Error EraserTool::_uncuttableItemType(SPItem *item) +{ + if (!item) { + return NON_EXISTENT; + } else if (dynamic_cast<SPGroup *>(item)) { + return ERROR_GROUP; // TODO: handle groups in the future + } else if (dynamic_cast<SPImage *>(item)) { + return RASTER_IMAGE; + } else if (_isStraightSegment(item)) { + return NO_AREA_PATH; + } else { + return ALL_GOOD; + } +} + +/** + * @brief Performs a boolean difference or cut operation which implements the CUT mode operation + * @param erasee - the item to be erased + * @param survivers - items that survived the erase operation will be added to this vector + */ +void EraserTool::_booleanErase(SPItem *erasee, std::vector<SPItem *> &survivers) const +{ + XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + XML::Node *duplicate_stroke = repr->duplicate(xml_doc); + repr->parent()->appendChild(duplicate_stroke); + GC::release(duplicate_stroke); // parent takes over + ObjectSet operands(_desktop); + operands.set(duplicate_stroke); + if (!nowidth) { + operands.pathUnion(true, true); + } + operands.add(erasee); + operands.removeLPESRecursive(true); + + _handleStrokeStyle(erasee); + + if (nowidth) { + operands.pathCut(true, true); + } else { + operands.pathDiff(true, true); + } + + auto *prefs = Preferences::get(); + bool break_apart = prefs->getBool("/tools/eraser/break_apart", false); + if (!break_apart) { + operands.combine(true, true); + } else if (!nowidth) { + operands.breakApart(true, false, true); + } + survivers.insert(survivers.end(), operands.items().begin(), operands.items().end()); +} + +/** Handles the "evenodd" stroke style */ +void EraserTool::_handleStrokeStyle(SPItem *item) const +{ + if (item->style->fill_rule.value == SP_WIND_RULE_EVENODD) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-rule", "evenodd"); + sp_desktop_set_style(_desktop, css); + sp_repr_css_attr_unref(css); + css = nullptr; + } +} + +/** Sets an error message in the status bar */ +void EraserTool::_setStatusBarMessage(char *message) +{ + MessageId id = _desktop->messageStack()->flash(WARNING_MESSAGE, message); + _our_messages.push_back(id); +} + +/** Clears all of messages sent by us to the status bar */ +void EraserTool::_clearStatusBar() +{ + if (!_our_messages.empty()) { + auto ms = _desktop->messageStack(); + for (MessageId id : _our_messages) { + ms->cancel(id); + } + _our_messages.clear(); + } +} + +/** Clips through an item */ +void EraserTool::_clipErase(SPItem *item, SPObject *parent, Geom::OptRect &eraser_box) +{ + Inkscape::ObjectSet w_selection(_desktop); + Geom::OptRect bbox = item->documentVisualBounds(); + Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + Inkscape::XML::Node *dup = repr->duplicate(xml_doc); + repr->parent()->appendChild(dup); + Inkscape::GC::release(dup); // parent takes over + w_selection.set(dup); + w_selection.pathUnion(true); + bool delete_old_clip_path = false; + if (bbox && bbox->intersects(*eraser_box)) { + SPClipPath *clip_path = item->getClipObject(); + if (clip_path) { + std::vector<SPItem *> selected; + selected.push_back(SP_ITEM(clip_path->firstChild())); + std::vector<Inkscape::XML::Node *> to_select; + std::vector<SPItem *> items(selected); + sp_item_list_to_curves(items, selected, to_select); + Inkscape::XML::Node *clip_data = SP_ITEM(clip_path->firstChild())->getRepr(); + if (!clip_data && !to_select.empty()) { + clip_data = *(to_select.begin()); + } + if (clip_data) { + Inkscape::XML::Node *dup_clip = clip_data->duplicate(xml_doc); + if (dup_clip) { + SPItem *dup_clip_obj = SP_ITEM(parent->appendChildRepr(dup_clip)); + Inkscape::GC::release(dup_clip); + if (dup_clip_obj) { + dup_clip_obj->transform *= item->getRelativeTransform(SP_ITEM(parent)); + dup_clip_obj->updateRepr(); + delete_old_clip_path = true; + w_selection.raiseToTop(true); + w_selection.add(dup_clip); + w_selection.pathDiff(true, true); + } + } + } + } else { + Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect"); + sp_desktop_apply_style_tool(_desktop, rect_repr, "/tools/eraser", false); + SPRect *rect = SP_RECT(parent->appendChildRepr(rect_repr)); + Inkscape::GC::release(rect_repr); + rect->setPosition(bbox->left(), bbox->top(), bbox->width(), bbox->height()); + rect->transform = SP_ITEM(rect->parent)->i2doc_affine().inverse(); + + rect->updateRepr(); + rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + w_selection.raiseToTop(true); + w_selection.add(rect); + w_selection.pathDiff(true, true); + } + w_selection.raiseToTop(true); + w_selection.add(item); + w_selection.setMask(true, false, true); + if (delete_old_clip_path) { + clip_path->deleteObject(true); + } + } else { + SPItem *erase_clip = w_selection.singleItem(); + if (erase_clip) { + erase_clip->deleteObject(true); + } + } +} + +/** Detects whether the given path is a straight line segment which encloses no area + or consists of several such segments */ +bool EraserTool::_isStraightSegment(SPItem *path) +{ + SPPath *as_path = dynamic_cast<SPPath *>(path); + if (!as_path) { + return false; + } + + auto const &curve = as_path->curve(); + if (!curve) { + return false; + } + auto const &pathvector = curve->get_pathvector(); + + // Check if all segments are straight and collinear + for (auto const &path : pathvector) { + Geom::Point initial_tangent = path.front().unitTangentAt(0.0); + for (auto const &segment : path) { + if (!segment.isLineSegment()) { + return false; + } else { + Geom::Point dir = segment.unitTangentAt(0.0); + if (!Geom::are_near(dir, initial_tangent) && !Geom::are_near(-dir, initial_tangent)) { + return false; + } + } + } + } + return true; +} + +void EraserTool::_addCap(SPCurve &curve, Geom::Point const &pre, Geom::Point const &from, Geom::Point const &to, + Geom::Point const &post, double rounding) +{ + Geom::Point vel = rounding * Geom::rot90(to - from) / M_SQRT2; + double mag = Geom::L2(vel); + + Geom::Point v_in = from - pre; + double mag_in = Geom::L2(v_in); + + if (mag_in > epsilon) { + v_in = mag * v_in / mag_in; + } else { + v_in = Geom::Point(0, 0); + } + + Geom::Point v_out = to - post; + double mag_out = Geom::L2(v_out); + + if (mag_out > epsilon) { + v_out = mag * v_out / mag_out; + } else { + v_out = Geom::Point(0, 0); + } + + if (Geom::L2(v_in) > epsilon || Geom::L2(v_out) > epsilon) { + curve.curveto(from + v_in, to + v_out, to); + } +} + +void EraserTool::_accumulate() +{ + // construct a crude outline of the eraser's path. + // this desperately needs to be rewritten to use the path outliner... + if (!cal1->get_segment_count() || !cal2->get_segment_count()) { + return; + } + + auto rev_cal2 = cal2->create_reverse(); + + g_assert(!cal1->first_path()->closed()); + g_assert(!rev_cal2->first_path()->closed()); + + Geom::BezierCurve const *dc_cal1_firstseg = dynamic_cast<Geom::BezierCurve const *>(cal1->first_segment()); + Geom::BezierCurve const *rev_cal2_firstseg = dynamic_cast<Geom::BezierCurve const *>(rev_cal2->first_segment()); + Geom::BezierCurve const *dc_cal1_lastseg = dynamic_cast<Geom::BezierCurve const *>(cal1->last_segment()); + Geom::BezierCurve const *rev_cal2_lastseg = dynamic_cast<Geom::BezierCurve const *>(rev_cal2->last_segment()); + + g_assert(dc_cal1_firstseg); + g_assert(rev_cal2_firstseg); + g_assert(dc_cal1_lastseg); + g_assert(rev_cal2_lastseg); + + accumulated->append(*cal1); + if (!nowidth) { + _addCap(*accumulated, + dc_cal1_lastseg->finalPoint() - dc_cal1_lastseg->unitTangentAt(1), + dc_cal1_lastseg->finalPoint(), + rev_cal2_firstseg->initialPoint(), + rev_cal2_firstseg->initialPoint() + rev_cal2_firstseg->unitTangentAt(0), + cap_rounding); + + accumulated->append(*rev_cal2, true); + + _addCap(*accumulated, + rev_cal2_lastseg->finalPoint() - rev_cal2_lastseg->unitTangentAt(1), + rev_cal2_lastseg->finalPoint(), + dc_cal1_firstseg->initialPoint(), + dc_cal1_firstseg->initialPoint() + dc_cal1_firstseg->unitTangentAt(0), + cap_rounding); + + accumulated->closepath(); + } + cal1->reset(); + cal2->reset(); +} + +void EraserTool::_fitAndSplit(bool releasing) +{ + double const tolerance_sq = square(_desktop->w2d().descrim() * tolerance); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + nowidth = (prefs->getDouble("/tools/eraser/width", 1) == 0); + +#ifdef ERASER_VERBOSE + g_print("[F&S:R=%c]", releasing ? 'T' : 'F'); +#endif + if (npoints >= SAMPLING_SIZE || npoints <= 0) { + return; // just clicked + } + + if (npoints == SAMPLING_SIZE - 1 || releasing) { + _completeBezier(tolerance_sq, releasing); + +#ifdef ERASER_VERBOSE + g_print("[%d]Yup\n", npoints); +#endif + if (!releasing) { + _fitDrawLastPoint(); + } + + // Copy last point + point1[0] = point1[npoints - 1]; + point2[0] = point2[npoints - 1]; + npoints = 1; + } else { + _drawTemporaryBox(); + } +} + +void EraserTool::_completeBezier(double tolerance_sq, bool releasing) +{ + /* Current eraser */ + if (cal1->is_empty() || cal2->is_empty()) { + /* dc->npoints > 0 */ + cal1->reset(); + cal2->reset(); + + cal1->moveto(point1[0]); + cal2->moveto(point2[0]); + } +#ifdef ERASER_VERBOSE + g_print("[F&S:#] npoints:%d, releasing:%s\n", npoints, releasing ? "TRUE" : "FALSE"); +#endif + + unsigned const bezier_size = 4; + unsigned const max_beziers = 8; + size_t const bezier_max_length = bezier_size * max_beziers; + + Geom::Point b1[bezier_max_length]; + gint const nb1 = Geom::bezier_fit_cubic_r(b1, point1, npoints, tolerance_sq, max_beziers); + g_assert(nb1 * bezier_size <= gint(G_N_ELEMENTS(b1))); + + Geom::Point b2[bezier_max_length]; + gint const nb2 = Geom::bezier_fit_cubic_r(b2, point2, npoints, tolerance_sq, max_beziers); + g_assert(nb2 * bezier_size <= gint(G_N_ELEMENTS(b2))); + + if (nb1 == -1 || nb2 == -1) { + _failedBezierFallback(); // TODO: do we ever need this? + return; + } + + /* Fit and draw and reset state */ +#ifdef ERASER_VERBOSE + g_print("nb1:%d nb2:%d\n", nb1, nb2); +#endif + + /* CanvasShape */ + if (!releasing) { + currentcurve->reset(); + currentcurve->moveto(b1[0]); + + for (Geom::Point *bp1 = b1; bp1 < b1 + bezier_size * nb1; bp1 += bezier_size) { + currentcurve->curveto(bp1[1], bp1[2], bp1[3]); + } + + currentcurve->lineto(b2[bezier_size * (nb2 - 1) + 3]); + + for (Geom::Point *bp2 = b2 + bezier_size * (nb2 - 1); bp2 >= b2; bp2 -= bezier_size) { + currentcurve->curveto(bp2[2], bp2[1], bp2[0]); + } + + // FIXME: segments is always NULL at this point?? + if (segments.empty()) { // first segment + _addCap(*currentcurve, b2[1], b2[0], b1[0], b1[1], cap_rounding); + } + + currentcurve->closepath(); + currentshape->set_bpath(currentcurve.get(), true); + } + + /* Current eraser */ + for (Geom::Point *bp1 = b1; bp1 < b1 + bezier_size * nb1; bp1 += bezier_size) { + cal1->curveto(bp1[1], bp1[2], bp1[3]); + } + + for (Geom::Point *bp2 = b2; bp2 < b2 + bezier_size * nb2; bp2 += bezier_size) { + cal2->curveto(bp2[1], bp2[2], bp2[3]); + } +} + +void EraserTool::_failedBezierFallback() +{ + /* fixme: ??? */ +#ifdef ERASER_VERBOSE + g_print("[_failedBezierFallback] - failed to fit cubic.\n"); +#endif + _drawTemporaryBox(); + + for (gint i = 1; i < npoints; i++) { + cal1->lineto(point1[i]); + } + + for (gint i = 1; i < npoints; i++) { + cal2->lineto(point2[i]); + } +} + +void EraserTool::_fitDrawLastPoint() +{ + g_assert(!currentcurve->is_empty()); + + guint32 fillColor = sp_desktop_get_color_tool(_desktop, "/tools/eraser", true); + double opacity = sp_desktop_get_master_opacity_tool(_desktop, "/tools/eraser"); + double fillOpacity = sp_desktop_get_opacity_tool(_desktop, "/tools/eraser", true); + + guint fill = (fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity * fillOpacity); + + auto cbp = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), currentcurve.get(), true); + cbp->set_fill(fill, trace_wind_rule); + cbp->set_stroke(0x0); + + /* fixme: Cannot we cascade it to root more clearly? */ + cbp->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), _desktop)); + segments.push_back(cbp); + + if (mode == EraserToolMode::DELETE) { + cbp->hide(); + currentshape->hide(); + } +} + +void EraserTool::_drawTemporaryBox() +{ + currentcurve->reset(); + + currentcurve->moveto(point1[npoints - 1]); + + for (gint i = npoints - 2; i >= 0; i--) { + currentcurve->lineto(point1[i]); + } + + for (gint i = 0; i < npoints; i++) { + currentcurve->lineto(point2[i]); + } + + if (npoints >= 2) { + _addCap(*currentcurve, + point2[npoints - 2], point2[npoints - 1], + point1[npoints - 1], point1[npoints - 2], cap_rounding); + } + + currentcurve->closepath(); + currentshape->set_bpath(currentcurve.get(), true); +} + +} // namespace Tools +} // 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/tools/eraser-tool.h b/src/ui/tools/eraser-tool.h new file mode 100644 index 0000000..2c9377a --- /dev/null +++ b/src/ui/tools/eraser-tool.h @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef ERASER_TOOL_H_SEEN +#define ERASER_TOOL_H_SEEN + +/* + * Handwriting-like drawing mode + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * The original dynadraw code: + * Paul Haeberli <paul@sgi.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2008 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> + +#include "message-stack.h" +#include "style.h" +#include "ui/tools/dynamic-base.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +enum class EraserToolMode +{ + DELETE, + CUT, + CLIP +}; +extern EraserToolMode const DEFAULT_ERASER_MODE; + +class EraserTool : public DynamicBase { + +private: + // non-static data: + EraserToolMode mode = DEFAULT_ERASER_MODE; + bool nowidth = false; + std::vector<MessageId> _our_messages; + + // static data: + inline static guint32 const trace_color_rgba = 0xff0000ff; // RGBA red + inline static SPWindRule const trace_wind_rule = SP_WIND_RULE_EVENODD; + + inline static double const tolerance = 0.1; + + inline static double const epsilon = 0.5e-6; + inline static double const epsilon_start = 0.5e-2; + inline static double const vel_start = 1e-5; + + inline static double const drag_default = 1.0; + inline static double const drag_min = 0.0; + inline static double const drag_max = 1.0; + + inline static double const min_pressure = 0.0; + inline static double const max_pressure = 1.0; + inline static double const default_pressure = 1.0; + + inline static double const min_tilt = -1.0; + inline static double const max_tilt = 1.0; + inline static double const default_tilt = 0.0; + +public: + // public member functions + EraserTool(SPDesktop *desktop); + ~EraserTool() override; + bool root_handler(GdkEvent *event) final; + + using Error = std::uint64_t; + inline static Error const ALL_GOOD = 0x0; + inline static Error const NOT_IN_BOUNDS = 0x1 << 0; + inline static Error const NON_EXISTENT = 0x1 << 1; + inline static Error const NO_AREA_PATH = 0x1 << 2; + inline static Error const RASTER_IMAGE = 0x1 << 3; + inline static Error const ERROR_GROUP = 0x1 << 4; + +private: + // private member functions + void _reset(Geom::Point p); + void _extinput(GdkEvent *event); + bool _apply(Geom::Point p); + void _brush(); + void _cancel(); + void _clearCurrent(); + void _setToAccumulated(); + void _accumulate(); + void _fitAndSplit(bool releasing); + void _drawTemporaryBox(); + bool _handleKeypress(GdkEventKey const *key); + void _completeBezier(double tolerance_sq, bool releasing); + void _failedBezierFallback(); + void _fitDrawLastPoint(); + void _clipErase(SPItem *item, SPObject *parent, Geom::OptRect &eraser_box); + Error _cutErase(SPItem *item, Geom::OptRect const &eraser_bbox, std::vector<SPItem *> &survivers); + void _booleanErase(SPItem *erasee, std::vector<SPItem*> &survivers) const; + void _handleStrokeStyle(SPItem *item) const; + void _updateMode(); + void _removeTemporarySegments(); + void _setStatusBarMessage(char *message); + void _clearStatusBar(); + + static void _generateNormalDist2(double &r1, double &r2); + static void _addCap(SPCurve &curve, Geom::Point const &pre, Geom::Point const &from, Geom::Point const &to, + Geom::Point const &post, double rounding); + static bool _isStraightSegment(SPItem *path); + static Error _uncuttableItemType(SPItem *item); +}; + +} // namespace Tools +} // namespace UI +} // namespace Inkscape + +#endif // ERASER_TOOL_H_SEEN + +/* + 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/tools/flood-tool.cpp b/src/ui/tools/flood-tool.cpp new file mode 100644 index 0000000..62ef634 --- /dev/null +++ b/src/ui/tools/flood-tool.cpp @@ -0,0 +1,1239 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Bucket fill drawing context, works by bitmap filling an area on a rendered version + * of the current display and then tracing the result using potrace. + */ +/* Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * John Bintz <jcoswell@coswellproductions.org> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2000-2005 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "flood-tool.h" + +#include <cmath> +#include <queue> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/pathvector.h> + +#include "color.h" +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "layer-manager.h" +#include "message-context.h" +#include "message-stack.h" +#include "rubberband.h" +#include "selection.h" +#include "page-manager.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-image.h" +#include "display/drawing.h" + +#include "include/macros.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "object/sp-root.h" + +#include "svg/svg.h" + +#include "trace/imagemap.h" +#include "trace/potrace/inkscape-potrace.h" + +#include "ui/icon-names.h" +#include "ui/shape-editor.h" +#include "ui/widget/canvas.h" // Canvas area + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +using Inkscape::Display::ExtractARGB32; +using Inkscape::Display::ExtractRGB32; +using Inkscape::Display::AssembleARGB32; + +namespace Inkscape { +namespace UI { +namespace Tools { + +// TODO: Replace by C++11 initialization +// Must match PaintBucketChannels enum +Glib::ustring ch_init[8] = { + _("Visible Colors"), + _("Red"), + _("Green"), + _("Blue"), + _("Hue"), + _("Saturation"), + _("Lightness"), + _("Alpha"), +}; +const std::vector<Glib::ustring> FloodTool::channel_list( ch_init, ch_init+8 ); + +Glib::ustring gap_init[4] = { + NC_("Flood autogap", "None"), + NC_("Flood autogap", "Small"), + NC_("Flood autogap", "Medium"), + NC_("Flood autogap", "Large") +}; +const std::vector<Glib::ustring> FloodTool::gap_list( gap_init, gap_init+4 ); + +FloodTool::FloodTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/paintbucket", "flood.svg") + , item(nullptr) +{ + // TODO: Why does the flood tool use a hardcoded tolerance instead of a pref? + this->tolerance = 4; + + this->shape_editor = new ShapeEditor(desktop); + + SPItem *item = desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = desktop->getSelection()->connectChanged( + sigc::mem_fun(this, &FloodTool::selection_changed) + ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/paintbucket/selcue")) { + this->enableSelectionCue(); + } +} + +FloodTool::~FloodTool() { + this->sel_changed_connection.disconnect(); + + delete shape_editor; + shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->item) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + */ +void FloodTool::selection_changed(Inkscape::Selection* selection) { + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); +} + +// Changes from 0.48 -> 0.49 (Cairo) +// 0.49: Ignores alpha in background +// 0.48: RGBA, 0.49 ARGB +// 0.49: premultiplied alpha +inline static guint32 compose_onto(guint32 px, guint32 bg) +{ + guint ap = 0, rp = 0, gp = 0, bp = 0; + guint rb = 0, gb = 0, bb = 0; + ExtractARGB32(px, ap, rp, gp, bp); + ExtractRGB32(bg, rb, gb, bb); + + // guint ao = 255*255 - (255-ap)*(255-bp); ao = (ao + 127) / 255; + // guint ao = (255-ap)*ab + 255*ap; ao = (ao + 127) / 255; + guint ao = 255; // Cairo version doesn't allow background to have alpha != 1. + guint ro = (255-ap)*rb + 255*rp; ro = (ro + 127) / 255; + guint go = (255-ap)*gb + 255*gp; go = (go + 127) / 255; + guint bo = (255-ap)*bb + 255*bp; bo = (bo + 127) / 255; + + guint pxout = AssembleARGB32(ao, ro, go, bo); + return pxout; +} + +/** + * Get the pointer to a pixel in a pixel buffer. + * @param px The pixel buffer. + * @param x The X coordinate. + * @param y The Y coordinate. + * @param stride The rowstride of the pixel buffer. + */ +inline guint32 get_pixel(guchar *px, int x, int y, int stride) { + return *reinterpret_cast<guint32*>(px + y * stride + x * 4); +} + +inline unsigned char * get_trace_pixel(guchar *trace_px, int x, int y, int width) { + return trace_px + (x + y * width); +} + +/** + * \brief Check whether two unsigned integers are close to each other + * + * \param[in] a The 1st unsigned int + * \param[in] b The 2nd unsigned int + * \param[in] d The threshold for comparison + * + * \return true if |a-b| <= d; false otherwise + */ +static bool compare_guint32(guint32 const a, guint32 const b, guint32 const d) +{ + const int difference = std::abs(static_cast<int>(a) - static_cast<int>(b)); + return difference <= d; +} + +/** + * Compare a pixel in a pixel buffer with another pixel to determine if a point should be included in the fill operation. + * @param check The pixel in the pixel buffer to check. + * @param orig The original selected pixel to use as the fill target color. + * @param merged_orig_pixel The original pixel merged with the background. + * @param dtc The desktop background color. + * @param threshold The fill threshold. + * @param method The fill method to use as defined in PaintBucketChannels. + */ +static bool compare_pixels(guint32 check, guint32 orig, guint32 merged_orig_pixel, guint32 dtc, int threshold, PaintBucketChannels method) +{ + float hsl_check[3] = {0,0,0}, hsl_orig[3] = {0,0,0}; + + guint32 ac = 0, rc = 0, gc = 0, bc = 0; + ExtractARGB32(check, ac, rc, gc, bc); + + guint32 ao = 0, ro = 0, go = 0, bo = 0; + ExtractARGB32(orig, ao, ro, go, bo); + + guint32 ad = 0, rd = 0, gd = 0, bd = 0; + ExtractARGB32(dtc, ad, rd, gd, bd); + + guint32 amop = 0, rmop = 0, gmop = 0, bmop = 0; + ExtractARGB32(merged_orig_pixel, amop, rmop, gmop, bmop); + + if ((method == FLOOD_CHANNELS_H) || + (method == FLOOD_CHANNELS_S) || + (method == FLOOD_CHANNELS_L)) { + double dac = ac; + double dao = ao; + SPColor::rgb_to_hsl_floatv(hsl_check, rc / dac, gc / dac, bc / dac); + SPColor::rgb_to_hsl_floatv(hsl_orig, ro / dao, go / dao, bo / dao); + } + + switch (method) { + case FLOOD_CHANNELS_ALPHA: + return compare_guint32(ac, ao, threshold); + case FLOOD_CHANNELS_R: + return compare_guint32(ac ? unpremul_alpha(rc, ac) : 0, + ao ? unpremul_alpha(ro, ao) : 0, + threshold); + case FLOOD_CHANNELS_G: + return compare_guint32(ac ? unpremul_alpha(gc, ac) : 0, + ao ? unpremul_alpha(go, ao) : 0, + threshold); + case FLOOD_CHANNELS_B: + return compare_guint32(ac ? unpremul_alpha(bc, ac) : 0, + ao ? unpremul_alpha(bo, ao) : 0, + threshold); + case FLOOD_CHANNELS_RGB: + { + guint32 amc, rmc, bmc, gmc; + //amc = 255*255 - (255-ac)*(255-ad); amc = (amc + 127) / 255; + //amc = (255-ac)*ad + 255*ac; amc = (amc + 127) / 255; + amc = 255; // Why are we looking at desktop? Cairo version ignores destop alpha + rmc = (255-ac)*rd + 255*rc; rmc = (rmc + 127) / 255; + gmc = (255-ac)*gd + 255*gc; gmc = (gmc + 127) / 255; + bmc = (255-ac)*bd + 255*bc; bmc = (bmc + 127) / 255; + + int diff = 0; // The total difference between each of the 3 color components + diff += std::abs(static_cast<int>(amc ? unpremul_alpha(rmc, amc) : 0) - static_cast<int>(amop ? unpremul_alpha(rmop, amop) : 0)); + diff += std::abs(static_cast<int>(amc ? unpremul_alpha(gmc, amc) : 0) - static_cast<int>(amop ? unpremul_alpha(gmop, amop) : 0)); + diff += std::abs(static_cast<int>(amc ? unpremul_alpha(bmc, amc) : 0) - static_cast<int>(amop ? unpremul_alpha(bmop, amop) : 0)); + return ((diff / 3) <= ((threshold * 3) / 4)); + } + case FLOOD_CHANNELS_H: + return ((int)(fabs(hsl_check[0] - hsl_orig[0]) * 100.0) <= threshold); + case FLOOD_CHANNELS_S: + return ((int)(fabs(hsl_check[1] - hsl_orig[1]) * 100.0) <= threshold); + case FLOOD_CHANNELS_L: + return ((int)(fabs(hsl_check[2] - hsl_orig[2]) * 100.0) <= threshold); + } + + return false; +} + +enum { + PIXEL_CHECKED = 1, + PIXEL_QUEUED = 2, + PIXEL_PAINTABLE = 4, + PIXEL_NOT_PAINTABLE = 8, + PIXEL_COLORED = 16 +}; + +static inline bool is_pixel_checked(unsigned char *t) { return (*t & PIXEL_CHECKED) == PIXEL_CHECKED; } +static inline bool is_pixel_queued(unsigned char *t) { return (*t & PIXEL_QUEUED) == PIXEL_QUEUED; } +static inline bool is_pixel_paintability_checked(unsigned char *t) { + return !((*t & PIXEL_PAINTABLE) == 0) && ((*t & PIXEL_NOT_PAINTABLE) == 0); +} +static inline bool is_pixel_paintable(unsigned char *t) { return (*t & PIXEL_PAINTABLE) == PIXEL_PAINTABLE; } +static inline bool is_pixel_colored(unsigned char *t) { return (*t & PIXEL_COLORED) == PIXEL_COLORED; } + +static inline void mark_pixel_checked(unsigned char *t) { *t |= PIXEL_CHECKED; } +static inline void mark_pixel_queued(unsigned char *t) { *t |= PIXEL_QUEUED; } +static inline void mark_pixel_paintable(unsigned char *t) { *t |= PIXEL_PAINTABLE; *t ^= PIXEL_NOT_PAINTABLE; } +static inline void mark_pixel_not_paintable(unsigned char *t) { *t |= PIXEL_NOT_PAINTABLE; *t ^= PIXEL_PAINTABLE; } +static inline void mark_pixel_colored(unsigned char *t) { *t |= PIXEL_COLORED; } + +static inline void clear_pixel_paintability(unsigned char *t) { *t ^= PIXEL_PAINTABLE; *t ^= PIXEL_NOT_PAINTABLE; } + +struct bitmap_coords_info { + bool is_left; + unsigned int x; + unsigned int y; + int y_limit; + unsigned int width; + unsigned int height; + unsigned int stride; + unsigned int threshold; + unsigned int radius; + PaintBucketChannels method; + guint32 dtc; + guint32 merged_orig_pixel; + Geom::Rect bbox; + Geom::Rect screen; + unsigned int max_queue_size; + unsigned int current_step; +}; + +/** + * Check if a pixel can be included in the fill. + * @param px The rendered pixel buffer to check. + * @param trace_t The pixel in the trace pixel buffer to check or mark. + * @param x The X coordinate. + * @param y The y coordinate. + * @param orig_color The original selected pixel to use as the fill target color. + * @param bci The bitmap_coords_info structure. + */ +inline static bool check_if_pixel_is_paintable(guchar *px, unsigned char *trace_t, int x, int y, guint32 orig_color, bitmap_coords_info bci) { + if (is_pixel_paintability_checked(trace_t)) { + return is_pixel_paintable(trace_t); + } else { + guint32 pixel = get_pixel(px, x, y, bci.stride); + if (compare_pixels(pixel, orig_color, bci.merged_orig_pixel, bci.dtc, bci.threshold, bci.method)) { + mark_pixel_paintable(trace_t); + return true; + } else { + mark_pixel_not_paintable(trace_t); + return false; + } + } +} + +/** + * Perform the bitmap-to-vector tracing and place the traced path onto the document. + * @param px The trace pixel buffer to trace to SVG. + * @param desktop The desktop on which to place the final SVG path. + * @param transform The transform to apply to the final SVG path. + * @param union_with_selection If true, merge the final SVG path with the current selection. + */ +static void do_trace(bitmap_coords_info bci, guchar *trace_px, SPDesktop *desktop, Geom::Affine transform, unsigned int min_x, unsigned int max_x, unsigned int min_y, unsigned int max_y, bool union_with_selection) { + SPDocument *document = desktop->getDocument(); + + unsigned char *trace_t; + + GrayMap *gray_map = GrayMapCreate((max_x - min_x + 1), (max_y - min_y + 1)); + if (!gray_map) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Failed mid-operation, no objects created.")); + return; + } + unsigned int gray_map_y = 0; + for (unsigned int y = min_y; y <= max_y; y++) { + unsigned long *gray_map_t = gray_map->rows[gray_map_y]; + + trace_t = get_trace_pixel(trace_px, min_x, y, bci.width); + for (unsigned int x = min_x; x <= max_x; x++) { + *gray_map_t = is_pixel_colored(trace_t) ? GRAYMAP_BLACK : GRAYMAP_WHITE; + gray_map_t++; + trace_t++; + } + gray_map_y++; + } + + Inkscape::Trace::Potrace::PotraceTracingEngine pte; + pte.keepGoing = 1; + std::vector<Inkscape::Trace::TracingEngineResult> results = pte.traceGrayMap(gray_map); + gray_map->destroy(gray_map); + + //XML Tree being used here directly while it shouldn't be...." + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + + long totalNodeCount = 0L; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double offset = prefs->getDouble("/tools/paintbucket/offset", 0.0); + + for (auto result : results) { + totalNodeCount += result.getNodeCount(); + + Inkscape::XML::Node *pathRepr = xml_doc->createElement("svg:path"); + /* Set style */ + sp_desktop_apply_style_tool (desktop, pathRepr, "/tools/paintbucket", false); + + Geom::PathVector pathv = sp_svg_read_pathv(result.getPathData().c_str()); + Path *path = new Path; + path->LoadPathVector(pathv); + + if (offset != 0) { + + Shape *path_shape = new Shape(); + + path->ConvertWithBackData(0.03); + path->Fill(path_shape, 0); + delete path; + + Shape *expanded_path_shape = new Shape(); + + expanded_path_shape->ConvertToShape(path_shape, fill_nonZero); + path_shape->MakeOffset(expanded_path_shape, offset * desktop->current_zoom(), join_round, 4); + expanded_path_shape->ConvertToShape(path_shape, fill_positive); + + Path *expanded_path = new Path(); + + expanded_path->Reset(); + expanded_path_shape->ConvertToForme(expanded_path); + expanded_path->ConvertEvenLines(1.0); + expanded_path->Simplify(1.0); + + delete path_shape; + delete expanded_path_shape; + + gchar *str = expanded_path->svg_dump_path(); + if (str && *str) { + pathRepr->setAttribute("d", str); + g_free(str); + } else { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Too much inset</b>, the result is empty.")); + Inkscape::GC::release(pathRepr); + g_free(str); + return; + } + + delete expanded_path; + + } else { + gchar *str = path->svg_dump_path(); + delete path; + pathRepr->setAttribute("d", str); + g_free(str); + } + + auto layer = desktop->layerManager().currentLayer(); + layer->addChild(pathRepr, nullptr); + + SPObject *reprobj = document->getObjectByRepr(pathRepr); + if (reprobj) { + SP_ITEM(reprobj)->doWriteTransform(transform); + + // premultiply the item transform by the accumulated parent transform in the paste layer + Geom::Affine local (layer->i2doc_affine()); + if (!local.isIdentity()) { + gchar const *t_str = pathRepr->attribute("transform"); + Geom::Affine item_t (Geom::identity()); + if (t_str) + sp_svg_transform_read(t_str, &item_t); + item_t *= local.inverse(); + // (we're dealing with unattached repr, so we write to its attr instead of using sp_item_set_transform) + pathRepr->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(item_t)); + } + + Inkscape::Selection *selection = desktop->getSelection(); + + pathRepr->setPosition(-1); + + if (union_with_selection) { + desktop->messageStack()->flashF( Inkscape::WARNING_MESSAGE, + ngettext("Area filled, path with <b>%d</b> node created and unioned with selection.","Area filled, path with <b>%d</b> nodes created and unioned with selection.", + SP_PATH(reprobj)->nodesInPath()), SP_PATH(reprobj)->nodesInPath() ); + selection->add(reprobj); + selection->pathUnion(true); + } else { + desktop->messageStack()->flashF( Inkscape::WARNING_MESSAGE, + ngettext("Area filled, path with <b>%d</b> node created.","Area filled, path with <b>%d</b> nodes created.", + SP_PATH(reprobj)->nodesInPath()), SP_PATH(reprobj)->nodesInPath() ); + selection->set(reprobj); + } + + } + + Inkscape::GC::release(pathRepr); + + } +} + +/** + * The possible return states of perform_bitmap_scanline_check(). + */ +enum ScanlineCheckResult { + SCANLINE_CHECK_OK, + SCANLINE_CHECK_ABORTED, + SCANLINE_CHECK_BOUNDARY +}; + +/** + * Determine if the provided coordinates are within the pixel buffer limits. + * @param x The X coordinate. + * @param y The Y coordinate. + * @param bci The bitmap_coords_info structure. + */ +inline static bool coords_in_range(unsigned int x, unsigned int y, bitmap_coords_info bci) { + return (x < bci.width) && + (y < bci.height); +} + +#define PAINT_DIRECTION_LEFT 1 +#define PAINT_DIRECTION_RIGHT 2 +#define PAINT_DIRECTION_UP 4 +#define PAINT_DIRECTION_DOWN 8 +#define PAINT_DIRECTION_ALL 15 + +/** + * Paint a pixel or a square (if autogap is enabled) on the trace pixel buffer. + * @param px The rendered pixel buffer to check. + * @param trace_px The trace pixel buffer. + * @param orig_color The original selected pixel to use as the fill target color. + * @param bci The bitmap_coords_info structure. + * @param original_point_trace_t The original pixel in the trace pixel buffer to check. + */ +inline static unsigned int paint_pixel(guchar *px, guchar *trace_px, guint32 orig_color, bitmap_coords_info bci, unsigned char *original_point_trace_t) { + if (bci.radius == 0) { + mark_pixel_colored(original_point_trace_t); + return PAINT_DIRECTION_ALL; + } else { + unsigned char *trace_t; + + bool can_paint_up = true; + bool can_paint_down = true; + bool can_paint_left = true; + bool can_paint_right = true; + + for (unsigned int ty = bci.y - bci.radius; ty <= bci.y + bci.radius; ty++) { + for (unsigned int tx = bci.x - bci.radius; tx <= bci.x + bci.radius; tx++) { + if (coords_in_range(tx, ty, bci)) { + trace_t = get_trace_pixel(trace_px, tx, ty, bci.width); + if (!is_pixel_colored(trace_t)) { + if (check_if_pixel_is_paintable(px, trace_t, tx, ty, orig_color, bci)) { + mark_pixel_colored(trace_t); + } else { + if (tx < bci.x) { can_paint_left = false; } + if (tx > bci.x) { can_paint_right = false; } + if (ty < bci.y) { can_paint_up = false; } + if (ty > bci.y) { can_paint_down = false; } + } + } + } + } + } + + unsigned int paint_directions = 0; + if (can_paint_left) { paint_directions += PAINT_DIRECTION_LEFT; } + if (can_paint_right) { paint_directions += PAINT_DIRECTION_RIGHT; } + if (can_paint_up) { paint_directions += PAINT_DIRECTION_UP; } + if (can_paint_down) { paint_directions += PAINT_DIRECTION_DOWN; } + + return paint_directions; + } +} + +/** + * Push a point to be checked onto the bottom of the rendered pixel buffer check queue. + * @param fill_queue The fill queue to add the point to. + * @param max_queue_size The maximum size of the fill queue. + * @param trace_t The trace pixel buffer pixel. + * @param x The X coordinate. + * @param y The Y coordinate. + */ +static void push_point_onto_queue(std::deque<Geom::Point> *fill_queue, unsigned int max_queue_size, unsigned char *trace_t, unsigned int x, unsigned int y) { + if (!is_pixel_queued(trace_t)) { + if ((fill_queue->size() < max_queue_size)) { + fill_queue->push_back(Geom::Point(x, y)); + mark_pixel_queued(trace_t); + } + } +} + +/** + * Shift a point to be checked onto the top of the rendered pixel buffer check queue. + * @param fill_queue The fill queue to add the point to. + * @param max_queue_size The maximum size of the fill queue. + * @param trace_t The trace pixel buffer pixel. + * @param x The X coordinate. + * @param y The Y coordinate. + */ +static void shift_point_onto_queue(std::deque<Geom::Point> *fill_queue, unsigned int max_queue_size, unsigned char *trace_t, unsigned int x, unsigned int y) { + if (!is_pixel_queued(trace_t)) { + if ((fill_queue->size() < max_queue_size)) { + fill_queue->push_front(Geom::Point(x, y)); + mark_pixel_queued(trace_t); + } + } +} + +/** + * Scan a row in the rendered pixel buffer and add points to the fill queue as necessary. + * @param fill_queue The fill queue to add the point to. + * @param px The rendered pixel buffer. + * @param trace_px The trace pixel buffer. + * @param orig_color The original selected pixel to use as the fill target color. + * @param bci The bitmap_coords_info structure. + */ +static ScanlineCheckResult perform_bitmap_scanline_check(std::deque<Geom::Point> *fill_queue, guchar *px, guchar *trace_px, guint32 orig_color, bitmap_coords_info bci, unsigned int *min_x, unsigned int *max_x) { + bool aborted = false; + bool reached_screen_boundary = false; + bool ok; + + bool keep_tracing; + bool initial_paint = true; + + unsigned char *current_trace_t = get_trace_pixel(trace_px, bci.x, bci.y, bci.width); + unsigned int paint_directions; + + bool currently_painting_top = false; + bool currently_painting_bottom = false; + + unsigned int top_ty = (bci.y > 0) ? bci.y - 1 : 0; + unsigned int bottom_ty = bci.y + 1; + + bool can_paint_top = (top_ty > 0); + bool can_paint_bottom = (bottom_ty < bci.height); + + Geom::Point front_of_queue = fill_queue->empty() ? Geom::Point() : fill_queue->front(); + + do { + ok = false; + if (bci.is_left) { + keep_tracing = (bci.x != 0); + } else { + keep_tracing = (bci.x < bci.width); + } + + *min_x = MIN(*min_x, bci.x); + *max_x = MAX(*max_x, bci.x); + + if (keep_tracing) { + if (check_if_pixel_is_paintable(px, current_trace_t, bci.x, bci.y, orig_color, bci)) { + paint_directions = paint_pixel(px, trace_px, orig_color, bci, current_trace_t); + if (bci.radius == 0) { + mark_pixel_checked(current_trace_t); + if ((!fill_queue->empty()) && + (front_of_queue[Geom::X] == bci.x) && + (front_of_queue[Geom::Y] == bci.y)) { + fill_queue->pop_front(); + front_of_queue = fill_queue->empty() ? Geom::Point() : fill_queue->front(); + } + } + + if (can_paint_top) { + if (paint_directions & PAINT_DIRECTION_UP) { + unsigned char *trace_t = current_trace_t - bci.width; + if (!is_pixel_queued(trace_t)) { + bool ok_to_paint = check_if_pixel_is_paintable(px, trace_t, bci.x, top_ty, orig_color, bci); + + if (initial_paint) { currently_painting_top = !ok_to_paint; } + + if (ok_to_paint && (!currently_painting_top)) { + currently_painting_top = true; + push_point_onto_queue(fill_queue, bci.max_queue_size, trace_t, bci.x, top_ty); + } + if ((!ok_to_paint) && currently_painting_top) { + currently_painting_top = false; + } + } + } + } + + if (can_paint_bottom) { + if (paint_directions & PAINT_DIRECTION_DOWN) { + unsigned char *trace_t = current_trace_t + bci.width; + if (!is_pixel_queued(trace_t)) { + bool ok_to_paint = check_if_pixel_is_paintable(px, trace_t, bci.x, bottom_ty, orig_color, bci); + + if (initial_paint) { currently_painting_bottom = !ok_to_paint; } + + if (ok_to_paint && (!currently_painting_bottom)) { + currently_painting_bottom = true; + push_point_onto_queue(fill_queue, bci.max_queue_size, trace_t, bci.x, bottom_ty); + } + if ((!ok_to_paint) && currently_painting_bottom) { + currently_painting_bottom = false; + } + } + } + } + + if (bci.is_left) { + if (paint_directions & PAINT_DIRECTION_LEFT) { + bci.x--; current_trace_t--; + ok = true; + } + } else { + if (paint_directions & PAINT_DIRECTION_RIGHT) { + bci.x++; current_trace_t++; + ok = true; + } + } + + initial_paint = false; + } + } else { + if (bci.bbox.min()[Geom::X] > bci.screen.min()[Geom::X]) { + aborted = true; break; + } else { + reached_screen_boundary = true; + } + } + } while (ok); + + if (aborted) { return SCANLINE_CHECK_ABORTED; } + if (reached_screen_boundary) { return SCANLINE_CHECK_BOUNDARY; } + return SCANLINE_CHECK_OK; +} + +/** + * Sort the rendered pixel buffer check queue vertically. + */ +static bool sort_fill_queue_vertical(Geom::Point a, Geom::Point b) { + return a[Geom::Y] > b[Geom::Y]; +} + +/** + * Sort the rendered pixel buffer check queue horizontally. + */ +static bool sort_fill_queue_horizontal(Geom::Point a, Geom::Point b) { + return a[Geom::X] > b[Geom::X]; +} + +/** + * Perform a flood fill operation. + * @param desktop The desktop of this tool's event context. + * @param event The details of this event. + * @param union_with_selection If true, union the new fill with the current selection. + * @param is_point_fill If false, use the Rubberband "touch selection" to get the initial points for the fill. + * @param is_touch_fill If true, use only the initial contact point in the Rubberband "touch selection" as the fill target color. + */ +static void sp_flood_do_flood_fill(SPDesktop *desktop, GdkEvent *event, + bool union_with_selection, bool is_point_fill, bool is_touch_fill) { + + SPDocument *document = desktop->getDocument(); + + document->ensureUpToDate(); + + Geom::OptRect bbox = document->getRoot()->visualBounds(); + + if (!bbox) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Area is not bounded</b>, cannot fill.")); + return; + } + + // Render 160% of the physical display to the render pixel buffer, so that available + // fill areas off the screen can be included in the fill. + double padding = 1.6; + + // image space is world space with an offset + Geom::Rect const screen_world = desktop->getCanvas()->get_area_world(); + Geom::Rect const screen = screen_world * desktop->w2d(); + Geom::IntPoint const img_dims = (screen_world.dimensions() * padding).ceil(); + Geom::Affine const world2img = Geom::Translate((img_dims - screen_world.dimensions()) / 2.0 - screen_world.min()); + Geom::Affine const doc2img = desktop->doc2dt() * desktop->d2w() * world2img; + + auto const width = img_dims.x(); + auto const height = img_dims.y(); + + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + guchar *px = g_new(guchar, stride * height); + guint32 bgcolor, dtc; + + // Draw image into data block px + { // this block limits the lifetime of Drawing and DrawingContext + /* Create DrawingItems and set transform */ + unsigned dkey = SPItem::display_key_new(1); + Inkscape::Drawing drawing; + Inkscape::DrawingItem *root = document->getRoot()->invoke_show( drawing, dkey, SP_ITEM_SHOW_DISPLAY); + root->setTransform(doc2img); + drawing.setRoot(root); + + Geom::IntRect final_bbox = Geom::IntRect::from_xywh(0, 0, width, height); + drawing.update(final_bbox); + + cairo_surface_t *s = cairo_image_surface_create_for_data( + px, CAIRO_FORMAT_ARGB32, width, height, stride); + Inkscape::DrawingContext dc(s, Geom::Point(0,0)); + // cairo_translate not necessary here - surface origin is at 0,0 + + bgcolor = document->getPageManager().background_color; + + // bgcolor is 0xrrggbbaa, we need 0xaarrggbb + dtc = (bgcolor >> 8) | (bgcolor << 24); + + dc.setSource(bgcolor); + dc.setOperator(CAIRO_OPERATOR_SOURCE); + dc.paint(); + dc.setOperator(CAIRO_OPERATOR_OVER); + + drawing.render(dc, final_bbox); + + //cairo_surface_write_to_png( s, "cairo.png" ); + + cairo_surface_flush(s); + cairo_surface_destroy(s); + + // Hide items + document->getRoot()->invoke_hide(dkey); + } + + // { + // // Dump data to png + // cairo_surface_t *s = cairo_image_surface_create_for_data( + // px, CAIRO_FORMAT_ARGB32, width, height, stride); + // cairo_surface_write_to_png( s, "cairo2.png" ); + // std::cout << " Wrote cairo2.png" << std::endl; + // } + + guchar *trace_px = g_new(guchar, width * height); + memset(trace_px, 0x00, width * height); + + std::deque<Geom::Point> fill_queue; + std::queue<Geom::Point> color_queue; + + std::vector<Geom::Point> fill_points; + + bool aborted = false; + int y_limit = height - 1; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + PaintBucketChannels method = (PaintBucketChannels) prefs->getInt("/tools/paintbucket/channels", 0); + int threshold = prefs->getIntLimited("/tools/paintbucket/threshold", 1, 0, 100); + + switch(method) { + case FLOOD_CHANNELS_ALPHA: + case FLOOD_CHANNELS_RGB: + case FLOOD_CHANNELS_R: + case FLOOD_CHANNELS_G: + case FLOOD_CHANNELS_B: + threshold = (255 * threshold) / 100; + break; + case FLOOD_CHANNELS_H: + case FLOOD_CHANNELS_S: + case FLOOD_CHANNELS_L: + break; + } + + bitmap_coords_info bci; + + bci.y_limit = y_limit; + bci.width = width; + bci.height = height; + bci.stride = stride; + bci.threshold = threshold; + bci.method = method; + bci.bbox = *bbox; + bci.screen = screen; + bci.dtc = dtc; + bci.radius = prefs->getIntLimited("/tools/paintbucket/autogap", 0, 0, 3); + bci.max_queue_size = (width * height) / 4; + bci.current_step = 0; + + if (is_point_fill) { + fill_points.emplace_back(event->button.x, event->button.y); + } else { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + fill_points = r->getPoints(); + } + + auto const img_max_indices = Geom::Rect::from_xywh(0, 0, width - 1, height - 1); + + for (unsigned int i = 0; i < fill_points.size(); i++) { + Geom::Point pw = fill_points[i] * world2img; + + pw = img_max_indices.clamp(pw); + + if (is_touch_fill) { + if (i == 0) { + color_queue.push(pw); + } else { + unsigned char *trace_t = get_trace_pixel(trace_px, (int)pw[Geom::X], (int)pw[Geom::Y], width); + push_point_onto_queue(&fill_queue, bci.max_queue_size, trace_t, (int)pw[Geom::X], (int)pw[Geom::Y]); + } + } else { + color_queue.push(pw); + } + } + + bool reached_screen_boundary = false; + + bool first_run = true; + + unsigned long sort_size_threshold = 5; + + unsigned int min_y = height; + unsigned int max_y = 0; + unsigned int min_x = width; + unsigned int max_x = 0; + + while (!color_queue.empty() && !aborted) { + Geom::Point color_point = color_queue.front(); + color_queue.pop(); + + int cx = (int)color_point[Geom::X]; + int cy = (int)color_point[Geom::Y]; + + guint32 orig_color = get_pixel(px, cx, cy, stride); + bci.merged_orig_pixel = compose_onto(orig_color, dtc); + + unsigned char *trace_t = get_trace_pixel(trace_px, cx, cy, width); + if (!is_pixel_checked(trace_t) && !is_pixel_colored(trace_t)) { + if (check_if_pixel_is_paintable(px, trace_px, cx, cy, orig_color, bci)) { + shift_point_onto_queue(&fill_queue, bci.max_queue_size, trace_t, cx, cy); + + if (!first_run) { + for (unsigned int y = 0; y < height; y++) { + trace_t = get_trace_pixel(trace_px, 0, y, width); + for (unsigned int x = 0; x < width; x++) { + clear_pixel_paintability(trace_t); + trace_t++; + } + } + } + first_run = false; + } + } + + unsigned long old_fill_queue_size = fill_queue.size(); + + while (!fill_queue.empty() && !aborted) { + Geom::Point cp = fill_queue.front(); + + if (bci.radius == 0) { + unsigned long new_fill_queue_size = fill_queue.size(); + + /* + * To reduce the number of points in the fill queue, periodically + * resort all of the points in the queue so that scanline checks + * can complete more quickly. A point cannot be checked twice + * in a normal scanline checks, so forcing scanline checks to start + * from one corner of the rendered area as often as possible + * will reduce the number of points that need to be checked and queued. + */ + if (new_fill_queue_size > sort_size_threshold) { + if (new_fill_queue_size > old_fill_queue_size) { + std::sort(fill_queue.begin(), fill_queue.end(), sort_fill_queue_vertical); + + std::deque<Geom::Point>::iterator start_sort = fill_queue.begin(); + std::deque<Geom::Point>::iterator end_sort = fill_queue.begin(); + unsigned int sort_y = (unsigned int)cp[Geom::Y]; + unsigned int current_y; + + for (std::deque<Geom::Point>::iterator i = fill_queue.begin(); i != fill_queue.end(); ++i) { + Geom::Point current = *i; + current_y = (unsigned int)current[Geom::Y]; + if (current_y != sort_y) { + if (start_sort != end_sort) { + std::sort(start_sort, end_sort, sort_fill_queue_horizontal); + } + sort_y = current_y; + start_sort = i; + } + end_sort = i; + } + if (start_sort != end_sort) { + std::sort(start_sort, end_sort, sort_fill_queue_horizontal); + } + + cp = fill_queue.front(); + } + } + + old_fill_queue_size = new_fill_queue_size; + } + + fill_queue.pop_front(); + + int x = (int)cp[Geom::X]; + int y = (int)cp[Geom::Y]; + + min_y = MIN((unsigned int)y, min_y); + max_y = MAX((unsigned int)y, max_y); + + unsigned char *trace_t = get_trace_pixel(trace_px, x, y, width); + if (!is_pixel_checked(trace_t)) { + mark_pixel_checked(trace_t); + + if (y == 0) { + if (bbox->min()[Geom::Y] > screen.min()[Geom::Y]) { + aborted = true; break; + } else { + reached_screen_boundary = true; + } + } + + if (y == y_limit) { + if (bbox->max()[Geom::Y] < screen.max()[Geom::Y]) { + aborted = true; break; + } else { + reached_screen_boundary = true; + } + } + + bci.is_left = true; + bci.x = x; + bci.y = y; + + ScanlineCheckResult result = perform_bitmap_scanline_check(&fill_queue, px, trace_px, orig_color, bci, &min_x, &max_x); + + switch (result) { + case SCANLINE_CHECK_ABORTED: + aborted = true; + break; + case SCANLINE_CHECK_BOUNDARY: + reached_screen_boundary = true; + break; + default: + break; + } + + if (bci.x < width) { + trace_t++; + if (!is_pixel_checked(trace_t) && !is_pixel_queued(trace_t)) { + mark_pixel_checked(trace_t); + bci.is_left = false; + bci.x = x + 1; + + result = perform_bitmap_scanline_check(&fill_queue, px, trace_px, orig_color, bci, &min_x, &max_x); + + switch (result) { + case SCANLINE_CHECK_ABORTED: + aborted = true; + break; + case SCANLINE_CHECK_BOUNDARY: + reached_screen_boundary = true; + break; + default: + break; + } + } + } + } + + bci.current_step++; + + if (bci.current_step > bci.max_queue_size) { + aborted = true; + } + } + } + + g_free(px); + + if (aborted) { + g_free(trace_px); + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Area is not bounded</b>, cannot fill.")); + return; + } + + if (reached_screen_boundary) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Only the visible part of the bounded area was filled.</b> If you want to fill all of the area, undo, zoom out, and fill again.")); + } + + unsigned int trace_padding = bci.radius + 1; + if (min_y > trace_padding) { min_y -= trace_padding; } + if (max_y < (y_limit - trace_padding)) { max_y += trace_padding; } + if (min_x > trace_padding) { min_x -= trace_padding; } + if (max_x < (width - 1 - trace_padding)) { max_x += trace_padding; } + + Geom::Affine inverted_affine = Geom::Translate(min_x, min_y) * doc2img.inverse(); + + do_trace(bci, trace_px, desktop, inverted_affine, min_x, max_x, min_y, max_y, union_with_selection); + + g_free(trace_px); + + DocumentUndo::done(document, _("Fill bounded area"), INKSCAPE_ICON("color-fill")); +} + +bool FloodTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if ((event->button.state & GDK_CONTROL_MASK) && event->button.button == 1) { + Geom::Point const button_w(event->button.x, event->button.y); + + SPItem *item = sp_event_context_find_item(_desktop, button_w, TRUE, TRUE); + + // Set style + _desktop->applyCurrentOrToolStyle(item, "/tools/paintbucket", false); + + DocumentUndo::done(_desktop->getDocument(), _("Set style on object"), INKSCAPE_ICON("color-fill")); + // Dead assignment: Value stored to 'ret' is never read + //ret = TRUE; + } + break; + + default: + break; + } + +// if (((ToolBaseClass *) sp_flood_context_parent_class)->item_handler) { +// ret = ((ToolBaseClass *) sp_flood_context_parent_class)->item_handler(event_context, item, event); +// } + // CPPIFY: ret is overwritten... + ret = ToolBase::item_handler(item, event); + + return ret; +} + +bool FloodTool::root_handler(GdkEvent* event) { + static bool dragging; + + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + if (!(event->button.state & GDK_CONTROL_MASK)) { + Geom::Point const button_w(event->button.x, event->button.y); + + if (Inkscape::have_viable_layer(_desktop, this->defaultMessageContext())) { + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + dragging = true; + + Geom::Point const p(_desktop->w2d(button_w)); + Inkscape::Rubberband::get(_desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH); + Inkscape::Rubberband::get(_desktop)->start(_desktop, p); + } + } + } + + case GDK_MOTION_NOTIFY: + if ( dragging && ( event->motion.state & GDK_BUTTON1_MASK )) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + + this->within_tolerance = false; + + Geom::Point const motion_pt(event->motion.x, event->motion.y); + Geom::Point const p(_desktop->w2d(motion_pt)); + + if (Inkscape::Rubberband::get(_desktop)->is_started()) { + Inkscape::Rubberband::get(_desktop)->move(p); + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw over</b> areas to add to fill, hold <b>Alt</b> for touch fill")); + gobble_motion_events(GDK_BUTTON1_MASK); + } + } + break; + + case GDK_BUTTON_RELEASE: + if (event->button.button == 1) { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop); + + if (r->is_started()) { + dragging = false; + bool is_point_fill = this->within_tolerance; + bool is_touch_fill = event->button.state & GDK_MOD1_MASK; + + // It's possible for the user to sneakily change the tool while the + // Gtk main loop has control, so we save the current desktop address: + SPDesktop* current_desktop = _desktop; + + current_desktop->setWaitingCursor(); + sp_flood_do_flood_fill(current_desktop, event, + event->button.state & GDK_SHIFT_MASK, + is_point_fill, is_touch_fill); + current_desktop->clearWaitingCursor(); + r->stop(); + + // We check whether our object was deleted by SPDesktop::setEventContext() + // TODO: fix SPDesktop so that it doesn't kill us before we're done + ToolBase *current_context = current_desktop->getEventContext(); + + if (current_context == (ToolBase*)this) { // We're still alive + this->defaultMessageContext()->clear(); + } // else just return without dereferencing `this`. + ret = true; + } + } + break; + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // prevent the zoom field from activation + if (!MOD__CTRL_ONLY(event)) + ret = TRUE; + break; + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void FloodTool::finishItem() { + this->message_context->clear(); + + if (this->item != nullptr) { + this->item->updateRepr(); + + _desktop->getSelection()->set(this->item); + DocumentUndo::done(_desktop->getDocument(), _("Fill bounded area"), INKSCAPE_ICON("color-fill")); + + this->item = nullptr; + } +} + +void FloodTool::set_channels(gint channels) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/paintbucket/channels", channels); +} + +} +} +} + +/* + 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/tools/flood-tool.h b/src/ui/tools/flood-tool.h new file mode 100644 index 0000000..290021e --- /dev/null +++ b/src/ui/tools/flood-tool.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_FLOOD_CONTEXT_H__ +#define __SP_FLOOD_CONTEXT_H__ + +/* + * Flood fill drawing context + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * John Bintz <jcoswell@coswellproductions.org> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> + +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" + +#define SP_FLOOD_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::FloodTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_FLOOD_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::FloodTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { + +class Selection; + +namespace UI { +namespace Tools { + +class FloodTool : public ToolBase { +public: + FloodTool(SPDesktop *desktop); + ~FloodTool() override; + + SPItem *item; + + sigc::connection sel_changed_connection; + + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + + static void set_channels(gint channels); + static const std::vector<Glib::ustring> channel_list; + static const std::vector<Glib::ustring> gap_list; + +private: + void selection_changed(Inkscape::Selection* selection); + void finishItem(); +}; + +enum PaintBucketChannels { + FLOOD_CHANNELS_RGB, + FLOOD_CHANNELS_R, + FLOOD_CHANNELS_G, + FLOOD_CHANNELS_B, + FLOOD_CHANNELS_H, + FLOOD_CHANNELS_S, + FLOOD_CHANNELS_L, + FLOOD_CHANNELS_ALPHA +}; + +} +} +} + +#endif diff --git a/src/ui/tools/freehand-base.cpp b/src/ui/tools/freehand-base.cpp new file mode 100644 index 0000000..8b5b56c --- /dev/null +++ b/src/ui/tools/freehand-base.cpp @@ -0,0 +1,1077 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Generic drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2012 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define DRAW_VERBOSE + +#include "freehand-base.h" + +#include "desktop-style.h" +#include "id-clash.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "style.h" + +#include "display/curve.h" +#include "display/control/canvas-item-bpath.h" + +#include "include/macros.h" + +#include "live_effects/lpe-bendpath.h" +#include "live_effects/lpe-patternalongpath.h" +#include "live_effects/lpe-simplify.h" +#include "live_effects/lpe-powerstroke.h" + +#include "object/sp-item-group.h" +#include "object/sp-path.h" +#include "object/sp-rect.h" +#include "object/sp-use.h" + +#include "svg/svg-color.h" +#include "svg/svg.h" + +#include "ui/clipboard.h" +#include "ui/draw-anchor.h" +#include "ui/icon-names.h" +#include "ui/tools/lpe-tool.h" +#include "ui/tools/pen-tool.h" +#include "ui/tools/pencil-tool.h" + +#define MIN_PRESSURE 0.0 +#define MAX_PRESSURE 1.0 +#define DEFAULT_PRESSURE 1.0 + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void spdc_selection_changed(Inkscape::Selection *sel, FreehandBase *dc); +static void spdc_selection_modified(Inkscape::Selection *sel, guint flags, FreehandBase *dc); + +static void spdc_attach_selection(FreehandBase *dc, Inkscape::Selection *sel); + +/** + * Flushes white curve(s) and additional curve into object. + * + * No cleaning of colored curves - this has to be done by caller + * No rereading of white data, so if you cannot rely on ::modified, do it in caller + */ +static void spdc_flush_white(FreehandBase *dc, SPCurve *gc); + +static void spdc_reset_white(FreehandBase *dc); +static void spdc_free_colors(FreehandBase *dc); + +FreehandBase::FreehandBase(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename) + : ToolBase(desktop, prefs_path, cursor_filename) + , selection(nullptr) + , attach(false) + , red_color(0xff00007f) + , blue_color(0x0000ff7f) + , green_color(0x00ff007f) + , highlight_color(0x0000007f) + , red_bpath(nullptr) + , red_curve(nullptr) + , blue_bpath(nullptr) + , blue_curve(nullptr) + , green_curve(nullptr) + , green_anchor(nullptr) + , green_closed(false) + , white_item(nullptr) + , sa_overwrited(nullptr) + , sa(nullptr) + , ea(nullptr) + , waiting_LPE_type(Inkscape::LivePathEffect::INVALID_LPE) + , red_curve_is_valid(false) + , anchor_statusbar(false) + , tablet_enabled(false) + , is_tablet(false) + , pressure(DEFAULT_PRESSURE) +{ + this->selection = desktop->getSelection(); + + // Connect signals to track selection changes + this->sel_changed_connection = this->selection->connectChanged( + sigc::bind(sigc::ptr_fun(&spdc_selection_changed), this) + ); + this->sel_modified_connection = this->selection->connectModified( + sigc::bind(sigc::ptr_fun(&spdc_selection_modified), this) + ); + + // Create red bpath + this->red_bpath = new Inkscape::CanvasItemBpath(desktop->getCanvasSketch()); + this->red_bpath->set_stroke(this->red_color); + this->red_bpath->set_fill(0x0, SP_WIND_RULE_NONZERO); + + // Create red curve + this->red_curve.reset(new SPCurve()); + + // Create blue bpath + this->blue_bpath = new Inkscape::CanvasItemBpath(desktop->getCanvasSketch()); + this->blue_bpath->set_stroke(this->blue_color); + this->blue_bpath->set_fill(0x0, SP_WIND_RULE_NONZERO); + + // Create blue curve + this->blue_curve.reset(new SPCurve()); + + // Create green curve + this->green_curve.reset(new SPCurve()); + + // No green anchor by default + this->green_anchor = nullptr; + this->green_closed = FALSE; + + // Create start anchor alternative curve + this->sa_overwrited.reset(new SPCurve()); + + this->attach = TRUE; + spdc_attach_selection(this, this->selection); +} + +FreehandBase::~FreehandBase() +{ + this->sel_changed_connection.disconnect(); + this->sel_modified_connection.disconnect(); + + ungrabCanvasEvents(); + + if (this->selection) { + this->selection = nullptr; + } + + spdc_free_colors(this); +} + +void FreehandBase::set(const Inkscape::Preferences::Entry& /*value*/) { +} + +bool FreehandBase::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // prevent the zoom field from activation + if (!MOD__CTRL_ONLY(event)) { + ret = TRUE; + } + break; + default: + break; + } + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +std::optional<Geom::Point> FreehandBase::red_curve_get_last_point() +{ + std::optional<Geom::Point> p; + if (!red_curve->is_empty()) { + p = red_curve->last_point(); + } + return p; +} + +static Glib::ustring const tool_name(FreehandBase *dc) +{ + return ( SP_IS_PEN_CONTEXT(dc) + ? "/tools/freehand/pen" + : "/tools/freehand/pencil" ); +} + +static void spdc_paste_curve_as_freehand_shape(Geom::PathVector const &newpath, FreehandBase *dc, SPItem *item) +{ + using namespace Inkscape::LivePathEffect; + + // TODO: Don't paste path if nothing is on the clipboard + SPDocument *document = dc->getDesktop()->doc(); + Effect::createAndApply(PATTERN_ALONG_PATH, document, item); + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + static_cast<LPEPatternAlongPath*>(lpe)->pattern.set_new_value(newpath,true); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double scale = prefs->getDouble("/live_effects/skeletal/width", 1); + if (!scale) { + scale = 1; + } + Inkscape::SVGOStringStream os; + os << scale; + lpe->getRepr()->setAttribute("prop_scale", os.str()); +} + +void spdc_apply_style(SPObject *obj) +{ + SPCSSAttr *css = sp_repr_css_attr_new(); + if (obj->style) { + if (obj->style->stroke.isPaintserver()) { + SPPaintServer *server = obj->style->getStrokePaintServer(); + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property(css, "fill", str.c_str()); + } + } else if (obj->style->stroke.isColor()) { + gchar c[64]; + sp_svg_write_color( + c, sizeof(c), + obj->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(obj->style->stroke_opacity.value))); + sp_repr_css_set_property(css, "fill", c); + } else { + sp_repr_css_set_property(css, "fill", "none"); + } + } else { + sp_repr_css_unset_property(css, "fill"); + } + + sp_repr_css_set_property(css, "fill-rule", "nonzero"); + sp_repr_css_set_property(css, "stroke", "none"); + + sp_desktop_apply_css_recursive(obj, css, true); + sp_repr_css_attr_unref(css); +} +static void spdc_apply_powerstroke_shape(std::vector<Geom::Point> points, FreehandBase *dc, SPItem *item, + gint maxrecursion = 0) +{ + using namespace Inkscape::LivePathEffect; + SPDesktop *desktop = dc->getDesktop(); + SPDocument *document = desktop->getDocument(); + if (!document || !desktop) { + return; + } + if (SP_IS_PENCIL_CONTEXT(dc)) { + if (dc->tablet_enabled) { + SPObject *elemref = nullptr; + if ((elemref = document->getObjectById("power_stroke_preview"))) { + elemref->getRepr()->removeAttribute("style"); + SPItem *successor = dynamic_cast<SPItem *>(elemref); + sp_desktop_apply_style_tool(desktop, successor->getRepr(), + Glib::ustring("/tools/freehand/pencil").data(), false); + spdc_apply_style(successor); + sp_object_ref(item); + item->deleteObject(false); + item->setSuccessor(successor); + sp_object_unref(item); + item = dynamic_cast<SPItem *>(successor); + dc->selection->set(item); + item->setLocked(false); + dc->white_item = item; + rename_id(item, "path-1"); + } + return; + } + } + Effect::createAndApply(POWERSTROKE, document, item); + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + + static_cast<LPEPowerStroke*>(lpe)->offset_points.param_set_and_write_new_value(points); + + // write powerstroke parameters: + lpe->getRepr()->setAttribute("start_linecap_type", "zerowidth"); + lpe->getRepr()->setAttribute("end_linecap_type", "zerowidth"); + lpe->getRepr()->setAttribute("sort_points", "true"); + lpe->getRepr()->setAttribute("not_jump", "false"); + lpe->getRepr()->setAttribute("interpolator_type", "CubicBezierJohan"); + lpe->getRepr()->setAttribute("interpolator_beta", "0.2"); + lpe->getRepr()->setAttribute("miter_limit", "4"); + lpe->getRepr()->setAttribute("scale_width", "1"); + lpe->getRepr()->setAttribute("linejoin_type", "extrp_arc"); +} + +static void spdc_apply_bend_shape(gchar const *svgd, FreehandBase *dc, SPItem *item) +{ + using namespace Inkscape::LivePathEffect; + SPUse *use = dynamic_cast<SPUse *>(item); + if ( use ) { + return; + } + SPDesktop *desktop = dc->getDesktop(); + SPDocument *document = desktop->getDocument(); + if (!document || !desktop) { + return; + } + if (!SP_IS_LPE_ITEM(item)) { + return; + } + if(!SP_LPE_ITEM(item)->hasPathEffectOfType(BEND_PATH)){ + Effect::createAndApply(BEND_PATH, document, item); + } + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + + // write bend parameters: + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double scale = prefs->getDouble("/live_effects/bend_path/width", 1); + if (!scale) { + scale = 1; + } + Inkscape::SVGOStringStream os; + os << scale; + lpe->getRepr()->setAttribute("prop_scale", os.str()); + lpe->getRepr()->setAttribute("scale_y_rel", "false"); + lpe->getRepr()->setAttribute("vertical", "false"); + static_cast<LPEBendPath*>(lpe)->bend_path.paste_param_path(svgd); +} + +static void spdc_apply_simplify(std::string threshold, FreehandBase *dc, SPItem *item) +{ + const SPDesktop *desktop = dc->getDesktop(); + SPDocument *document = desktop->getDocument(); + if (!document || !desktop) { + return; + } + using namespace Inkscape::LivePathEffect; + + Effect::createAndApply(SIMPLIFY, document, item); + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + // write simplify parameters: + lpe->getRepr()->setAttribute("steps", "1"); + lpe->getRepr()->setAttributeOrRemoveIfEmpty("threshold", threshold); + lpe->getRepr()->setAttribute("smooth_angles", "360"); + lpe->getRepr()->setAttribute("helper_size", "0"); + lpe->getRepr()->setAttribute("simplify_individual_paths", "false"); + lpe->getRepr()->setAttribute("simplify_just_coalesce", "false"); +} + +static shapeType previous_shape_type = NONE; + +static void spdc_check_for_and_apply_waiting_LPE(FreehandBase *dc, SPItem *item, SPCurve *curve, bool is_bend) +{ + using namespace Inkscape::LivePathEffect; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + auto *desktop = dc->getDesktop(); + + if (item && SP_IS_LPE_ITEM(item)) { + double defsize = 10 / (0.265 * dc->getDesktop()->getDocument()->getDocumentScale()[0]); +#define SHAPE_LENGTH defsize +#define SHAPE_HEIGHT defsize + //Store the clipboard path to apply in the future without the use of clipboard + static Geom::PathVector previous_shape_pathv; + static SPItem *bend_item = nullptr; + shapeType shape = (shapeType)prefs->getInt(tool_name(dc) + "/shape", 0); + if (previous_shape_type == NONE) { + previous_shape_type = shape; + } + if(shape == LAST_APPLIED){ + shape = previous_shape_type; + if(shape == CLIPBOARD || shape == BEND_CLIPBOARD){ + shape = LAST_APPLIED; + } + } + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + if (is_bend && + (shape == BEND_CLIPBOARD || (shape == LAST_APPLIED && previous_shape_type != CLIPBOARD)) && + cm->paste(desktop, true)) + { + bend_item = dc->selection->singleItem(); + if(!bend_item || (!SP_IS_SHAPE(bend_item) && !SP_IS_GROUP(bend_item))){ + previous_shape_type = NONE; + return; + } + } else if(is_bend) { + return; + } + if (!is_bend && previous_shape_type == BEND_CLIPBOARD && shape == BEND_CLIPBOARD) { + return; + } + bool shape_applied = false; + bool simplify = prefs->getInt(tool_name(dc) + "/simplify", 0); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0); + if(simplify && mode != 2){ + double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0); + tol = tol/(100.0*(102.0-tol)); + std::ostringstream ss; + ss << tol; + spdc_apply_simplify(ss.str(), dc, item); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), true, false); + } + if (prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 1) { + Effect::createAndApply(SPIRO, dc->getDesktop()->getDocument(), item); + } + + if (prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 2) { + Effect::createAndApply(BSPLINE, dc->getDesktop()->getDocument(), item); + } + SPShape *sp_shape = dynamic_cast<SPShape *>(item); + if (sp_shape) { + curve = sp_shape->curve(); + } + auto curveref = curve->ref(); + SPCSSAttr *css_item = sp_css_attr_from_object(item, SP_STYLE_FLAG_ALWAYS); + const char *cstroke = sp_repr_css_property(css_item, "stroke", "none"); + const char *cfill = sp_repr_css_property(css_item, "fill", "none"); + const char *stroke_width = sp_repr_css_property(css_item, "stroke-width", "0"); + double swidth; + sp_svg_number_read_d(stroke_width, &swidth); + swidth = prefs->getDouble("/live_effects/powerstroke/width", SHAPE_HEIGHT / 2); + if (!swidth) { + swidth = swidth/2; + } + swidth = std::abs(swidth); + if (SP_IS_PENCIL_CONTEXT(dc)) { + if (dc->tablet_enabled) { + std::vector<Geom::Point> points; + spdc_apply_powerstroke_shape(points, dc, item); + shape_applied = true; + shape = NONE; + previous_shape_type = NONE; + } + } + + switch (shape) { + case NONE: + // don't apply any shape + break; + case TRIANGLE_IN: + { + // "triangle in" + std::vector<Geom::Point> points(1); + + points[0] = Geom::Point(0., swidth); + //points[0] *= i2anc_affine(static_cast<SPItem *>(item->parent), NULL).inverse(); + spdc_apply_powerstroke_shape(points, dc, item); + + shape_applied = false; + break; + } + case TRIANGLE_OUT: + { + // "triangle out" + guint curve_length = curveref->get_segment_count(); + std::vector<Geom::Point> points(1); + points[0] = Geom::Point(0, swidth); + //points[0] *= i2anc_affine(static_cast<SPItem *>(item->parent), NULL).inverse(); + points[0][Geom::X] = (double)curve_length; + spdc_apply_powerstroke_shape(points, dc, item); + + shape_applied = false; + break; + } + case ELLIPSE: + { + // "ellipse" + auto c = std::make_unique<SPCurve>(); + const double C1 = 0.552; + c->moveto(0, SHAPE_HEIGHT/2); + c->curveto(0, (1 - C1) * SHAPE_HEIGHT/2, (1 - C1) * SHAPE_LENGTH/2, 0, SHAPE_LENGTH/2, 0); + c->curveto((1 + C1) * SHAPE_LENGTH/2, 0, SHAPE_LENGTH, (1 - C1) * SHAPE_HEIGHT/2, SHAPE_LENGTH, SHAPE_HEIGHT/2); + c->curveto(SHAPE_LENGTH, (1 + C1) * SHAPE_HEIGHT/2, (1 + C1) * SHAPE_LENGTH/2, SHAPE_HEIGHT, SHAPE_LENGTH/2, SHAPE_HEIGHT); + c->curveto((1 - C1) * SHAPE_LENGTH/2, SHAPE_HEIGHT, 0, (1 + C1) * SHAPE_HEIGHT/2, 0, SHAPE_HEIGHT/2); + c->closepath(); + spdc_paste_curve_as_freehand_shape(c->get_pathvector(), dc, item); + + shape_applied = true; + break; + } + case CLIPBOARD: + { + // take shape from clipboard; + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + if(cm->paste(desktop,true)){ + SPItem * pasted_clipboard = dc->selection->singleItem(); + dc->selection->toCurves(true); + pasted_clipboard = dc->selection->singleItem(); + if(pasted_clipboard){ + Inkscape::XML::Node *pasted_clipboard_root = pasted_clipboard->getRepr(); + Inkscape::XML::Node *path = sp_repr_lookup_name(pasted_clipboard_root, "svg:path", -1); // unlimited search depth + if ( path != nullptr ) { + gchar const *svgd = path->attribute("d"); + dc->selection->remove(pasted_clipboard); + previous_shape_pathv = sp_svg_read_pathv(svgd); + previous_shape_pathv *= pasted_clipboard->transform; + spdc_paste_curve_as_freehand_shape(previous_shape_pathv, dc, item); + + shape = CLIPBOARD; + shape_applied = true; + pasted_clipboard->deleteObject(); + } else { + shape = NONE; + } + } else { + shape = NONE; + } + } else { + shape = NONE; + } + break; + } + case BEND_CLIPBOARD: + { + gchar const *svgd = item->getRepr()->attribute("d"); + if(bend_item && (SP_IS_SHAPE(bend_item) || SP_IS_GROUP(bend_item))){ + // If item is a SPRect, convert it to path first: + if ( dynamic_cast<SPRect *>(bend_item) ) { + if (desktop) { + Inkscape::Selection *sel = desktop->getSelection(); + if ( sel && !sel->isEmpty() ) { + sel->clear(); + sel->add(bend_item); + sel->toCurves(); + bend_item = sel->singleItem(); + } + } + } + bend_item->moveTo(item,false); + bend_item->transform.setTranslation(Geom::Point()); + spdc_apply_bend_shape(svgd, dc, bend_item); + dc->selection->add(bend_item); + + shape = BEND_CLIPBOARD; + } else { + bend_item = nullptr; + shape = NONE; + } + break; + } + case LAST_APPLIED: + { + if(previous_shape_type == CLIPBOARD){ + if(previous_shape_pathv.size() != 0){ + spdc_paste_curve_as_freehand_shape(previous_shape_pathv, dc, item); + shape_applied = true; + shape = CLIPBOARD; + } else{ + shape = NONE; + } + } else { + if(bend_item != nullptr && bend_item->getRepr() != nullptr){ + gchar const *svgd = item->getRepr()->attribute("d"); + dc->selection->add(bend_item); + dc->selection->duplicate(); + dc->selection->remove(bend_item); + bend_item = dc->selection->singleItem(); + if(bend_item){ + bend_item->moveTo(item,false); + Geom::Coord expansion_X = bend_item->transform.expansionX(); + Geom::Coord expansion_Y = bend_item->transform.expansionY(); + bend_item->transform = Geom::Affine(1,0,0,1,0,0); + bend_item->transform.setExpansionX(expansion_X); + bend_item->transform.setExpansionY(expansion_Y); + spdc_apply_bend_shape(svgd, dc, bend_item); + dc->selection->add(bend_item); + + shape = BEND_CLIPBOARD; + } else { + shape = NONE; + } + } else { + shape = NONE; + } + } + break; + } + default: + break; + } + previous_shape_type = shape; + + if (shape_applied) { + // apply original stroke color as fill and unset stroke; then return + SPCSSAttr *css = sp_repr_css_attr_new(); + if (!strcmp(cfill, "none")) { + sp_repr_css_set_property (css, "fill", cstroke); + } else { + sp_repr_css_set_property (css, "fill", cfill); + } + sp_repr_css_set_property (css, "stroke", "none"); + sp_desktop_apply_css_recursive(dc->white_item, css, true); + sp_repr_css_attr_unref(css); + return; + } + if (dc->waiting_LPE_type != INVALID_LPE) { + Effect::createAndApply(dc->waiting_LPE_type, dc->getDesktop()->getDocument(), item); + dc->waiting_LPE_type = INVALID_LPE; + + if (SP_IS_LPETOOL_CONTEXT(dc)) { + // since a geometric LPE was applied, we switch back to "inactive" mode + lpetool_context_switch_mode(SP_LPETOOL_CONTEXT(dc), INVALID_LPE); + } + } + if (SP_IS_PEN_CONTEXT(dc)) { + SP_PEN_CONTEXT(dc)->setPolylineMode(); + } + } +} + +/* + * Selection handlers + */ + +static void spdc_selection_changed(Inkscape::Selection *sel, FreehandBase *dc) +{ + if (dc->attach) { + spdc_attach_selection(dc, sel); + } +} + +/* fixme: We have to ensure this is not delayed (Lauris) */ + +static void spdc_selection_modified(Inkscape::Selection *sel, guint /*flags*/, FreehandBase *dc) +{ + if (dc->attach) { + spdc_attach_selection(dc, sel); + } +} + +static void spdc_attach_selection(FreehandBase *dc, Inkscape::Selection */*sel*/) +{ + // We reset white and forget white/start/end anchors + spdc_reset_white(dc); + dc->sa = nullptr; + dc->ea = nullptr; + + SPItem *item = dc->selection ? dc->selection->singleItem() : nullptr; + + if ( item && SP_IS_PATH(item) ) { + // Create new white data + // Item + dc->white_item = item; + + // Curve list + // We keep it in desktop coordinates to eliminate calculation errors + auto path = static_cast<SPPath *>(item); + auto norm = SPCurve::copy(path->curveForEdit()); + g_return_if_fail( norm != nullptr ); + norm->transform((dc->white_item)->i2dt_affine()); + dc->white_curves = norm->split(); + + // Anchor list + for (auto const &c_smart_ptr : dc->white_curves) { + auto *c = c_smart_ptr.get(); + g_return_if_fail( c->get_segment_count() > 0 ); + if ( !c->is_closed() ) { + std::unique_ptr<SPDrawAnchor> a = + std::make_unique<SPDrawAnchor>(dc, c, TRUE, *(c->first_point())); + if (a) + dc->white_anchors.push_back(std::move(a)); + a = std::make_unique<SPDrawAnchor>(dc, c, FALSE, *(c->last_point())); + if (a) + dc->white_anchors.push_back(std::move(a)); + } + } + // fixme: recalculate active anchor? + } +} + + +void spdc_endpoint_snap_rotation(ToolBase* const ec, Geom::Point &p, Geom::Point const &o, + guint state) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + unsigned const snaps = abs(prefs->getInt("/options/rotationsnapsperpi/value", 12)); + + SnapManager &m = ec->getDesktop()->namedview->snap_manager; + m.setup(ec->getDesktop()); + + bool snap_enabled = m.snapprefs.getSnapEnabledGlobally(); + if (state & GDK_SHIFT_MASK) { + // SHIFT disables all snapping, except the angular snapping. After all, the user explicitly asked for angular + // snapping by pressing CTRL, otherwise we wouldn't have arrived here. But although we temporarily disable + // the snapping here, we must still call for a constrained snap in order to apply the constraints (i.e. round + // to the nearest angle increment) + m.snapprefs.setSnapEnabledGlobally(false); + } + + Inkscape::SnappedPoint dummy = m.constrainedAngularSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE), std::optional<Geom::Point>(), o, snaps); + p = dummy.getPoint(); + + if (state & GDK_SHIFT_MASK) { + m.snapprefs.setSnapEnabledGlobally(snap_enabled); // restore the original setting + } + + m.unSetup(); +} + + +void spdc_endpoint_snap_free(ToolBase* const ec, Geom::Point& p, std::optional<Geom::Point> &start_of_line, guint const /*state*/) +{ + const SPDesktop *dt = ec->getDesktop(); + SnapManager &m = dt->namedview->snap_manager; + Inkscape::Selection *selection = dt->getSelection(); + + // selection->singleItem() is the item that is currently being drawn. This item will not be snapped to (to avoid self-snapping) + // TODO: Allow snapping to the stationary parts of the item, and only ignore the last segment + + m.setup(dt, true, selection->singleItem()); + Inkscape::SnapCandidatePoint scp(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + if (start_of_line) { + scp.addOrigin(*start_of_line); + } + + Inkscape::SnappedPoint sp = m.freeSnap(scp); + p = sp.getPoint(); + + m.unSetup(); +} + +void spdc_concat_colors_and_flush(FreehandBase *dc, gboolean forceclosed) +{ + // Concat RBG + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Green + auto c = std::make_unique<SPCurve>(); + std::swap(c, dc->green_curve); + for (auto path : dc->green_bpaths) { + delete path; + } + dc->green_bpaths.clear(); + + // Blue + c->append_continuous(*dc->blue_curve); + dc->blue_curve->reset(); + dc->blue_bpath->set_bpath(nullptr); + + // Red + if (dc->red_curve_is_valid) { + c->append_continuous(*(dc->red_curve)); + } + dc->red_curve->reset(); + dc->red_bpath->set_bpath(nullptr); + + if (c->is_empty()) { + return; + } + + // Step A - test, whether we ended on green anchor + if ( (forceclosed && + (!dc->sa || (dc->sa && dc->sa->curve->is_empty()))) || + ( dc->green_anchor && dc->green_anchor->active)) + { + // We hit green anchor, closing Green-Blue-Red + dc->getDesktop()->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed.")); + c->closepath_current(); + // Closed path, just flush + spdc_flush_white(dc, c.get()); + return; + } + + // Step B - both start and end anchored to same curve + if ( dc->sa && dc->ea + && ( dc->sa->curve == dc->ea->curve ) + && ( ( dc->sa != dc->ea ) + || dc->sa->curve->is_closed() ) ) + { + // We hit bot start and end of single curve, closing paths + dc->getDesktop()->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Closing path.")); + dc->sa_overwrited->append_continuous(*c); + dc->sa_overwrited->closepath_current(); + if (!dc->white_curves.empty()) { + dc->white_curves.erase(std::find(dc->white_curves.begin(),dc->white_curves.end(), dc->sa->curve)); + } + dc->white_curves.push_back(std::move(dc->sa_overwrited)); + spdc_flush_white(dc, nullptr); + return; + } + // Step C - test start + if (dc->sa) { + if (!dc->white_curves.empty()) { + dc->white_curves.erase(std::find(dc->white_curves.begin(),dc->white_curves.end(), dc->sa->curve)); + } + dc->sa_overwrited->append_continuous(*c); + c = std::move(dc->sa_overwrited); + } else /* Step D - test end */ if (dc->ea) { + auto e = std::move(dc->ea->curve); + if (!dc->white_curves.empty()) { + dc->white_curves.erase(std::find(dc->white_curves.begin(),dc->white_curves.end(), e)); + } + if (!dc->ea->start) { + e = e->create_reverse(); + } + if(prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 1 || + prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 2){ + e = e->create_reverse(); + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*e->last_segment()); + if(cubic){ + auto lastSeg = std::make_unique<SPCurve>(); + lastSeg->moveto((*cubic)[0]); + lastSeg->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]); + if( e->get_segment_count() == 1){ + e = std::move(lastSeg); + }else{ + //we eliminate the last segment + e->backspace(); + //and we add it again with the recreation + e->append_continuous(*lastSeg); + } + } + e = e->create_reverse(); + } + c->append_continuous(*e); + } + if (forceclosed) + { + dc->getDesktop()->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed.")); + c->closepath_current(); + } + spdc_flush_white(dc, c.get()); +} + +static void spdc_flush_white(FreehandBase *dc, SPCurve *gc) +{ + std::unique_ptr<SPCurve> c; + + if (! dc->white_curves.empty()) { + g_assert(dc->white_item); + + // c = concat(white_curves) + c = std::make_unique<SPCurve>(); + for (auto const &wc : dc->white_curves) { + c->append(*wc); + } + + dc->white_curves.clear(); + if (gc) { + c->append(*gc); + } + } else if (gc) { + c = gc->ref(); + } else { + return; + } + + SPDesktop *desktop = dc->getDesktop(); + SPDocument *doc = desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + // Now we have to go back to item coordinates at last + c->transform( dc->white_item + ? (dc->white_item)->dt2i_affine() + : desktop->dt2doc() ); + + if ( c && !c->is_empty() ) { + // We actually have something to write + + bool has_lpe = false; + Inkscape::XML::Node *repr; + + if (dc->white_item) { + repr = dc->white_item->getRepr(); + has_lpe = SP_LPE_ITEM(dc->white_item)->hasPathEffectRecursive(); + } else { + repr = xml_doc->createElement("svg:path"); + // Set style + sp_desktop_apply_style_tool(desktop, repr, tool_name(dc).data(), false); + } + + auto str = sp_svg_write_path(c->get_pathvector()); + if (has_lpe) + repr->setAttribute("inkscape:original-d", str); + else + repr->setAttribute("d", str); + + auto layer = dc->currentLayer(); + if (SP_IS_PENCIL_CONTEXT(dc) && dc->tablet_enabled) { + if (!dc->white_item) { + dc->white_item = SP_ITEM(layer->appendChildRepr(repr)); + } + spdc_check_for_and_apply_waiting_LPE(dc, dc->white_item, c.get(), false); + } + if (!dc->white_item) { + // Attach repr + SPItem *item = dynamic_cast<SPItem *>(layer->appendChildRepr(repr)); + dc->white_item = item; + //Bend needs the transforms applied after, Other effects best before + spdc_check_for_and_apply_waiting_LPE(dc, item, c.get(), true); + Inkscape::GC::release(repr); + item->transform = layer->i2doc_affine().inverse(); + item->updateRepr(); + item->doWriteTransform(item->transform, nullptr, true); + spdc_check_for_and_apply_waiting_LPE(dc, item, c.get(), false); + if(previous_shape_type == BEND_CLIPBOARD){ + repr->parent()->removeChild(repr); + dc->white_item = nullptr; + } else { + dc->selection->set(repr); + } + } + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(dc->white_item); + if (lpeitem && lpeitem->hasPathEffectRecursive()) { + sp_lpe_item_update_patheffect(lpeitem, true, false); + } + DocumentUndo::done(doc, _("Draw path"), SP_IS_PEN_CONTEXT(dc)? INKSCAPE_ICON("draw-path") : INKSCAPE_ICON("draw-freehand")); + + // When quickly drawing several subpaths with Shift, the next subpath may be finished and + // flushed before the selection_modified signal is fired by the previous change, which + // results in the tool losing all of the selected path's curve except that last subpath. To + // fix this, we force the selection_modified callback now, to make sure the tool's curve is + // in sync immediately. + spdc_selection_modified(desktop->getSelection(), 0, dc); + } + + // Flush pending updates + doc->ensureUpToDate(); +} + +SPDrawAnchor *spdc_test_inside(FreehandBase *dc, Geom::Point p) +{ + SPDrawAnchor *active = nullptr; + + // Test green anchor + if (dc->green_anchor) { + active = dc->green_anchor->anchorTest(p, TRUE); + } + + for (auto& i:dc->white_anchors) { + SPDrawAnchor *na = i->anchorTest(p, !active); + if ( !active && na ) { + active = na; + } + } + return active; +} + +static void spdc_reset_white(FreehandBase *dc) +{ + if (dc->white_item) { + // We do not hold refcount + dc->white_item = nullptr; + } + dc->white_curves.clear(); + dc->white_anchors.clear(); +} + +static void spdc_free_colors(FreehandBase *dc) +{ + // Red + if (dc->red_bpath) { + delete dc->red_bpath; + dc->red_bpath = nullptr; + } + dc->red_curve.reset(); + + // Blue + if (dc->blue_bpath) { + delete dc->blue_bpath; + dc->blue_bpath = nullptr; + } + dc->blue_curve.reset(); + + // Overwrite start anchor curve + dc->sa_overwrited.reset(); + // Green + for (auto path : dc->green_bpaths) { + delete path; + } + dc->green_bpaths.clear(); + dc->green_curve.reset(); + dc->green_anchor.reset(); + + // White + if (dc->white_item) { + // We do not hold refcount + dc->white_item = nullptr; + } + dc->white_curves.clear(); + dc->white_anchors.clear(); +} + +void spdc_create_single_dot(ToolBase *ec, Geom::Point const &pt, char const *tool, guint event_state) { + g_return_if_fail(!strcmp(tool, "/tools/freehand/pen") || !strcmp(tool, "/tools/freehand/pencil") + || !strcmp(tool, "/tools/calligraphic") ); + Glib::ustring tool_path = tool; + + SPDesktop *desktop = ec->getDesktop(); + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "arc"); + auto layer = ec->currentLayer(); + SPItem *item = SP_ITEM(layer->appendChildRepr(repr)); + item->transform = layer->i2doc_affine().inverse(); + Inkscape::GC::release(repr); + + // apply the tool's current style + sp_desktop_apply_style_tool(desktop, repr, tool, false); + + // find out stroke width (TODO: is there an easier way??) + double stroke_width = 3.0; + gchar const *style_str = repr->attribute("style"); + if (style_str) { + SPStyle style(desktop->doc()); + style.mergeString(style_str); + stroke_width = style.stroke_width.computed; + } + + // unset stroke and set fill color to former stroke color + gchar * str; + str = strcmp(tool, "/tools/calligraphic") ? g_strdup_printf("fill:#%06x;stroke:none;", sp_desktop_get_color_tool(desktop, tool, false) >> 8) + : g_strdup_printf("fill:#%06x;stroke:#%06x;", sp_desktop_get_color_tool(desktop, tool, true) >> 8, sp_desktop_get_color_tool(desktop, tool, false) >> 8); + repr->setAttribute("style", str); + g_free(str); + + // put the circle where the mouse click occurred and set the diameter to the + // current stroke width, multiplied by the amount specified in the preferences + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + Geom::Affine const i2d (item->i2dt_affine ()); + Geom::Point pp = pt * i2d.inverse(); + + double rad = 0.5 * prefs->getDouble(tool_path + "/dot-size", 3.0); + if (!strcmp(tool, "/tools/calligraphic")) + rad = 0.0333 * prefs->getDouble(tool_path + "/width", 3.0) / desktop->current_zoom() / desktop->getDocument()->getDocumentScale()[Geom::X]; + if (event_state & GDK_MOD1_MASK) { + // TODO: We vary the dot size between 0.5*rad and 1.5*rad, where rad is the dot size + // as specified in prefs. Very simple, but it might be sufficient in practice. If not, + // we need to devise something more sophisticated. + double s = g_random_double_range(-0.5, 0.5); + rad *= (1 + s); + } + if (event_state & GDK_SHIFT_MASK) { + // double the point size + rad *= 2; + } + + repr->setAttributeSvgDouble("sodipodi:cx", pp[Geom::X]); + repr->setAttributeSvgDouble("sodipodi:cy", pp[Geom::Y]); + repr->setAttributeSvgDouble("sodipodi:rx", rad * stroke_width); + repr->setAttributeSvgDouble("sodipodi:ry", rad * stroke_width); + item->updateRepr(); + item->doWriteTransform(item->transform, nullptr, true); + + desktop->getSelection()->set(item); + + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating single dot")); + DocumentUndo::done(desktop->getDocument(), _("Create single dot"), ""); +} + +} +} +} + +/* + 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/tools/freehand-base.h b/src/ui/tools/freehand-base.h new file mode 100644 index 0000000..e01af7a --- /dev/null +++ b/src/ui/tools/freehand-base.h @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_DRAW_CONTEXT_H +#define SEEN_SP_DRAW_CONTEXT_H + +/* + * Generic drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <optional> + +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" +#include "live_effects/effect-enum.h" + +class SPCurve; +class SPCanvasItem; + +struct SPDrawAnchor; + +namespace Inkscape { + class CanvasItemBpath; + class Selection; +} + +namespace Inkscape { +namespace UI { +namespace Tools { + +enum shapeType { NONE, TRIANGLE_IN, TRIANGLE_OUT, ELLIPSE, CLIPBOARD, BEND_CLIPBOARD, LAST_APPLIED }; + +class FreehandBase : public ToolBase { +public: + FreehandBase(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename); + ~FreehandBase() override; + + Inkscape::Selection *selection; + + bool attach; + + guint32 red_color; + guint32 blue_color; + guint32 green_color; + guint32 highlight_color; + + // Red - Last segment as it's drawn. + Inkscape::CanvasItemBpath *red_bpath; + std::unique_ptr<SPCurve> red_curve; + std::optional<Geom::Point> red_curve_get_last_point(); + + // Blue - New path after LPE as it's drawn. + Inkscape::CanvasItemBpath *blue_bpath; + std::unique_ptr<SPCurve> blue_curve; + + // Green - New path as it's drawn. + std::vector<Inkscape::CanvasItemBpath *> green_bpaths; + std::unique_ptr<SPCurve> green_curve; + std::unique_ptr<SPDrawAnchor> green_anchor; + gboolean green_closed; // a flag meaning we hit the green anchor, so close the path on itself + + // White + SPItem *white_item; + std::list<std::unique_ptr<SPCurve>> white_curves; + std::vector<std::unique_ptr<SPDrawAnchor>> white_anchors; + + // Temporary modified curve when start anchor + std::unique_ptr<SPCurve> sa_overwrited; + + // Start anchor + SPDrawAnchor *sa; + + // End anchor + SPDrawAnchor *ea; + + + /* Type of the LPE that is to be applied automatically to a finished path (if any) */ + Inkscape::LivePathEffect::EffectType waiting_LPE_type; + + sigc::connection sel_changed_connection; + sigc::connection sel_modified_connection; + + bool red_curve_is_valid; + + bool anchor_statusbar; + + bool tablet_enabled; + + bool is_tablet; + + gdouble pressure; + void set(const Inkscape::Preferences::Entry& val) override; + +protected: + bool root_handler(GdkEvent* event) override; +}; + +/** + * Returns FIRST active anchor (the activated one). + */ +SPDrawAnchor *spdc_test_inside(FreehandBase *dc, Geom::Point p); + +/** + * Concats red, blue and green. + * If any anchors are defined, process these, optionally removing curves from white list + * Invoke _flush_white to write result back to object. + */ +void spdc_concat_colors_and_flush(FreehandBase *dc, gboolean forceclosed); + +/** + * Snaps node or handle to PI/rotationsnapsperpi degree increments. + * + * @param dc draw context. + * @param p cursor point (to be changed by snapping). + * @param o origin point. + * @param state keyboard state to check if ctrl or shift was pressed. + */ +void spdc_endpoint_snap_rotation(ToolBase* const ec, Geom::Point &p, Geom::Point const &o, guint state); + +void spdc_endpoint_snap_free(ToolBase* const ec, Geom::Point &p, std::optional<Geom::Point> &start_of_line, guint state); + +/** + * If we have an item and a waiting LPE, apply the effect to the item + * (spiro spline mode is treated separately). + */ +void spdc_check_for_and_apply_waiting_LPE(FreehandBase *dc, SPItem *item); + +/** + * Create a single dot represented by a circle. + */ +void spdc_create_single_dot(ToolBase *ec, Geom::Point const &pt, char const *tool, guint event_state); + +} +} +} + +#endif // SEEN_SP_DRAW_CONTEXT_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/tools/gradient-tool.cpp b/src/ui/tools/gradient-tool.cpp new file mode 100644 index 0000000..fff39e8 --- /dev/null +++ b/src/ui/tools/gradient-tool.cpp @@ -0,0 +1,822 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient drawing and editing tool + * + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Abhishek Sharma + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gdk/gdkkeysyms.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "gradient-drag.h" +#include "message-context.h" +#include "message-stack.h" +#include "rubberband.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "snap.h" + +#include "include/macros.h" + +#include "object/sp-namedview.h" +#include "object/sp-stop.h" + +#include "display/control/canvas-item-curve.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-names.h" +#include "ui/tools/gradient-tool.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + + +GradientTool::GradientTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/gradient", "gradient.svg") + , cursor_addnode(false) + , node_added(false) +// TODO: Why are these connections stored as pointers? + , selcon(nullptr) + , subselcon(nullptr) +{ + // TODO: This value is overwritten in the root handler + this->tolerance = 6; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/gradient/selcue", true)) { + this->enableSelectionCue(); + } + + this->enableGrDrag(); + Inkscape::Selection *selection = desktop->getSelection(); + + this->selcon = new sigc::connection(selection->connectChanged( + sigc::mem_fun(this, &GradientTool::selection_changed) + )); + + subselcon = new sigc::connection(desktop->connect_gradient_stop_selected( + [=](void* sender, SPStop* stop) { + selection_changed(nullptr); + if (stop) { + // sync stop selection: + _grdrag->selectByStop(stop, false, true); + } + } + )); + + this->selection_changed(selection); +} + +GradientTool::~GradientTool() { + this->enableGrDrag(false); + + this->selcon->disconnect(); + delete this->selcon; + + this->subselcon->disconnect(); + delete this->subselcon; +} + +// This must match GrPointType enum sp-gradient.h +// We should move this to a shared header (can't simply move to gradient.h since that would require +// including <glibmm/i18n.h> which messes up "N_" in extensions... argh!). +const gchar *gr_handle_descr [] = { + N_("Linear gradient <b>start</b>"), //POINT_LG_BEGIN + N_("Linear gradient <b>end</b>"), + N_("Linear gradient <b>mid stop</b>"), + N_("Radial gradient <b>center</b>"), + N_("Radial gradient <b>radius</b>"), + N_("Radial gradient <b>radius</b>"), + N_("Radial gradient <b>focus</b>"), // POINT_RG_FOCUS + N_("Radial gradient <b>mid stop</b>"), + N_("Radial gradient <b>mid stop</b>"), + N_("Mesh gradient <b>corner</b>"), + N_("Mesh gradient <b>handle</b>"), + N_("Mesh gradient <b>tensor</b>") +}; + +void GradientTool::selection_changed(Inkscape::Selection*) { + + GrDrag *drag = _grdrag; + Inkscape::Selection *selection = _desktop->getSelection(); + if (selection == nullptr) { + return; + } + guint n_obj = (guint) boost::distance(selection->items()); + + if (!drag->isNonEmpty() || selection->isEmpty()) + return; + guint n_tot = drag->numDraggers(); + guint n_sel = drag->numSelected(); + + //The use of ngettext in the following code is intentional even if the English singular form would never be used + if (n_sel == 1) { + if (drag->singleSelectedDraggerNumDraggables() == 1) { + gchar * message = g_strconcat( + //TRANSLATORS: %s will be substituted with the point name (see previous messages); This is part of a compound message + _("%s selected"), + //TRANSLATORS: Mind the space in front. This is part of a compound message + ngettext(" out of %d gradient handle"," out of %d gradient handles",n_tot), + ngettext(" on %d selected object"," on %d selected objects",n_obj),nullptr); + message_context->setF(Inkscape::NORMAL_MESSAGE, + message,_(gr_handle_descr[drag->singleSelectedDraggerSingleDraggableType()]), n_tot, n_obj); + } else { + gchar * message = g_strconcat( + //TRANSLATORS: This is a part of a compound message (out of two more indicating: grandint handle count & object count) + ngettext("One handle merging %d stop (drag with <b>Shift</b> to separate) selected", + "One handle merging %d stops (drag with <b>Shift</b> to separate) selected",drag->singleSelectedDraggerNumDraggables()), + ngettext(" out of %d gradient handle"," out of %d gradient handles",n_tot), + ngettext(" on %d selected object"," on %d selected objects",n_obj),nullptr); + message_context->setF(Inkscape::NORMAL_MESSAGE,message,drag->singleSelectedDraggerNumDraggables(), n_tot, n_obj); + } + } else if (n_sel > 1) { + //TRANSLATORS: The plural refers to number of selected gradient handles. This is part of a compound message (part two indicates selected object count) + gchar * message = g_strconcat(ngettext("<b>%d</b> gradient handle selected out of %d","<b>%d</b> gradient handles selected out of %d",n_sel), + //TRANSLATORS: Mind the space in front. (Refers to gradient handles selected). This is part of a compound message + ngettext(" on %d selected object"," on %d selected objects",n_obj),nullptr); + message_context->setF(Inkscape::NORMAL_MESSAGE,message, n_sel, n_tot, n_obj); + } else if (n_sel == 0) { + message_context->setF(Inkscape::NORMAL_MESSAGE, + //TRANSLATORS: The plural refers to number of selected objects + ngettext("<b>No</b> gradient handles selected out of %d on %d selected object", + "<b>No</b> gradient handles selected out of %d on %d selected objects",n_obj), n_tot, n_obj); + } +} + +void GradientTool::select_next() +{ + g_assert(_grdrag); + GrDragger *d = _grdrag->select_next(); + _desktop->scroll_to_point(d->point, 1.0); +} + +void GradientTool::select_prev() +{ + g_assert(_grdrag); + GrDragger *d = _grdrag->select_prev(); + _desktop->scroll_to_point(d->point, 1.0); +} + +SPItem *GradientTool::is_over_curve(Geom::Point event_p) +{ + //Translate mouse point into proper coord system: needed later. + mousepoint_doc = _desktop->w2d(event_p); + + for (auto curve : _grdrag->item_curves) { + if (curve->contains(event_p, tolerance)) { + return curve->get_item(); + } + } + return nullptr; +} + +static std::vector<Geom::Point> +sp_gradient_context_get_stop_intervals (GrDrag *drag, std::vector<SPStop *> &these_stops, std::vector<SPStop *> &next_stops) +{ + std::vector<Geom::Point> coords; + + // for all selected draggers + for (std::set<GrDragger *>::const_iterator i = drag->selected.begin(); i != drag->selected.end() ; ++i ) { + GrDragger *dragger = *i; + // remember the coord of the dragger to reselect it later + coords.push_back(dragger->point); + // for all draggables of dragger + for (std::vector<GrDraggable *>::const_iterator j = dragger->draggables.begin(); j != dragger->draggables.end(); ++j) { + GrDraggable *d = *j; + + // find the gradient + SPGradient *gradient = getGradient(d->item, d->fill_or_stroke); + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (gradient, false); + + // these draggable types cannot have a next draggabe to insert a stop between them + if (d->point_type == POINT_LG_END || + d->point_type == POINT_RG_FOCUS || + d->point_type == POINT_RG_R1 || + d->point_type == POINT_RG_R2) { + continue; + } + + // from draggables to stops + SPStop *this_stop = sp_get_stop_i (vector, d->point_i); + SPStop *next_stop = this_stop->getNextStop(); + SPStop *last_stop = sp_last_stop (vector); + + Inkscape::PaintTarget fs = d->fill_or_stroke; + SPItem *item = d->item; + gint type = d->point_type; + gint p_i = d->point_i; + + // if there's a next stop, + if (next_stop) { + GrDragger *dnext = nullptr; + // find its dragger + // (complex because it may have different types, and because in radial, + // more than one dragger may correspond to a stop, so we must distinguish) + if (type == POINT_LG_BEGIN || type == POINT_LG_MID) { + if (next_stop == last_stop) { + dnext = drag->getDraggerFor(item, POINT_LG_END, p_i+1, fs); + } else { + dnext = drag->getDraggerFor(item, POINT_LG_MID, p_i+1, fs); + } + } else { // radial + if (type == POINT_RG_CENTER || type == POINT_RG_MID1) { + if (next_stop == last_stop) { + dnext = drag->getDraggerFor(item, POINT_RG_R1, p_i+1, fs); + } else { + dnext = drag->getDraggerFor(item, POINT_RG_MID1, p_i+1, fs); + } + } + if ((type == POINT_RG_MID2) || + (type == POINT_RG_CENTER && dnext && !dnext->isSelected())) { + if (next_stop == last_stop) { + dnext = drag->getDraggerFor(item, POINT_RG_R2, p_i+1, fs); + } else { + dnext = drag->getDraggerFor(item, POINT_RG_MID2, p_i+1, fs); + } + } + } + + // if both adjacent draggers selected, + if ((std::find(these_stops.begin(),these_stops.end(),this_stop)==these_stops.end()) && dnext && dnext->isSelected()) { + + // remember the coords of the future dragger to select it + coords.push_back(0.5*(dragger->point + dnext->point)); + + // do not insert a stop now, it will confuse the loop; + // just remember the stops + these_stops.push_back(this_stop); + next_stops.push_back(next_stop); + } + } + } + } + return coords; +} + +void GradientTool::add_stops_between_selected_stops() +{ + SPDocument *doc = nullptr; + GrDrag *drag = _grdrag; + + std::vector<SPStop *> these_stops; + std::vector<SPStop *> next_stops; + + std::vector<Geom::Point> coords = sp_gradient_context_get_stop_intervals (drag, these_stops, next_stops); + + if (these_stops.empty() && drag->numSelected() == 1) { + // if a single stop is selected, add between that stop and the next one + GrDragger *dragger = *(drag->selected.begin()); + for (auto d : dragger->draggables) { + if (d->point_type == POINT_RG_FOCUS) { + /* + * There are 2 draggables at the center (start) of a radial gradient + * To avoid creating 2 separate stops, ignore this draggable point type + */ + continue; + } + SPGradient *gradient = getGradient(d->item, d->fill_or_stroke); + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (gradient, false); + SPStop *this_stop = sp_get_stop_i (vector, d->point_i); + if (this_stop) { + SPStop *next_stop = this_stop->getNextStop(); + if (next_stop) { + these_stops.push_back(this_stop); + next_stops.push_back(next_stop); + } + } + } + } + + // now actually create the new stops + auto i = these_stops.rbegin(); + auto j = next_stops.rbegin(); + std::vector<SPStop *> new_stops; + + for (;i != these_stops.rend() && j != next_stops.rend(); ++i, ++j ) { + SPStop *this_stop = *i; + SPStop *next_stop = *j; + gfloat offset = 0.5*(this_stop->offset + next_stop->offset); + SPObject *parent = this_stop->parent; + if (SP_IS_GRADIENT (parent)) { + doc = parent->document; + SPStop *new_stop = sp_vector_add_stop (SP_GRADIENT (parent), this_stop, next_stop, offset); + new_stops.push_back(new_stop); + SP_GRADIENT(parent)->ensureVector(); + } + } + + if (!these_stops.empty() && doc) { + DocumentUndo::done(doc, _("Add gradient stop"), INKSCAPE_ICON("color-gradient")); + drag->updateDraggers(); + // so that it does not automatically update draggers in idle loop, as this would deselect + drag->local_change = true; + + // select the newly created stops + for (auto i:new_stops) { + drag->selectByStop(i); + } + } +} + +static double sqr(double x) {return x*x;} + +/** + * Remove unnecessary stops in the adjacent currently selected stops + * + * For selected stops that are adjacent to each other, remove + * stops that don't change the gradient visually, within a range of tolerance. + * + * @param tolerance maximum difference between stop and expected color at that position + */ +void GradientTool::simplify(double tolerance) +{ + SPDocument *doc = nullptr; + GrDrag *drag = _grdrag; + + std::vector<SPStop *> these_stops; + std::vector<SPStop *> next_stops; + + std::vector<Geom::Point> coords = sp_gradient_context_get_stop_intervals (drag, these_stops, next_stops); + + std::set<SPStop *> todel; + + auto i = these_stops.begin(); + auto j = next_stops.begin(); + for (; i != these_stops.end() && j != next_stops.end(); ++i, ++j) { + SPStop *stop0 = *i; + SPStop *stop1 = *j; + + // find the next adjacent stop if it exists and is in selection + auto i1 = std::find(these_stops.begin(), these_stops.end(), stop1); + if (i1 != these_stops.end()) { + if (next_stops.size()>(i1-these_stops.begin())) { + SPStop *stop2 = *(next_stops.begin() + (i1-these_stops.begin())); + + if (todel.find(stop0)!=todel.end() || todel.find(stop2) != todel.end()) + continue; + + // compare color of stop1 to the average color of stop0 and stop2 + guint32 const c0 = stop0->get_rgba32(); + guint32 const c2 = stop2->get_rgba32(); + guint32 const c1r = stop1->get_rgba32(); + guint32 c1 = average_color (c0, c2, + (stop1->offset - stop0->offset) / (stop2->offset - stop0->offset)); + + double diff = + sqr(SP_RGBA32_R_F(c1) - SP_RGBA32_R_F(c1r)) + + sqr(SP_RGBA32_G_F(c1) - SP_RGBA32_G_F(c1r)) + + sqr(SP_RGBA32_B_F(c1) - SP_RGBA32_B_F(c1r)) + + sqr(SP_RGBA32_A_F(c1) - SP_RGBA32_A_F(c1r)); + + if (diff < tolerance) + todel.insert(stop1); + } + } + } + + for (auto stop : todel) { + doc = stop->document; + Inkscape::XML::Node * parent = stop->getRepr()->parent(); + parent->removeChild( stop->getRepr() ); + } + + if (!todel.empty()) { + DocumentUndo::done(doc, _("Simplify gradient"), INKSCAPE_ICON("color-gradient")); + drag->local_change = true; + drag->updateDraggers(); + drag->selectByCoords(coords); + } +} + +void GradientTool::add_stop_near_point(SPItem *item, Geom::Point mouse_p, guint32 /*etime*/) +{ + // item is the selected item. mouse_p the location in doc coordinates of where to add the stop + SPStop *newstop = get_drag()->addStopNearPoint(item, mouse_p, tolerance / _desktop->current_zoom()); + + DocumentUndo::done(_desktop->getDocument(), _("Add gradient stop"), INKSCAPE_ICON("color-gradient")); + + get_drag()->updateDraggers(); + get_drag()->local_change = true; + get_drag()->selectByStop(newstop); +} + +bool GradientTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = _desktop->getSelection(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + GrDrag *drag = this->_grdrag; + g_assert (drag); + + gint ret = FALSE; + + switch (event->type) { + case GDK_2BUTTON_PRESS: + if ( event->button.button == 1 ) { + SPItem *item = is_over_curve(Geom::Point(event->motion.x, event->motion.y)); + if (item) { + // we take the first item in selection, because with doubleclick, the first click + // always resets selection to the single object under cursor + add_stop_near_point(selection->items().front(), mousepoint_doc, event->button.time); + } else { + auto items= selection->items(); + for (auto i = items.begin();i!=items.end();++i) { + SPItem *item = *i; + SPGradientType new_type = (SPGradientType) prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR); + Inkscape::PaintTarget fsmode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + + SPGradient *vector = sp_gradient_vector_for_object(_desktop->getDocument(), _desktop, item, fsmode); + + SPGradient *priv = sp_item_set_gradient(item, vector, new_type, fsmode); + sp_gradient_reset_to_userspace(priv, item); + } + _desktop->redrawDesktop();; + DocumentUndo::done(_desktop->getDocument(), _("Create default gradient"), INKSCAPE_ICON("color-gradient")); + } + ret = TRUE; + } + break; + + case GDK_BUTTON_PRESS: + if ( event->button.button == 1 ) { + Geom::Point button_w(event->button.x, event->button.y); + + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + dragging = true; + + Geom::Point button_dt = _desktop->w2d(button_w); + if (event->button.state & GDK_SHIFT_MASK) { + Inkscape::Rubberband::get(_desktop)->start(_desktop, button_dt); + } else { + // remember clicked item, disregarding groups, honoring Alt; do nothing with Crtl to + // enable Ctrl+doubleclick of exactly the selected item(s) + if (!(event->button.state & GDK_CONTROL_MASK)) { + this->item_to_select = sp_event_context_find_item (_desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + } + + if (!selection->isEmpty()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + } + + this->origin = button_dt; + } + + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && ( event->motion.state & GDK_BUTTON1_MASK )) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->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 draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point const motion_dt = _desktop->w2d(motion_w); + + if (Inkscape::Rubberband::get(_desktop)->is_started()) { + Inkscape::Rubberband::get(_desktop)->move(motion_dt); + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw around</b> handles to select them")); + } else { + this->drag(motion_dt, event->motion.state, event->motion.time); + } + + gobble_motion_events(GDK_BUTTON1_MASK); + + ret = TRUE; + } else { + if (!drag->mouseOver() && !selection->isEmpty()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt = _desktop->w2d(motion_w); + + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + + SPItem *item = is_over_curve(Geom::Point(event->motion.x, event->motion.y)); + + if (this->cursor_addnode && !item) { + this->set_cursor("gradient.svg"); + this->cursor_addnode = false; + } else if (!this->cursor_addnode && item) { + this->set_cursor("gradient-add.svg"); + this->cursor_addnode = true; + } + } + break; + + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + + if ( event->button.button == 1 ) { + SPItem *item = is_over_curve(Geom::Point(event->motion.x, event->motion.y)); + + if ( (event->button.state & GDK_CONTROL_MASK) && (event->button.state & GDK_MOD1_MASK ) ) { + if (item) { + this->add_stop_near_point(item, this->mousepoint_doc, 0); + ret = TRUE; + } + } else { + dragging = false; + + // unless clicked with Ctrl (to enable Ctrl+doubleclick). + if (event->button.state & GDK_CONTROL_MASK) { + ret = TRUE; + break; + } + + if (!this->within_tolerance) { + // we've been dragging, either do nothing (grdrag handles that), + // or rubberband-select if we have rubberband + Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop); + + if (r->is_started() && !this->within_tolerance) { + // this was a rubberband drag + if (r->getMode() == RUBBERBAND_MODE_RECT) { + Geom::OptRect const b = r->getRectangle(); + drag->selectRect(*b); + } + } + } else if (this->item_to_select) { + if (item) { + // Clicked on an existing gradient line, don't change selection. This stops + // possible change in selection during a double click with overlapping objects + } else { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + drag->deselectAll(); + selection->set(this->item_to_select); + } + } + } else { + // click in an empty space; do the same as Esc + if (!drag->selected.empty()) { + drag->deselectAll(); + } else { + selection->clear(); + } + } + + this->item_to_select = nullptr; + ret = TRUE; + } + + Inkscape::Rubberband::get(_desktop)->stop(); + } + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + sp_event_show_modifier_tip (this->defaultMessageContext(), event, + _("<b>Ctrl</b>: snap gradient angle"), + _("<b>Shift</b>: draw gradient around the starting point"), + nullptr); + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("altx-grad"); + ret = TRUE; + } + break; + + case GDK_KEY_A: + case GDK_KEY_a: + if (MOD__CTRL_ONLY(event) && drag->isNonEmpty()) { + drag->selectAll(); + ret = TRUE; + } + break; + + case GDK_KEY_L: + case GDK_KEY_l: + if (MOD__CTRL_ONLY(event) && drag->isNonEmpty() && drag->hasSelection()) { + this->simplify(1e-4); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (!drag->selected.empty()) { + drag->deselectAll(); + } else { + Inkscape::SelectionHelper::selectNone(_desktop); + } + ret = TRUE; + //TODO: make dragging escapable by Esc + break; + + case GDK_KEY_r: + case GDK_KEY_R: + if (MOD__SHIFT_ONLY(event)) { + sp_gradient_reverse_selected_gradients(_desktop); + ret = TRUE; + } + break; + + case GDK_KEY_Insert: + case GDK_KEY_KP_Insert: + // with any modifiers: + this->add_stops_between_selected_stops(); + ret = TRUE; + break; + + case GDK_KEY_i: + case GDK_KEY_I: + if (MOD__SHIFT_ONLY(event)) { + // Shift+I - insert stops (alternate keybinding for keyboards + // that don't have the Insert key) + this->add_stops_between_selected_stops(); + ret = TRUE; + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + case GDK_KEY_Tab: + if (hasGradientDrag()) { + select_next(); + ret = TRUE; + } + break; + + case GDK_KEY_ISO_Left_Tab: + if (hasGradientDrag()) { + select_prev(); + ret = TRUE; + } + break; + + default: + ret = drag->key_press_handler(event); + break; + } + break; + + case GDK_KEY_RELEASE: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +// Creates a new linear or radial gradient. +void GradientTool::drag(Geom::Point const pt, guint /*state*/, guint32 etime) +{ + Inkscape::Selection *selection = _desktop->getSelection(); + SPDocument *document = _desktop->getDocument(); + + if (!selection->isEmpty()) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int type = prefs->getInt("/tools/gradient/newgradient", 1); + Inkscape::PaintTarget fill_or_stroke = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + + SPGradient *vector; + if (item_to_select) { + // pick color from the object where drag started + vector = sp_gradient_vector_for_object(document, _desktop, item_to_select, fill_or_stroke); + } else { + // Starting from empty space: + // Sort items so that the topmost comes last + std::vector<SPItem*> items(selection->items().begin(), selection->items().end()); + sort(items.begin(),items.end(),sp_item_repr_compare_position_bool); + // take topmost + vector = sp_gradient_vector_for_object(document, _desktop, items.back(), fill_or_stroke); + } + + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + + auto itemlist = selection->items(); + for (auto i = itemlist.begin();i!=itemlist.end();++i) { + + //FIXME: see above + sp_repr_css_change_recursive((*i)->getRepr(), css, "style"); + + sp_item_set_gradient(*i, vector, (SPGradientType) type, fill_or_stroke); + + if (type == SP_GRADIENT_TYPE_LINEAR) { + sp_item_gradient_set_coords(*i, POINT_LG_BEGIN, 0, origin, fill_or_stroke, true, false); + sp_item_gradient_set_coords (*i, POINT_LG_END, 0, pt, fill_or_stroke, true, false); + } else if (type == SP_GRADIENT_TYPE_RADIAL) { + sp_item_gradient_set_coords(*i, POINT_RG_CENTER, 0, origin, fill_or_stroke, true, false); + sp_item_gradient_set_coords (*i, POINT_RG_R1, 0, pt, fill_or_stroke, true, false); + } + (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + if (_grdrag) { + _grdrag->updateDraggers(); + // prevent regenerating draggers by selection modified signal, which sometimes + // comes too late and thus destroys the knot which we will now grab: + _grdrag->local_change = true; + // give the grab out-of-bounds values of xp/yp because we're already dragging + // and therefore are already out of tolerance + _grdrag->grabKnot (selection->items().front(), + type == SP_GRADIENT_TYPE_LINEAR? POINT_LG_END : POINT_RG_R1, + -1, // ignore number (though it is always 1) + fill_or_stroke, 99999, 99999, etime); + } + // We did an undoable action, but SPDocumentUndo::done will be called by the knot when released + + // status text; we do not track coords because this branch is run once, not all the time + // during drag + int n_objects = (int) boost::distance(selection->items()); + message_context->setF(Inkscape::NORMAL_MESSAGE, + ngettext("<b>Gradient</b> for %d object; with <b>Ctrl</b> to snap angle", + "<b>Gradient</b> for %d objects; with <b>Ctrl</b> to snap angle", n_objects), + n_objects); + } else { + _desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>objects</b> on which to create gradient.")); + } +} + +} +} +} + + +/* + 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/tools/gradient-tool.h b/src/ui/tools/gradient-tool.h new file mode 100644 index 0000000..6bd2972 --- /dev/null +++ b/src/ui/tools/gradient-tool.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_GRADIENT_CONTEXT_H__ +#define __SP_GRADIENT_CONTEXT_H__ + +/* + * Gradient drawing and editing tool + * + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org. + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005,2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include "ui/tools/tool-base.h" + +#define SP_GRADIENT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::GradientTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_GRADIENT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::GradientTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +class GrDrag; + +namespace Inkscape { + +class Selection; + +namespace UI { +namespace Tools { + +class GradientTool : public ToolBase { +public: + GradientTool(SPDesktop *desktop); + ~GradientTool() override; + + bool root_handler(GdkEvent *event) override; + void add_stops_between_selected_stops(); + + void select_next(); + void select_prev(); + +private: + Geom::Point mousepoint_doc; // stores mousepoint when over_line in doc coords + Geom::Point origin; + bool cursor_addnode; + bool node_added; + + sigc::connection *selcon; + sigc::connection *subselcon; + + void selection_changed(Inkscape::Selection *); + void simplify(double tolerance); + void add_stop_near_point(SPItem *item, Geom::Point mouse_p, guint32 etime); + void drag(Geom::Point const pt, guint state, guint32 etime); + SPItem *is_over_curve(Geom::Point event_p); +}; + +} +} +} + +#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/tools/lpe-tool.cpp b/src/ui/tools/lpe-tool.cpp new file mode 100644 index 0000000..efb7d35 --- /dev/null +++ b/src/ui/tools/lpe-tool.cpp @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LPEToolContext: a context for a generic tool composed of subtools that are given by LPEs + * + * Authors: + * Maximilian Albert <maximilian.albert@gmail.com> + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2008 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iomanip> + +#include <glibmm/i18n.h> +#include <gtk/gtk.h> + +#include <2geom/sbasis-geometric.h> + +#include "desktop.h" +#include "document.h" +#include "message-context.h" +#include "message-stack.h" +#include "selection.h" + +#include "display/curve.h" +#include "display/control/canvas-item-rect.h" +#include "display/control/canvas-item-text.h" + +#include "object/sp-path.h" + +#include "util/units.h" + +#include "ui/toolbar/lpe-toolbar.h" +#include "ui/tools/lpe-tool.h" +#include "ui/shape-editor.h" + +using Inkscape::Util::unit_table; +using Inkscape::UI::Tools::PenTool; + +const int num_subtools = 8; + +SubtoolEntry lpesubtools[] = { + // this must be here to account for the "all inactive" action + {Inkscape::LivePathEffect::INVALID_LPE, "draw-geometry-inactive"}, + {Inkscape::LivePathEffect::LINE_SEGMENT, "draw-geometry-line-segment"}, + {Inkscape::LivePathEffect::CIRCLE_3PTS, "draw-geometry-circle-from-three-points"}, + {Inkscape::LivePathEffect::CIRCLE_WITH_RADIUS, "draw-geometry-circle-from-radius"}, + {Inkscape::LivePathEffect::PARALLEL, "draw-geometry-line-parallel"}, + {Inkscape::LivePathEffect::PERP_BISECTOR, "draw-geometry-line-perpendicular"}, + {Inkscape::LivePathEffect::ANGLE_BISECTOR, "draw-geometry-angle-bisector"}, + {Inkscape::LivePathEffect::MIRROR_SYMMETRY, "draw-geometry-mirror"} +}; + +namespace Inkscape::UI::Tools { + +void sp_lpetool_context_selection_changed(Inkscape::Selection *selection, gpointer data); + +LpeTool::LpeTool(SPDesktop *desktop) + : PenTool(desktop, "/tools/lpetool", "geometric.svg") + , mode(Inkscape::LivePathEffect::BEND_PATH) +{ + Inkscape::Selection *selection = desktop->getSelection(); + SPItem *item = selection->singleItem(); + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = + selection->connectChanged(sigc::bind(sigc::ptr_fun(&sp_lpetool_context_selection_changed), (gpointer)this)); + + this->shape_editor = new ShapeEditor(desktop); + + lpetool_context_switch_mode(this, Inkscape::LivePathEffect::INVALID_LPE); + lpetool_context_reset_limiting_bbox(this); + lpetool_create_measuring_items(this); + +// TODO temp force: + this->enableSelectionCue(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (item) { + this->shape_editor->set_item(item); + } + + if (prefs->getBool("/tools/lpetool/selcue")) { + this->enableSelectionCue(); + } +} + +LpeTool::~LpeTool() +{ + delete this->shape_editor; + + if (canvas_bbox) { + delete canvas_bbox; + } + + lpetool_delete_measuring_items(this); + measuring_items.clear(); + + this->sel_changed_connection.disconnect(); +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new nodepath and reassigns listeners to the new selected item's repr. + */ +void sp_lpetool_context_selection_changed(Inkscape::Selection *selection, gpointer data) +{ + LpeTool *lc = SP_LPETOOL_CONTEXT(data); + + lc->shape_editor->unset_item(); + SPItem *item = selection->singleItem(); + lc->shape_editor->set_item(item); +} + +void LpeTool::set(const Inkscape::Preferences::Entry& val) { + if (val.getEntryName() == "mode") { + Inkscape::Preferences::get()->setString("/tools/geometric/mode", "drag"); + SP_PEN_CONTEXT(this)->mode = PenTool::MODE_DRAG; + } +} + +bool LpeTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + { + // select the clicked item but do nothing else + Inkscape::Selection *const selection = _desktop->getSelection(); + selection->clear(); + selection->add(item); + ret = TRUE; + break; + } + case GDK_BUTTON_RELEASE: + // TODO: do we need to catch this or can we pass it on to the parent handler? + ret = TRUE; + break; + default: + break; + } + + if (!ret) { + ret = PenTool::item_handler(item, event); + } + + return ret; +} + +bool LpeTool::root_handler(GdkEvent* event) { + Inkscape::Selection *selection = _desktop->getSelection(); + + bool ret = false; + + if (this->hasWaitingLPE()) { + // quit when we are waiting for a LPE to be applied + //ret = ((ToolBaseClass *) sp_lpetool_context_parent_class)->root_handler(event_context, event); + return PenTool::root_handler(event); + } + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + if (this->mode == Inkscape::LivePathEffect::INVALID_LPE) { + // don't do anything for now if we are inactive (except clearing the selection + // since this was a click into empty space) + selection->clear(); + _desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Choose a construction tool from the toolbar.")); + ret = true; + break; + } + + // save drag origin + this->xp = (gint) event->button.x; + this->yp = (gint) event->button.y; + this->within_tolerance = true; + + using namespace Inkscape::LivePathEffect; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int mode = prefs->getInt("/tools/lpetool/mode"); + EffectType type = lpesubtools[mode].type; + + //bool over_stroke = lc->shape_editor->is_over_stroke(Geom::Point(event->button.x, event->button.y), true); + + this->waitForLPEMouseClicks(type, Inkscape::LivePathEffect::Effect::acceptsNumClicks(type)); + + // we pass the mouse click on to pen tool as the first click which it should collect + //ret = ((ToolBaseClass *) sp_lpetool_context_parent_class)->root_handler(event_context, event); + ret = PenTool::root_handler(event); + } + break; + + + case GDK_BUTTON_RELEASE: + { + /** + break; + **/ + } + + case GDK_KEY_PRESS: + /** + switch (get_latin_keyval (&event->key)) { + } + break; + **/ + + case GDK_KEY_RELEASE: + /** + switch (get_latin_keyval(&event->key)) { + case GDK_Control_L: + case GDK_Control_R: + dc->_message_context->clear(); + break; + default: + break; + } + **/ + + default: + break; + } + + if (!ret) { + ret = PenTool::root_handler(event); + } + + return ret; +} + +/* + * Finds the index in the list of geometric subtools corresponding to the given LPE type. + * Returns -1 if no subtool is found. + */ +int +lpetool_mode_to_index(Inkscape::LivePathEffect::EffectType const type) { + for (int i = 0; i < num_subtools; ++i) { + if (lpesubtools[i].type == type) { + return i; + } + } + return -1; +} + +/* + * Checks whether an item has a construction applied as LPE and if so returns the index in + * lpesubtools of this construction + */ +int lpetool_item_has_construction(LpeTool */*lc*/, SPItem *item) +{ + if (!SP_IS_LPE_ITEM(item)) { + return -1; + } + + Inkscape::LivePathEffect::Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + if (!lpe) { + return -1; + } + return lpetool_mode_to_index(lpe->effectType()); +} + +/* + * Attempts to perform the construction of the given type (i.e., to apply the corresponding LPE) to + * a single selected item. Returns whether we succeeded. + */ +bool +lpetool_try_construction(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type) +{ + Inkscape::Selection *selection = lc->getDesktop()->getSelection(); + SPItem *item = selection->singleItem(); + + // TODO: should we check whether type represents a valid geometric construction? + if (item && SP_IS_LPE_ITEM(item) && Inkscape::LivePathEffect::Effect::acceptsNumClicks(type) == 0) { + Inkscape::LivePathEffect::Effect::createAndApply(type, lc->getDesktop()->getDocument(), item); + return true; + } + return false; +} + +void +lpetool_context_switch_mode(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type) +{ + int index = lpetool_mode_to_index(type); + if (index != -1) { + lc->mode = type; + auto tb = dynamic_cast<UI::Toolbar::LPEToolbar*>(lc->getDesktop()->get_toolbar_by_name("LPEToolToolbar")); + + if(tb) { + tb->set_mode(index); + } else { + std::cerr << "Could not access LPE toolbar" << std::endl; + } + } else { + g_warning ("Invalid mode selected: %d", type); + return; + } +} + +void +lpetool_get_limiting_bbox_corners(SPDocument *document, Geom::Point &A, Geom::Point &B) { + Geom::Coord w = document->getWidth().value("px"); + Geom::Coord h = document->getHeight().value("px"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + double ulx = prefs->getDouble("/tools/lpetool/bbox_upperleftx", 0); + double uly = prefs->getDouble("/tools/lpetool/bbox_upperlefty", 0); + double lrx = prefs->getDouble("/tools/lpetool/bbox_lowerrightx", w); + double lry = prefs->getDouble("/tools/lpetool/bbox_lowerrighty", h); + + A = Geom::Point(ulx, uly); + B = Geom::Point(lrx, lry); +} + +/* + * Reads the limiting bounding box from preferences and draws it on the screen + */ +// TODO: Note that currently the bbox is not user-settable; we simply use the page borders +void +lpetool_context_reset_limiting_bbox(LpeTool *lc) +{ + if (lc->canvas_bbox) { + delete lc->canvas_bbox; + lc->canvas_bbox = nullptr; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs->getBool("/tools/lpetool/show_bbox", true)) + return; + + SPDocument *document = lc->getDesktop()->getDocument(); + + Geom::Point A, B; + lpetool_get_limiting_bbox_corners(document, A, B); + Geom::Affine doc2dt(lc->getDesktop()->doc2dt()); + A *= doc2dt; + B *= doc2dt; + + Geom::Rect rect(A, B); + lc->canvas_bbox = new Inkscape::CanvasItemRect(lc->getDesktop()->getCanvasControls(), rect); + lc->canvas_bbox->set_stroke(0x0000ffff); + lc->canvas_bbox->set_dashed(true); +} + +static void +set_pos_and_anchor(Inkscape::CanvasItemText *canvas_text, const Geom::Piecewise<Geom::D2<Geom::SBasis> > &pwd2, + const double t, const double length, bool /*use_curvature*/ = false) +{ + using namespace Geom; + + Piecewise<D2<SBasis> > pwd2_reparam = arc_length_parametrization(pwd2, 2 , 0.1); + double t_reparam = pwd2_reparam.cuts.back() * t; + Point pos = pwd2_reparam.valueAt(t_reparam); + Point dir = unit_vector(derivative(pwd2_reparam).valueAt(t_reparam)); + Point n = -rot90(dir); + double angle = Geom::angle_between(dir, Point(1,0)); + + canvas_text->set_coord(pos + n * length); + canvas_text->set_anchor(Geom::Point(std::sin(angle), -std::cos(angle))); +} + +void +lpetool_create_measuring_items(LpeTool *lc, Inkscape::Selection *selection) +{ + if (!selection) { + selection = lc->getDesktop()->getSelection(); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show = prefs->getBool("/tools/lpetool/show_measuring_info", true); + + Inkscape::CanvasItemText *canvas_text; + Inkscape::CanvasItemGroup *tmpgrp = lc->getDesktop()->getCanvasTemp(); + + Inkscape::Util::Unit const * unit = nullptr; + if (prefs->getString("/tools/lpetool/unit").compare("")) { + unit = unit_table.getUnit(prefs->getString("/tools/lpetool/unit")); + } else { + unit = unit_table.getUnit("px"); + } + + auto items= selection->items(); + for (auto i : items) { + SPPath *path = dynamic_cast<SPPath *>(i); + if (path) { + SPCurve const *curve = path->curve(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = paths_to_pw(curve->get_pathvector()); + + double lengthval = Geom::length(pwd2); + lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit); + + Glib::ustring arc_length = Glib::ustring::format(std::setprecision(2), std::fixed, lengthval); + arc_length += " "; + arc_length += unit->abbr; + + canvas_text = new Inkscape::CanvasItemText(tmpgrp, Geom::Point(0,0), arc_length); + set_pos_and_anchor(canvas_text, pwd2, 0.5, 10); + if (!show) { + canvas_text->hide(); + } + + (lc->measuring_items)[path] = canvas_text; + } + } +} + +void +lpetool_delete_measuring_items(LpeTool *lc) +{ + for (auto& i : lc->measuring_items) { + delete i.second; + } + lc->measuring_items.clear(); +} + +void +lpetool_update_measuring_items(LpeTool *lc) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::Util::Unit const * unit = nullptr; + if (prefs->getString("/tools/lpetool/unit").compare("")) { + unit = unit_table.getUnit(prefs->getString("/tools/lpetool/unit")); + } else { + unit = unit_table.getUnit("px"); + } + + for (auto& i : lc->measuring_items) { + + SPPath *path = i.first; + SPCurve const *curve = path->curve(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = Geom::paths_to_pw(curve->get_pathvector()); + double lengthval = Geom::length(pwd2); + lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit); + + Glib::ustring arc_length = Glib::ustring::format(std::setprecision(2), std::fixed, lengthval); + arc_length += " "; + arc_length += unit->abbr; + + i.second->set_text(arc_length); + set_pos_and_anchor(i.second, pwd2, 0.5, 10); + } +} + +void +lpetool_show_measuring_info(LpeTool *lc, bool show) +{ + for (auto& i : lc->measuring_items) { + if (show) { + i.second->show(); + } else { + i.second->hide(); + } + } +} + +} // namespace Inkscape::UI::Tools + +/* + 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/tools/lpe-tool.h b/src/ui/tools/lpe-tool.h new file mode 100644 index 0000000..fd77a13 --- /dev/null +++ b/src/ui/tools/lpe-tool.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_LPETOOL_CONTEXT_H_SEEN +#define SP_LPETOOL_CONTEXT_H_SEEN + +/* + * LPEToolContext: a context for a generic tool composed of subtools that are given by LPEs + * + * Authors: + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2008 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/pen-tool.h" + +#define SP_LPETOOL_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::LpeTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_LPETOOL_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::LpeTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +/* This is the list of subtools from which the toolbar of the LPETool is built automatically */ +extern const int num_subtools; + +struct SubtoolEntry { + Inkscape::LivePathEffect::EffectType type; + gchar const *icon_name; +}; + +extern SubtoolEntry lpesubtools[]; + +enum LPEToolState { + LPETOOL_STATE_PEN, + LPETOOL_STATE_NODE +}; + +namespace Inkscape { +class Selection; +} + +class ShapeEditor; + +namespace Inkscape { + +class CanvasItemText; +class CanvasItemRect; + +namespace UI { +namespace Tools { + +class LpeTool : public PenTool { +public: + LpeTool(SPDesktop *desktop); + ~LpeTool() override; + + ShapeEditor* shape_editor = nullptr; + Inkscape::CanvasItemRect *canvas_bbox = nullptr; + Inkscape::LivePathEffect::EffectType mode; + + std::map<SPPath *, Inkscape::CanvasItemText*> measuring_items; + + sigc::connection sel_changed_connection; + sigc::connection sel_modified_connection; +protected: + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; +}; + +int lpetool_mode_to_index(Inkscape::LivePathEffect::EffectType const type); +int lpetool_item_has_construction(LpeTool *lc, SPItem *item); +bool lpetool_try_construction(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type); +void lpetool_context_switch_mode(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type); +void lpetool_get_limiting_bbox_corners(SPDocument *document, Geom::Point &A, Geom::Point &B); +void lpetool_context_reset_limiting_bbox(LpeTool *lc); +void lpetool_create_measuring_items(LpeTool *lc, Inkscape::Selection *selection = nullptr); +void lpetool_delete_measuring_items(LpeTool *lc); +void lpetool_update_measuring_items(LpeTool *lc); +void lpetool_show_measuring_info(LpeTool *lc, bool show = true); + +} +} +} + +#endif // SP_LPETOOL_CONTEXT_H_SEEN + +/* + 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/tools/marker-tool.cpp b/src/ui/tools/marker-tool.cpp new file mode 100644 index 0000000..e2e2631 --- /dev/null +++ b/src/ui/tools/marker-tool.cpp @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Marker edit mode - onCanvas marker editing of marker orientation, position, scale + *//* + * Authors: + * see git history + * Rachana Podaralla <rpodaralla3@gatech.edu> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/curve.h" + +#include "desktop.h" +#include "document.h" +#include "style.h" +#include "message-context.h" +#include "selection.h" + +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-marker.h" + +#include "ui/shape-editor.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tools/marker-tool.h" + + +namespace Inkscape { +namespace UI { +namespace Tools { + +MarkerTool::MarkerTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/marker", "select.svg") +{ + Inkscape::Selection *selection = desktop->getSelection(); + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = selection->connectChanged( + sigc::mem_fun(this, &MarkerTool::selection_changed) + ); + this->selection_changed(selection); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/marker/selcue")) this->enableSelectionCue(); + if (prefs->getBool("/tools/marker/gradientdrag")) this->enableGrDrag(); +} + +MarkerTool::~MarkerTool() +{ + ungrabCanvasEvents(); + + this->message_context->clear(); + this->_shape_editors.clear(); + + this->enableGrDrag(false); + this->sel_changed_connection.disconnect(); +} + +/* +- cycles through all the selected items to see if any have a marker in the right location (based on enterMarkerMode) +- if a matching item is found, loads the corresponding marker on the shape into the shape-editor and exits the loop +- forces user to only edit one marker at a time +*/ +void MarkerTool::selection_changed(Inkscape::Selection *selection) { + using namespace Inkscape::UI; + + g_assert(_desktop != nullptr); + + SPDocument *doc = _desktop->getDocument(); + g_assert(doc != nullptr); + + auto selected_items = selection->items(); + this->_shape_editors.clear(); + + for(auto i = selected_items.begin(); i != selected_items.end(); ++i){ + SPItem *item = *i; + + if(item) { + SPShape* shape = dynamic_cast<SPShape*>(item); + + if(shape && shape->hasMarkers() && (editMarkerMode != -1)) { + SPObject *obj = shape->_marker[editMarkerMode]; + + if(obj) { + + SPMarker *sp_marker = dynamic_cast<SPMarker *>(obj); + g_assert(sp_marker != nullptr); + + sp_validate_marker(sp_marker, doc); + + ShapeRecord sr; + switch(editMarkerMode) { + case SP_MARKER_LOC_START: + sr = get_marker_transform(shape, item, sp_marker, SP_MARKER_LOC_START); + break; + + case SP_MARKER_LOC_MID: + sr = get_marker_transform(shape, item, sp_marker, SP_MARKER_LOC_MID); + break; + + case SP_MARKER_LOC_END: + sr = get_marker_transform(shape, item, sp_marker, SP_MARKER_LOC_END); + break; + + default: + break; + } + + auto si = std::make_unique<ShapeEditor>(_desktop, sr.edit_transform, sr.edit_rotation, editMarkerMode); + si->set_item(dynamic_cast<SPItem *>(sr.object)); + + this->_shape_editors.insert({item, std::move(si)}); + break; + } + } + } + } +} + +// handles selection of new items +bool MarkerTool::root_handler(GdkEvent* event) { + g_assert(_desktop != nullptr); + + Inkscape::Selection *selection = _desktop->getSelection(); + gint ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + + Geom::Point const button_w(event->button.x, event->button.y); + this->item_to_select = sp_event_context_find_item (_desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + + grabCanvasEvents(); + ret = true; + } + break; + case GDK_BUTTON_RELEASE: + if (event->button.button == 1) { + + if (this->item_to_select) { + // unselect all items, except for newly selected item + selection->set(this->item_to_select); + } else { + // clicked into empty space, deselect any selected items + selection->clear(); + } + + this->item_to_select = nullptr; + ungrabCanvasEvents(); + ret = true; + } + break; + default: + break; + } + + return (!ret? ToolBase::root_handler(event): ret); +} + +/* +- this function uses similar logic that exists in sp_shape_update_marker_view +- however, the tangent angle needs to be saved here and parent_item->i2dt_affine() needs to also be accounted for in the right places +- calculate where the shape-editor knotholders need to go based on the reference shape +*/ +ShapeRecord MarkerTool::get_marker_transform(SPShape* shape, SPItem *parent_item, SPMarker *sp_marker, SPMarkerLoc marker_type) +{ + + // scale marker transform with parent stroke width + SPStyle *style = shape->style; + SPDocument *doc = _desktop->getDocument(); + Geom::Scale scale = doc->getDocumentScale(); + + if(sp_marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + scale *= Geom::Scale(style->stroke_width.computed); + } + + Geom::PathVector const &pathv = shape->curve()->get_pathvector(); + Geom::Affine ret = Geom::identity(); //edit_transform + double angle = 0.0; // edit_rotation - tangent angle used for auto orientation + Geom::Point p; + + if(marker_type == SP_MARKER_LOC_START) { + + Geom::Curve const &c = pathv.begin()->front(); + p = c.pointAt(0); + ret = Geom::Translate(p * parent_item->i2doc_affine()); + + if (!c.isDegenerate()) { + Geom::Point tang = c.unitTangentAt(0); + angle = Geom::atan2(tang); + ret = Geom::Rotate(angle) * ret; + } + + } else if(marker_type == SP_MARKER_LOC_MID) { + /* + - a shape can have multiple mid markers - only one is needed + - once a valid mid marker is found, save edit_transfom and edit_rotation and break out of loop + */ + for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) { + + // mid marker start position + if (path_it != pathv.begin() && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0))) + { + Geom::Curve const &c = path_it->front(); + p = c.pointAt(0); + ret = Geom::Translate(p * parent_item->i2doc_affine()); + + if (!c.isDegenerate()) { + Geom::Point tang = c.unitTangentAt(0); + angle = Geom::atan2(tang); + ret = Geom::Rotate(angle) * ret; + break; + } + } + + // mid marker mid positions + if ( path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); + while (curve_it2 != path_it->end_default()) + { + Geom::Curve const & c1 = *curve_it1; + Geom::Curve const & c2 = *curve_it2; + + p = c1.pointAt(1); + Geom::Curve * c1_reverse = c1.reverse(); + Geom::Point tang1 = - c1_reverse->unitTangentAt(0); + delete c1_reverse; + Geom::Point tang2 = c2.unitTangentAt(0); + + double const angle1 = Geom::atan2(tang1); + double const angle2 = Geom::atan2(tang2); + + angle = .5 * (angle1 + angle2); + + if ( fabs( angle2 - angle1 ) > M_PI ) { + angle += M_PI; + } + + ret = Geom::Rotate(angle) * Geom::Translate(p * parent_item->i2doc_affine()); + + ++curve_it1; + ++curve_it2; + break; + } + } + + // mid marker end position + if ( path_it != (pathv.end()-1) && !path_it->empty()) { + Geom::Curve const &c = path_it->back_default(); + p = c.pointAt(1); + ret = Geom::Translate(p * parent_item->i2doc_affine()); + + if ( !c.isDegenerate() ) { + Geom::Curve * c_reverse = c.reverse(); + Geom::Point tang = - c_reverse->unitTangentAt(0); + delete c_reverse; + angle = Geom::atan2(tang); + ret = Geom::Rotate(angle) * ret; + break; + } + } + } + + } else if (marker_type == SP_MARKER_LOC_END) { + + Geom::Path const &path_last = pathv.back(); + unsigned int index = path_last.size_default(); + if (index > 0) index--; + + Geom::Curve const &c = path_last[index]; + p = c.pointAt(1); + ret = Geom::Translate(p * parent_item->i2doc_affine()); + + if ( !c.isDegenerate() ) { + Geom::Curve * c_reverse = c.reverse(); + Geom::Point tang = - c_reverse->unitTangentAt(0); + delete c_reverse; + angle = Geom::atan2(tang); + ret = Geom::Rotate(angle) * ret; + } + } + + /* scale by stroke width */ + ret = scale * ret; + /* account for parent transform */ + ret = parent_item->transform.withoutTranslation() * ret; + + ShapeRecord sr; + sr.object = sp_marker; + sr.edit_transform = ret; + sr.edit_rotation = angle * 180.0/M_PI; + sr.role = SHAPE_ROLE_NORMAL; + return sr; +} + +}}} diff --git a/src/ui/tools/marker-tool.h b/src/ui/tools/marker-tool.h new file mode 100644 index 0000000..92d77a2 --- /dev/null +++ b/src/ui/tools/marker-tool.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Marker edit mode - onCanvas marker editing of marker orientation, position, scale + *//* + * Authors: + * see git history + * Rachana Podaralla <rpodaralla3@gatech.edu> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __SP_MARKER_CONTEXT_H__ +#define __SP_MARKER_CONTEXT_H__ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include <2geom/point.h> + +#include "object/sp-marker.h" +#include "object/sp-marker-loc.h" + +#include "ui/tools/tool-base.h" +#include "ui/tool/shape-record.h" + +namespace Inkscape { +class Selection; +namespace UI { +namespace Tools { + +class MarkerTool : public ToolBase { +public: + MarkerTool(SPDesktop *desktop); + ~MarkerTool() override; + + void selection_changed(Inkscape::Selection *selection); + + bool root_handler(GdkEvent *event) override; + std::map<SPItem *, std::unique_ptr<ShapeEditor>> _shape_editors; + + int editMarkerMode = -1; + +private: + sigc::connection sel_changed_connection; + ShapeRecord get_marker_transform(SPShape *shape, SPItem *parent_item, SPMarker *sp_marker, SPMarkerLoc marker_type); +}; + +}}} + +#endif diff --git a/src/ui/tools/measure-tool.cpp b/src/ui/tools/measure-tool.cpp new file mode 100644 index 0000000..d9ba7b1 --- /dev/null +++ b/src/ui/tools/measure-tool.cpp @@ -0,0 +1,1470 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Our nice measuring tool + * + * Authors: + * Felipe Correa da Silva Sanches <juca@members.fsf.org> + * Jon A. Cruz <jon@joncruz.org> + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "measure-tool.h" + +#include <iomanip> + +#include <gtkmm.h> +#include <glibmm/i18n.h> + +#include <boost/none_t.hpp> + +#include <2geom/line.h> +#include <2geom/path-intersection.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "inkscape.h" +#include "layer-manager.h" +#include "path-chemistry.h" +#include "rubberband.h" +#include "text-editing.h" + +#include "display/curve.h" +#include "display/control/canvas-item-curve.h" +#include "display/control/canvas-item-ctrl.h" +#include "display/control/canvas-item-group.h" +#include "display/control/canvas-item-text.h" + +#include "object/sp-defs.h" +#include "object/sp-flowtext.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "svg/stringstream.h" +#include "svg/svg-color.h" +#include "svg/svg.h" + +#include "ui/dialog/knot-properties.h" +#include "ui/icon-names.h" +#include "ui/knot/knot.h" +#include "ui/tools/freehand-base.h" +#include "ui/widget/canvas.h" // Canvas area + +#include "util/units.h" + +using Inkscape::Util::unit_table; +using Inkscape::DocumentUndo; + +const guint32 MT_KNOT_COLOR_NORMAL = 0xffffff00; +const guint32 MT_KNOT_COLOR_MOUSEOVER = 0xff000000; + + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + +/** + * Simple class to use for removing label overlap. + */ +class LabelPlacement { +public: + + double lengthVal; + double offset; + Geom::Point start; + Geom::Point end; +}; + +bool SortLabelPlacement(LabelPlacement const &first, LabelPlacement const &second) +{ + if (first.end[Geom::Y] == second.end[Geom::Y]) { + return first.end[Geom::X] < second.end[Geom::X]; + } else { + return first.end[Geom::Y] < second.end[Geom::Y]; + } +} + +//precision is for give the number of decimal positions +//of the label to calculate label width +void repositionOverlappingLabels(std::vector<LabelPlacement> &placements, SPDesktop *desktop, Geom::Point const &normal, double fontsize, int precision) +{ + std::sort(placements.begin(), placements.end(), SortLabelPlacement); + + double border = 3; + Geom::Rect box; + { + Geom::Point tmp(fontsize * (6 + precision) + (border * 2), fontsize + (border * 2)); + tmp = desktop->w2d(tmp); + box = Geom::Rect(-tmp[Geom::X] / 2, -tmp[Geom::Y] / 2, tmp[Geom::X] / 2, tmp[Geom::Y] / 2); + } + + // Using index since vector may be re-ordered as we go. + // Starting at one, since the first item can't overlap itself + for (size_t i = 1; i < placements.size(); i++) { + LabelPlacement &place = placements[i]; + + bool changed = false; + do { + Geom::Rect current(box + place.end); + + changed = false; + bool overlaps = false; + for (size_t j = i; (j > 0) && !overlaps; --j) { + LabelPlacement &otherPlace = placements[j - 1]; + Geom::Rect target(box + otherPlace.end); + if (current.intersects(target)) { + overlaps = true; + } + } + if (overlaps) { + place.offset += (fontsize + border); + place.end = place.start - desktop->w2d(normal * place.offset); + changed = true; + } + } while (changed); + + std::sort(placements.begin(), placements.begin() + i + 1, SortLabelPlacement); + } +} + +/** + * Calculates where to place the anchor for the display text and arc. + * + * @param desktop the desktop that is being used. + * @param angle the angle to be displaying. + * @param baseAngle the angle of the initial baseline. + * @param startPoint the point that is the vertex of the selected angle. + * @param endPoint the point that is the end the user is manipulating for measurement. + * @param fontsize the size to display the text label at. + */ +Geom::Point calcAngleDisplayAnchor(SPDesktop *desktop, double angle, double baseAngle, + Geom::Point const &startPoint, Geom::Point const &endPoint, + double fontsize) +{ + // Time for the trick work of figuring out where things should go, and how. + double lengthVal = (endPoint - startPoint).length(); + double effective = baseAngle + (angle / 2); + Geom::Point where(lengthVal, 0); + where *= Geom::Affine(Geom::Rotate(effective)) * Geom::Affine(Geom::Translate(startPoint)); + + // When the angle is tight, the label would end up under the cursor and/or lines. Bump it + double scaledFontsize = std::abs(fontsize * desktop->w2d(Geom::Point(0, 1.0))[Geom::Y]); + if (std::abs((where - endPoint).length()) < scaledFontsize) { + where[Geom::Y] += scaledFontsize * 2; + } + + // We now have the ideal position, but need to see if it will fit/work. + + Geom::Rect screen_world = desktop->getCanvas()->get_area_world(); + if (screen_world.interiorContains(desktop->d2w(startPoint)) || + screen_world.interiorContains(desktop->d2w(endPoint))) { + screen_world.expandBy(fontsize * -3, fontsize / -2); + where = desktop->w2d(screen_world.clamp(desktop->d2w(where))); + } // else likely initialized the measurement tool, keep display near the measurement. + + return where; +} + +} // namespace + +/** + * Given an angle, the arc center and edge point, draw an arc segment centered around that edge point. + * + * @param desktop the desktop that is being used. + * @param center the center point for the arc. + * @param end the point that ends at the edge of the arc segment. + * @param anchor the anchor point for displaying the text label. + * @param angle the angle of the arc segment to draw. + * @param measure_rpr the container of the curve if converted to items. + * + */ +void MeasureTool::createAngleDisplayCurve(Geom::Point const ¢er, Geom::Point const &end, Geom::Point const &anchor, + double angle, bool to_phantom, + std::vector<Inkscape::CanvasItem *> &measure_phantom_items, + std::vector<Inkscape::CanvasItem *> &measure_tmp_items, + Inkscape::XML::Node *measure_repr) +{ + // Given that we have a point on the arc's edge and the angle of the arc, we need to get the two endpoints. + + double textLen = std::abs((anchor - center).length()); + double sideLen = std::abs((end - center).length()); + if (sideLen > 0.0) { + double factor = std::min(1.0, textLen / sideLen); + + // arc start + Geom::Point p1 = end * (Geom::Affine(Geom::Translate(-center)) + * Geom::Affine(Geom::Scale(factor)) + * Geom::Affine(Geom::Translate(center))); + + // arc end + Geom::Point p4 = p1 * (Geom::Affine(Geom::Translate(-center)) + * Geom::Affine(Geom::Rotate(-angle)) + * Geom::Affine(Geom::Translate(center))); + + // from Riskus + double xc = center[Geom::X]; + double yc = center[Geom::Y]; + double ax = p1[Geom::X] - xc; + double ay = p1[Geom::Y] - yc; + double bx = p4[Geom::X] - xc; + double by = p4[Geom::Y] - yc; + double q1 = (ax * ax) + (ay * ay); + double q2 = q1 + (ax * bx) + (ay * by); + + double k2; + + /* + * The denominator of the expression for k2 can become 0, so this should be handled. + * The function for k2 tends to a limit for very small values of (ax * by) - (ay * bx), so theoretically + * it should be correct for values close to 0, however due to floating point inaccuracies this + * is not the case, and instabilities still exist. Therefore do a range check on the denominator. + * (This also solves some instances where again due to floating point inaccuracies, the square root term + * becomes slightly negative in case of very small values for ax * by - ay * bx). + * The values of this range have been generated by trying to make this term as small as possible, + * by zooming in as much as possible in the GUI, using the measurement tool and + * trying to get as close to 180 or 0 degrees as possible. + * Smallest value I was able to get was around 1e-5, and then I added some zeroes for good measure. + */ + if (!((ax * by - ay * bx < 0.00000000001) && (ax * by - ay * bx > -0.00000000001))) { + k2 = (4.0 / 3.0) * (std::sqrt(2 * q1 * q2) - q2) / ((ax * by) - (ay * bx)); + } else { + // If the denominator is 0, there are 2 cases: + // Either the angle is (almost) +-180 degrees, in which case the limit of k2 tends to -+4.0/3.0. + if (angle > 3.14 || angle < -3.14) { // The angle is in radians + // Now there are also 2 cases, where inkscape thinks it is 180 degrees, or -180 degrees. + // Adjust the value of k2 accordingly + if (angle > 0) { + k2 = -4.0 / 3.0; + } else { + k2 = 4.0 / 3.0; + } + } else { + // if the angle is (almost) 0, k2 is equal to 0 + k2 = 0.0; + } + } + + Geom::Point p2(xc + ax - (k2 * ay), + yc + ay + (k2 * ax)); + Geom::Point p3(xc + bx + (k2 * by), + yc + by - (k2 * bx)); + + auto *curve = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2, p3, p4); + curve->set_name("CanvasItemCurve:MeasureToolCurve"); + curve->set_stroke(Inkscape::CANVAS_ITEM_SECONDARY); + curve->set_z_position(0); + curve->show(); + if(to_phantom){ + curve->set_stroke(0x8888887f); + measure_phantom_items.emplace_back(curve); + } else { + measure_tmp_items.emplace_back(curve); + } + + if(measure_repr) { + Geom::PathVector pathv; + Geom::Path path; + path.start(_desktop->doc2dt(p1)); + path.appendNew<Geom::CubicBezier>(_desktop->doc2dt(p2), _desktop->doc2dt(p3), _desktop->doc2dt(p4)); + pathv.push_back(path); + auto layer = _desktop->layerManager().currentLayer(); + pathv *= layer->i2doc_affine().inverse(); + if(!pathv.empty()) { + setMeasureItem(pathv, true, false, 0xff00007f, measure_repr); + } + } + } +} + +std::optional<Geom::Point> explicit_base_tmp = std::nullopt; + +MeasureTool::MeasureTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/measure", "measure.svg") +{ + start_p = readMeasurePoint(true); + end_p = readMeasurePoint(false); + + // create the knots + this->knot_start = new SPKnot(desktop, _("Measure start, <b>Shift+Click</b> for position dialog"), + Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "CanvasItemCtrl:MeasureTool"); + this->knot_start->setMode(Inkscape::CANVAS_ITEM_CTRL_MODE_XOR); + this->knot_start->setFill(MT_KNOT_COLOR_NORMAL, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER); + this->knot_start->setStroke(0x0000007f, 0x0000007f, 0x0000007f, 0x0000007f); + this->knot_start->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + this->knot_start->updateCtrl(); + this->knot_start->moveto(start_p); + this->knot_start->show(); + + this->knot_end = new SPKnot(desktop, _("Measure end, <b>Shift+Click</b> for position dialog"), + Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "CanvasItemCtrl:MeasureTool"); + this->knot_end->setMode(Inkscape::CANVAS_ITEM_CTRL_MODE_XOR); + this->knot_end->setFill(MT_KNOT_COLOR_NORMAL, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER); + this->knot_end->setStroke(0x0000007f, 0x0000007f, 0x0000007f, 0x0000007f); + this->knot_end->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + this->knot_end->updateCtrl(); + this->knot_end->moveto(end_p); + this->knot_end->show(); + + showCanvasItems(); + + this->_knot_start_moved_connection = this->knot_start->moved_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotStartMovedHandler)); + this->_knot_start_click_connection = this->knot_start->click_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotClickHandler)); + this->_knot_start_ungrabbed_connection = this->knot_start->ungrabbed_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotUngrabbedHandler)); + this->_knot_end_moved_connection = this->knot_end->moved_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotEndMovedHandler)); + this->_knot_end_click_connection = this->knot_end->click_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotClickHandler)); + this->_knot_end_ungrabbed_connection = this->knot_end->ungrabbed_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotUngrabbedHandler)); + +} + +MeasureTool::~MeasureTool() +{ + this->enableGrDrag(false); + ungrabCanvasEvents(); + + this->_knot_start_moved_connection.disconnect(); + this->_knot_start_ungrabbed_connection.disconnect(); + this->_knot_end_moved_connection.disconnect(); + this->_knot_end_ungrabbed_connection.disconnect(); + + /* unref should call destroy */ + knot_unref(this->knot_start); + knot_unref(this->knot_end); + + for (auto & measure_tmp_item : measure_tmp_items) { + delete measure_tmp_item; + } + measure_tmp_items.clear(); + + for (auto & idx : measure_item) { + delete idx; + } + measure_item.clear(); + + for (auto & measure_phantom_item : measure_phantom_items) { + delete measure_phantom_item; + } + measure_phantom_items.clear(); +} + +Geom::Point MeasureTool::readMeasurePoint(bool is_start) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring measure_point = is_start ? "/tools/measure/measure-start" : "/tools/measure/measure-end"; + return prefs->getPoint(measure_point, Geom::Point(Geom::infinity(),Geom::infinity())); +} + +void MeasureTool::writeMeasurePoint(Geom::Point point, bool is_start) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring measure_point = is_start ? "/tools/measure/measure-start" : "/tools/measure/measure-end"; + prefs->setPoint(measure_point, point); +} + +//This function is used to reverse the Measure, I do it in two steps because when +//we move the knot the start_ or the end_p are overwritten so I need the original values. +void MeasureTool::reverseKnots() +{ + Geom::Point start = start_p; + Geom::Point end = end_p; + this->knot_start->moveto(end); + this->knot_start->show(); + this->knot_end->moveto(start); + this->knot_end->show(); + start_p = end; + end_p = start; + this->showCanvasItems(); +} + +void MeasureTool::knotClickHandler(SPKnot *knot, guint state) +{ + if (state & GDK_SHIFT_MASK) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring const unit_name = prefs->getString("/tools/measure/unit", "px"); + explicit_base = explicit_base_tmp; + Inkscape::UI::Dialogs::KnotPropertiesDialog::showDialog(_desktop, knot, unit_name); + } +} + +void MeasureTool::knotStartMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state) +{ + Geom::Point point = this->knot_start->position(); + if (state & GDK_CONTROL_MASK) { + spdc_endpoint_snap_rotation(this, point, end_p, state); + } else if (!(state & GDK_SHIFT_MASK)) { + SnapManager &snap_manager = _desktop->namedview->snap_manager; + snap_manager.setup(_desktop); + Inkscape::SnapCandidatePoint scp(point, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(this->knot_end->position()); + Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp); + point = sp.getPoint(); + snap_manager.unSetup(); + } + if(start_p != point) { + start_p = point; + this->knot_start->moveto(start_p); + } + showCanvasItems(); +} + +void MeasureTool::knotEndMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state) +{ + Geom::Point point = this->knot_end->position(); + if (state & GDK_CONTROL_MASK) { + spdc_endpoint_snap_rotation(this, point, start_p, state); + } else if (!(state & GDK_SHIFT_MASK)) { + SnapManager &snap_manager = _desktop->namedview->snap_manager; + snap_manager.setup(_desktop); + Inkscape::SnapCandidatePoint scp(point, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(this->knot_start->position()); + Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp); + point = sp.getPoint(); + snap_manager.unSetup(); + } + if(end_p != point) { + end_p = point; + this->knot_end->moveto(end_p); + } + showCanvasItems(); +} + +void MeasureTool::knotUngrabbedHandler(SPKnot */*knot*/, unsigned int state) +{ + this->knot_start->moveto(start_p); + this->knot_end->moveto(end_p); + showCanvasItems(); +} + +static void calculate_intersections(SPDesktop *desktop, SPItem *item, Geom::PathVector const &lineseg, + std::unique_ptr<SPCurve> &&curve, std::vector<double> &intersections) +{ + curve->transform(item->i2doc_affine()); + // Find all intersections of the control-line with this shape + Geom::CrossingSet cs = Geom::crossings(lineseg, curve->get_pathvector()); + Geom::delete_duplicates(cs[0]); + + // Reconstruct and store the points of intersection + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show_hidden = prefs->getBool("/tools/measure/show_hidden", true); + for (const auto & m : cs[0]) { + if (!show_hidden) { + double eps = 0.0001; + if ((m.ta > eps && + item == desktop->getItemAtPoint(desktop->d2w(desktop->dt2doc(lineseg[0].pointAt(m.ta - eps))), true, nullptr)) || + (m.ta + eps < 1 && + item == desktop->getItemAtPoint(desktop->d2w(desktop->dt2doc(lineseg[0].pointAt(m.ta + eps))), true, nullptr))) { + intersections.push_back(m.ta); + } + } else { + intersections.push_back(m.ta); + } + } +} + +bool MeasureTool::root_handler(GdkEvent* event) +{ + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: { + if (event->button.button != 1) { + break; + } + this->knot_start->hide(); + this->knot_end->hide(); + Geom::Point const button_w(event->button.x, event->button.y); + explicit_base = std::nullopt; + explicit_base_tmp = std::nullopt; + last_end = std::nullopt; + + // save drag origin + start_p = _desktop->w2d(Geom::Point(event->button.x, event->button.y)); + within_tolerance = true; + + SnapManager &snap_manager = _desktop->namedview->snap_manager; + snap_manager.setup(_desktop); + snap_manager.freeSnapReturnByRef(start_p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + snap_manager.unSetup(); + + grabCanvasEvents(Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK ); + ret = TRUE; + break; + } + case GDK_KEY_PRESS: { + if ((event->key.keyval == GDK_KEY_Control_L) || (event->key.keyval == GDK_KEY_Control_R)) { + explicit_base_tmp = explicit_base; + explicit_base = end_p; + showInfoBox(last_pos, true); + } + break; + } + case GDK_KEY_RELEASE: { + if ((event->key.keyval == GDK_KEY_Control_L) || (event->key.keyval == GDK_KEY_Control_R)) { + showInfoBox(last_pos, false); + } + break; + } + case GDK_MOTION_NOTIFY: { + if (!(event->motion.state & GDK_BUTTON1_MASK)) { + if(!(event->motion.state & GDK_SHIFT_MASK)) { + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + + SnapManager &snap_manager = _desktop->namedview->snap_manager; + snap_manager.setup(_desktop); + + Inkscape::SnapCandidatePoint scp(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(start_p); + + snap_manager.preSnap(scp); + snap_manager.unSetup(); + } + last_pos = Geom::Point(event->motion.x, event->motion.y); + if (event->motion.state & GDK_CONTROL_MASK) { + showInfoBox(last_pos, true); + } else { + showInfoBox(last_pos, false); + } + } else { + // Inkscape::Util::Unit const * unit = _desktop->getNamedView()->getDisplayUnit(); + for (auto & idx : measure_item) { + delete idx; + } + measure_item.clear(); + + ret = TRUE; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + Geom::Point const motion_w(event->motion.x, event->motion.y); + if ( within_tolerance) { + if ( Geom::LInfty( motion_w - start_p ) < tolerance) { + return FALSE; // Do not drag if we're within tolerance from origin. + } + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + within_tolerance = false; + if(event->motion.time == 0 || !last_end || Geom::LInfty( motion_w - *last_end ) > (tolerance/4.0)) { + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + end_p = motion_dt; + + if (event->motion.state & GDK_CONTROL_MASK) { + spdc_endpoint_snap_rotation(this, end_p, start_p, event->motion.state); + } else if (!(event->motion.state & GDK_SHIFT_MASK)) { + SnapManager &snap_manager = _desktop->namedview->snap_manager; + snap_manager.setup(_desktop); + Inkscape::SnapCandidatePoint scp(end_p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(start_p); + Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp); + end_p = sp.getPoint(); + snap_manager.unSetup(); + } + showCanvasItems(); + last_end = motion_w ; + } + gobble_motion_events(GDK_BUTTON1_MASK); + } + break; + } + case GDK_BUTTON_RELEASE: { + if (event->button.button != 1) { + break; + } + this->knot_start->moveto(start_p); + this->knot_start->show(); + if(last_end) { + end_p = _desktop->w2d(*last_end); + if (event->button.state & GDK_CONTROL_MASK) { + spdc_endpoint_snap_rotation(this, end_p, start_p, event->motion.state); + } else if (!(event->button.state & GDK_SHIFT_MASK)) { + SnapManager &snap_manager = _desktop->namedview->snap_manager; + snap_manager.setup(_desktop); + Inkscape::SnapCandidatePoint scp(end_p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(start_p); + Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp); + end_p = sp.getPoint(); + snap_manager.unSetup(); + } + } + this->knot_end->moveto(end_p); + this->knot_end->show(); + showCanvasItems(); + + ungrabCanvasEvents(); + break; + } + default: + break; + } + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void MeasureTool::setMarkers() +{ + SPDocument *doc = _desktop->getDocument(); + SPObject *arrowStart = doc->getObjectById("Arrow2Sstart"); + SPObject *arrowEnd = doc->getObjectById("Arrow2Send"); + if (!arrowStart) { + setMarker(true); + } + if(!arrowEnd) { + setMarker(false); + } +} +void MeasureTool::setMarker(bool isStart) +{ + SPDocument *doc = _desktop->getDocument(); + SPDefs *defs = doc->getDefs(); + Inkscape::XML::Node *rmarker; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + rmarker = xml_doc->createElement("svg:marker"); + rmarker->setAttribute("id", isStart ? "Arrow2Sstart" : "Arrow2Send"); + rmarker->setAttribute("inkscape:isstock", "true"); + rmarker->setAttribute("inkscape:stockid", isStart ? "Arrow2Sstart" : "Arrow2Send"); + rmarker->setAttribute("orient", "auto"); + rmarker->setAttribute("refX", "0.0"); + rmarker->setAttribute("refY", "0.0"); + rmarker->setAttribute("style", "overflow:visible;"); + SPItem *marker = SP_ITEM(defs->appendChildRepr(rmarker)); + Inkscape::GC::release(rmarker); + marker->updateRepr(); + Inkscape::XML::Node *rpath; + rpath = xml_doc->createElement("svg:path"); + rpath->setAttribute("d", "M 8.72,4.03 L -2.21,0.02 L 8.72,-4.00 C 6.97,-1.63 6.98,1.62 8.72,4.03 z"); + rpath->setAttribute("id", isStart ? "Arrow2SstartPath" : "Arrow2SendPath"); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property (css, "stroke", "none"); + sp_repr_css_set_property (css, "fill", "#000000"); + sp_repr_css_set_property (css, "fill-opacity", "1"); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + rpath->setAttribute("style", css_str); + sp_repr_css_attr_unref (css); + rpath->setAttribute("transform", isStart ? "scale(0.3) translate(-2.3,0)" : "scale(0.3) rotate(180) translate(-2.3,0)"); + SPItem *path = SP_ITEM(marker->appendChildRepr(rpath)); + Inkscape::GC::release(rpath); + path->updateRepr(); +} + +void MeasureTool::toGuides() +{ + if (!_desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) { + return; + } + SPDocument *doc = _desktop->getDocument(); + Geom::Point start = _desktop->doc2dt(start_p) * _desktop->doc2dt(); + Geom::Point end = _desktop->doc2dt(end_p) * _desktop->doc2dt(); + Geom::Ray ray(start,end); + SPNamedView *namedview = _desktop->namedview; + if(!namedview) { + return; + } + setGuide(start,ray.angle(), _("Measure")); + if(explicit_base) { + auto layer = _desktop->layerManager().currentLayer(); + explicit_base = *explicit_base * layer->i2doc_affine().inverse(); + ray.setPoints(start, *explicit_base); + if(ray.angle() != 0) { + setGuide(start,ray.angle(), _("Base")); + } + } + setGuide(start,0,""); + setGuide(start,Geom::rad_from_deg(90),_("Start")); + setGuide(end,0,_("End")); + setGuide(end,Geom::rad_from_deg(90),""); + showCanvasItems(true); + doc->ensureUpToDate(); + DocumentUndo::done(_desktop->getDocument(), _("Add guides from measure tool"), INKSCAPE_ICON("tool-measure")); +} + +void MeasureTool::toPhantom() +{ + if (!_desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) { + return; + } + SPDocument *doc = _desktop->getDocument(); + + for (auto & measure_phantom_item : measure_phantom_items) { + delete measure_phantom_item; + } + measure_phantom_items.clear(); + + for (auto & measure_tmp_item : measure_tmp_items) { + delete measure_tmp_item; + } + measure_tmp_items.clear(); + + showCanvasItems(false, false, true); + doc->ensureUpToDate(); + DocumentUndo::done(_desktop->getDocument(), _("Keep last measure on the canvas, for reference"), INKSCAPE_ICON("tool-measure")); +} + +void MeasureTool::toItem() +{ + if (!_desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) { + return; + } + SPDocument *doc = _desktop->getDocument(); + Geom::Ray ray(start_p,end_p); + guint32 line_color_primary = 0x0000ff7f; + Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + Inkscape::XML::Node *rgroup = xml_doc->createElement("svg:g"); + showCanvasItems(false, true, false, rgroup); + setLine(start_p,end_p, false, line_color_primary, rgroup); + SPItem *measure_item = SP_ITEM(_desktop->layerManager().currentLayer()->appendChildRepr(rgroup)); + Inkscape::GC::release(rgroup); + measure_item->updateRepr(); + doc->ensureUpToDate(); + DocumentUndo::done(_desktop->getDocument(), _("Convert measure to items"), INKSCAPE_ICON("tool-measure")); + reset(); +} + +void MeasureTool::toMarkDimension() +{ + if (!_desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) { + return; + } + SPDocument *doc = _desktop->getDocument(); + setMarkers(); + Geom::Ray ray(start_p,end_p); + Geom::Point start = start_p + Geom::Point::polar(ray.angle(), 5); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + dimension_offset = prefs->getDouble("/tools/measure/offset", 5.0); + start = start + Geom::Point::polar(ray.angle() + Geom::rad_from_deg(90), -dimension_offset); + Geom::Point end = end_p + Geom::Point::polar(ray.angle(), -5); + end = end+ Geom::Point::polar(ray.angle() + Geom::rad_from_deg(90), -dimension_offset); + guint32 color = 0x000000ff; + setLine(start, end, true, color); + Glib::ustring unit_name = prefs->getString("/tools/measure/unit"); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); + + Geom::Point middle = Geom::middle_point(start, end); + double totallengthval = (end_p - start_p).length(); + totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name); + double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0; + + + int precision = prefs->getInt("/tools/measure/precision", 2); + Glib::ustring total = Glib::ustring::format(std::fixed, std::setprecision(precision), totallengthval * scale); + total += unit_name; + + double textangle = Geom::rad_from_deg(180) - ray.angle(); + if (_desktop->is_yaxisdown()) { + textangle = ray.angle() - Geom::rad_from_deg(180); + } + + setLabelText(total, middle, fontsize, textangle, color); + + doc->ensureUpToDate(); + DocumentUndo::done(_desktop->getDocument(), _("Add global measure line"), INKSCAPE_ICON("tool-measure")); +} + +void MeasureTool::setGuide(Geom::Point origin, double angle, const char *label) +{ + SPDocument *doc = _desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + SPRoot const *root = doc->getRoot(); + Geom::Affine affine(Geom::identity()); + if(root) { + affine *= root->c2p.inverse(); + } + SPNamedView *namedview = _desktop->namedview; + if(!namedview) { + return; + } + + // <sodipodi:guide> stores inverted y-axis coordinates + if (_desktop->is_yaxisdown()) { + origin[Geom::Y] = doc->getHeight().value("px") - origin[Geom::Y]; + angle *= -1.0; + } + + origin *= affine; + //measure angle + Inkscape::XML::Node *guide; + guide = xml_doc->createElement("sodipodi:guide"); + std::stringstream position; + position.imbue(std::locale::classic()); + position << origin[Geom::X] << "," << origin[Geom::Y]; + guide->setAttribute("position", position.str() ); + guide->setAttribute("inkscape:color", "rgb(167,0,255)"); + guide->setAttribute("inkscape:label", label); + Geom::Point unit_vector = Geom::rot90(origin.polar(angle)); + std::stringstream angle_str; + angle_str.imbue(std::locale::classic()); + angle_str << unit_vector[Geom::X] << "," << unit_vector[Geom::Y]; + guide->setAttribute("orientation", angle_str.str()); + namedview->appendChild(guide); + Inkscape::GC::release(guide); +} + +void MeasureTool::setLine(Geom::Point start_point,Geom::Point end_point, bool markers, guint32 color, Inkscape::XML::Node *measure_repr) +{ + if (!_desktop || !start_p.isFinite() || !end_p.isFinite()) { + return; + } + Geom::PathVector pathv; + Geom::Path path; + path.start(_desktop->doc2dt(start_point)); + path.appendNew<Geom::LineSegment>(_desktop->doc2dt(end_point)); + pathv.push_back(path); + pathv *= _desktop->layerManager().currentLayer()->i2doc_affine().inverse(); + if(!pathv.empty()) { + setMeasureItem(pathv, false, markers, color, measure_repr); + } +} + +void MeasureTool::setPoint(Geom::Point origin, Inkscape::XML::Node *measure_repr) +{ + if (!_desktop || !origin.isFinite()) { + return; + } + char const * svgd; + svgd = "m 0.707,0.707 6.586,6.586 m 0,-6.586 -6.586,6.586"; + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + Geom::Scale scale = Geom::Scale(_desktop->current_zoom()).inverse(); + pathv *= Geom::Translate(Geom::Point(-3.5,-3.5)); + pathv *= scale; + pathv *= Geom::Translate(Geom::Point() - (scale.vector() * 0.5)); + pathv *= Geom::Translate(_desktop->doc2dt(origin)); + pathv *= _desktop->layerManager().currentLayer()->i2doc_affine().inverse(); + if (!pathv.empty()) { + guint32 line_color_secondary = 0xff0000ff; + setMeasureItem(pathv, false, false, line_color_secondary, measure_repr); + } +} + +void MeasureTool::setLabelText(Glib::ustring const &value, Geom::Point pos, double fontsize, Geom::Coord angle, + guint32 background, Inkscape::XML::Node *measure_repr) +{ + Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + /* Create <text> */ + pos = _desktop->doc2dt(pos); + Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text"); + rtext->setAttribute("xml:space", "preserve"); + + + /* Set style */ + sp_desktop_apply_style_tool(_desktop, rtext, "/tools/text", true); + if(measure_repr) { + rtext->setAttributeSvgDouble("x", 2); + rtext->setAttributeSvgDouble("y", 2); + } else { + rtext->setAttributeSvgDouble("x", 0); + rtext->setAttributeSvgDouble("y", 0); + } + + /* Create <tspan> */ + Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan"); + rtspan->setAttribute("sodipodi:role", "line"); + SPCSSAttr *css = sp_repr_css_attr_new(); + std::stringstream font_size; + font_size.imbue(std::locale::classic()); + if(measure_repr) { + font_size << fontsize; + } else { + font_size << fontsize << "pt"; + } + sp_repr_css_set_property (css, "font-size", font_size.str().c_str()); + sp_repr_css_set_property (css, "font-style", "normal"); + sp_repr_css_set_property (css, "font-weight", "normal"); + sp_repr_css_set_property (css, "line-height", "125%"); + sp_repr_css_set_property (css, "letter-spacing", "0"); + sp_repr_css_set_property (css, "word-spacing", "0"); + sp_repr_css_set_property (css, "text-align", "center"); + sp_repr_css_set_property (css, "text-anchor", "middle"); + if(measure_repr) { + sp_repr_css_set_property (css, "fill", "#FFFFFF"); + } else { + sp_repr_css_set_property (css, "fill", "#000000"); + } + sp_repr_css_set_property (css, "fill-opacity", "1"); + sp_repr_css_set_property (css, "stroke", "none"); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + rtspan->setAttribute("style", css_str); + sp_repr_css_attr_unref (css); + rtext->addChild(rtspan, nullptr); + Inkscape::GC::release(rtspan); + /* Create TEXT */ + Inkscape::XML::Node *rstring = xml_doc->createTextNode(value.c_str()); + rtspan->addChild(rstring, nullptr); + Inkscape::GC::release(rstring); + auto layer = _desktop->layerManager().currentLayer(); + auto text_item = dynamic_cast<SPText*>(layer->appendChildRepr(rtext)); + Inkscape::GC::release(rtext); + text_item->rebuildLayout(); + text_item->updateRepr(); + Geom::OptRect bbox = text_item->geometricBounds(); + if (!measure_repr && bbox) { + Geom::Point center = bbox->midpoint(); + text_item->transform *= Geom::Translate(center).inverse(); + pos += Geom::Point::polar(angle+ Geom::rad_from_deg(90), -bbox->height()); + } + if(measure_repr) { + /* Create <group> */ + Inkscape::XML::Node *rgroup = xml_doc->createElement("svg:g"); + /* Create <rect> */ + Inkscape::XML::Node *rrect = xml_doc->createElement("svg:rect"); + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar color_line[64]; + sp_svg_write_color (color_line, sizeof(color_line), background); + sp_repr_css_set_property (css, "fill", color_line); + sp_repr_css_set_property (css, "fill-opacity", "0.5"); + sp_repr_css_set_property (css, "stroke-width", "0"); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + rrect->setAttribute("style", css_str); + sp_repr_css_attr_unref (css); + rgroup->setAttributeSvgDouble("x", 0); + rgroup->setAttributeSvgDouble("y", 0); + rrect->setAttributeSvgDouble("x", -bbox->width()/2.0); + rrect->setAttributeSvgDouble("y", -bbox->height()); + rrect->setAttributeSvgDouble("width", bbox->width() + 6); + rrect->setAttributeSvgDouble("height", bbox->height() + 6); + Inkscape::XML::Node *rtextitem = text_item->getRepr(); + text_item->deleteObject(); + rgroup->addChild(rtextitem, nullptr); + Inkscape::GC::release(rtextitem); + rgroup->addChild(rrect, nullptr); + Inkscape::GC::release(rrect); + SPItem *text_item_box = SP_ITEM(layer->appendChildRepr(rgroup)); + Geom::Scale scale = Geom::Scale(_desktop->current_zoom()).inverse(); + if(bbox) { + text_item_box->transform *= Geom::Translate(bbox->midpoint() - Geom::Point(1.0,1.0)).inverse(); + } + text_item_box->transform *= scale; + text_item_box->transform *= Geom::Translate(Geom::Point() - (scale.vector() * 0.5)); + text_item_box->transform *= Geom::Translate(pos); + text_item_box->transform *= layer->i2doc_affine().inverse(); + text_item_box->updateRepr(); + text_item_box->doWriteTransform(text_item_box->transform, nullptr, true); + Inkscape::XML::Node *rlabel = text_item_box->getRepr(); + text_item_box->deleteObject(); + measure_repr->addChild(rlabel, nullptr); + Inkscape::GC::release(rlabel); + } else { + text_item->transform *= Geom::Rotate(angle); + text_item->transform *= Geom::Translate(pos); + text_item->transform *= layer->i2doc_affine().inverse(); + text_item->doWriteTransform(text_item->transform, nullptr, true); + } +} + +void MeasureTool::reset() +{ + this->knot_start->hide(); + this->knot_end->hide(); + + for (auto & measure_tmp_item : measure_tmp_items) { + delete measure_tmp_item; + } + measure_tmp_items.clear(); +} + +void MeasureTool::setMeasureCanvasText(bool is_angle, double precision, double amount, double fontsize, + Glib::ustring unit_name, Geom::Point position, guint32 background, + bool to_left, bool to_item, + bool to_phantom, Inkscape::XML::Node *measure_repr) +{ + Glib::ustring measure = Glib::ustring::format(std::setprecision(precision), std::fixed, amount); + measure += " "; + measure += (is_angle ? "°" : unit_name); + auto canvas_tooltip = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), position, measure); + canvas_tooltip->set_fontsize(fontsize); + canvas_tooltip->set_fill(0xffffffff); + canvas_tooltip->set_background(background); + if (to_left) { + canvas_tooltip->set_anchor(Geom::Point(0, 0.5)); + } else { + canvas_tooltip->set_anchor(Geom::Point(0.5, 0.5)); + } + + if (to_phantom){ + canvas_tooltip->set_background(0x4444447f); + measure_phantom_items.push_back(canvas_tooltip); + } else { + measure_tmp_items.push_back(canvas_tooltip); + } + + if (to_item) { + setLabelText(measure, position, fontsize, 0, background, measure_repr); + } + + canvas_tooltip->show(); + +} + +void MeasureTool::setMeasureCanvasItem(Geom::Point position, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr){ + guint32 color = 0xff0000ff; + if (to_phantom){ + color = 0x888888ff; + } + + auto canvas_item = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, position); + canvas_item->set_stroke(color); + canvas_item->set_z_position(0); + canvas_item->set_pickable(false); + canvas_item->show(); + + if (to_phantom){ + measure_phantom_items.emplace_back(canvas_item); + } else { + measure_tmp_items.emplace_back(canvas_item); + } + + if(to_item) { + setPoint(position, measure_repr); + } +} + +void MeasureTool::setMeasureCanvasControlLine(Geom::Point start, Geom::Point end, bool to_item, bool to_phantom, + Inkscape::CanvasItemColor ctrl_line_type, + Inkscape::XML::Node *measure_repr){ + gint32 color = (ctrl_line_type == Inkscape::CANVAS_ITEM_PRIMARY) ? 0x0000ff7f : 0xff00007f; + if (to_phantom) { + color = (ctrl_line_type == Inkscape::CANVAS_ITEM_PRIMARY) ? 0x4444447f : 0x8888887f; + } + + auto control_line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), start, end); + control_line->set_stroke(color); + control_line->set_z_position(0); + control_line->show(); + + if (to_phantom) { + measure_phantom_items.emplace_back(control_line); + } else { + measure_tmp_items.emplace_back(control_line); + } + + if (to_item) { + setLine(start, end, false, color, measure_repr); + } +} + +// This is the text that follows the cursor around. +void MeasureTool::showItemInfoText(Geom::Point pos, Glib::ustring const &measure_str, double fontsize) +{ + auto canvas_tooltip = new CanvasItemText(_desktop->getCanvasTemp(), pos, measure_str); + canvas_tooltip->set_fontsize(fontsize); + canvas_tooltip->set_fill(0xffffffff); + canvas_tooltip->set_background(0x00000099); + canvas_tooltip->set_anchor(Geom::Point(0, 0)); + canvas_tooltip->set_fixed_line(true); + canvas_tooltip->show(); + measure_item.push_back(canvas_tooltip); +} + +void MeasureTool::showInfoBox(Geom::Point cursor, bool into_groups) +{ + using Inkscape::Util::Quantity; + + for (auto & idx : measure_item) { + delete(idx); + } + measure_item.clear(); + + SPItem *newover = _desktop->getItemAtPoint(cursor, into_groups); + if (!newover) { + // Clear over when the cursor isn't over anything. + over = nullptr; + return; + } + Inkscape::Util::Unit const *unit = _desktop->getNamedView()->getDisplayUnit(); + + // Load preferences for measuring the new object. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int precision = prefs->getInt("/tools/measure/precision", 2); + bool selected = prefs->getBool("/tools/measure/only_selected", false); + auto box_type = prefs->getBool("/tools/bounding_box", false) ? SPItem::GEOMETRIC_BBOX : SPItem::VISUAL_BBOX; + double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); + double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0; + Glib::ustring unit_name = prefs->getString("/tools/measure/unit", unit->abbr); + + Geom::Scale zoom = Geom::Scale(Quantity::convert(_desktop->current_zoom(), "px", unit->abbr)).inverse(); + + if(newover != over) { + // Get information for the item, and cache it to save time. + over = newover; + auto affine = over->i2dt_affine() * Geom::Scale(scale); + if (auto bbox = over->bounds(box_type, affine)) { + item_width = Quantity::convert(bbox->width(), "px", unit_name); + item_height = Quantity::convert(bbox->height(), "px", unit_name); + item_x = Quantity::convert(bbox->left(), "px", unit_name); + item_y = Quantity::convert(bbox->top(), "px", unit_name); + + if (auto shape = dynamic_cast<SPShape *>(over)) { + auto pw = paths_to_pw(shape->curve()->get_pathvector()); + item_length = Quantity::convert(Geom::length(pw * affine), "px", unit_name); + } + } + } + + gchar *measure_str = nullptr; + std::stringstream precision_str; + precision_str.imbue(std::locale::classic()); + double origin = Quantity::convert(14, "px", unit->abbr); + Geom::Point rel_position = Geom::Point(origin, origin); + Geom::Point pos = _desktop->w2d(cursor); + double gap = Quantity::convert(7 + fontsize, "px", unit->abbr); + double yaxisdir = _desktop->yaxisdir(); + + if (selected) { + showItemInfoText(pos - (yaxisdir * rel_position * zoom), _desktop->getSelection()->includes(over) ? _("Selected") : _("Not selected"), fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + } + + if (SP_IS_SHAPE(over)) { + + precision_str << _("Length") << ": %." << precision << "f %s"; + measure_str = g_strdup_printf(precision_str.str().c_str(), item_length, unit_name.c_str()); + precision_str.str(""); + showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + + } else if (SP_IS_GROUP(over)) { + + measure_str = _("Press 'CTRL' to measure into group"); + showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + + } + + precision_str << "Y: %." << precision << "f %s"; + measure_str = g_strdup_printf(precision_str.str().c_str(), item_y, unit_name.c_str()); + precision_str.str(""); + showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + + precision_str << "X: %." << precision << "f %s"; + measure_str = g_strdup_printf(precision_str.str().c_str(), item_x, unit_name.c_str()); + precision_str.str(""); + showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + + precision_str << _("Height") << ": %." << precision << "f %s"; + measure_str = g_strdup_printf(precision_str.str().c_str(), item_height, unit_name.c_str()); + precision_str.str(""); + showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + + precision_str << _("Width") << ": %." << precision << "f %s"; + measure_str = g_strdup_printf(precision_str.str().c_str(), item_width, unit_name.c_str()); + precision_str.str(""); + showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize); + g_free(measure_str); +} + +void MeasureTool::showCanvasItems(bool to_guides, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr) +{ + if (!_desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) { + return; + } + writeMeasurePoint(start_p, true); + writeMeasurePoint(end_p, false); + + //clear previous canvas items, we'll draw new ones + for (auto & measure_tmp_item : measure_tmp_items) { + delete measure_tmp_item; + } + measure_tmp_items.clear(); + + //TODO:Calculate the measure area for current length and origin + // and use canvas->requestRedraw. In the calculation need a gap for outside text + // maybe this remove the trash lines on measure use + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show_in_between = prefs->getBool("/tools/measure/show_in_between", true); + bool all_layers = prefs->getBool("/tools/measure/all_layers", true); + dimension_offset = 70; + Geom::PathVector lineseg; + Geom::Path p; + Geom::Point start_p_doc = start_p * _desktop->dt2doc(); + Geom::Point end_p_doc = end_p * _desktop->dt2doc(); + p.start(start_p_doc); + p.appendNew<Geom::LineSegment>(end_p_doc); + lineseg.push_back(p); + + double angle = atan2(end_p - start_p); + double baseAngle = 0; + + if (explicit_base) { + baseAngle = atan2(*explicit_base - start_p); + angle -= baseAngle; + + // make sure that the angle is between -pi and pi. + if (angle > M_PI) { + angle -= 2 * M_PI; + } + if (angle < -M_PI) { + angle += 2 * M_PI; + } + } + + std::vector<SPItem*> items; + SPDocument *doc = _desktop->getDocument(); + Geom::Rect rect(start_p_doc, end_p_doc); + items = doc->getItemsPartiallyInBox(_desktop->dkey, rect, false, true, false, true); + SPGroup *current_layer = _desktop->layerManager().currentLayer(); + + std::vector<double> intersection_times; + bool only_selected = prefs->getBool("/tools/measure/only_selected", false); + for (auto i : items) { + SPItem *item = i; + if (!_desktop->getSelection()->includes(i) && only_selected) { + continue; + } + if (all_layers || _desktop->layerManager().layerForObject(item) == current_layer) { + if (auto shape = dynamic_cast<SPShape const *>(item)) { + calculate_intersections(_desktop, item, lineseg, SPCurve::copy(shape->curve()), intersection_times); + } else { + if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + Inkscape::Text::Layout::iterator iter = te_get_layout(item)->begin(); + do { + Inkscape::Text::Layout::iterator iter_next = iter; + iter_next.nextGlyph(); // iter_next is one glyph ahead from iter + if (iter == iter_next) { + break; + } + + // get path from iter to iter_next: + auto curve = te_get_layout(item)->convertToCurves(iter, iter_next); + iter = iter_next; // shift to next glyph + if (!curve) { + continue; // error converting this glyph + } + if (curve->is_empty()) { // whitespace glyph? + continue; + } + + calculate_intersections(_desktop, item, lineseg, std::move(curve), intersection_times); + if (iter == te_get_layout(item)->end()) { + break; + } + } while (true); + } + } + } + } + Glib::ustring unit_name = prefs->getString("/tools/measure/unit"); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0; + double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); + // Normal will be used for lines and text + Geom::Point windowNormal = Geom::unit_vector(Geom::rot90(_desktop->d2w(end_p - start_p))); + Geom::Point normal = _desktop->w2d(windowNormal); + + std::vector<Geom::Point> intersections; + std::sort(intersection_times.begin(), intersection_times.end()); + for (double & intersection_time : intersection_times) { + intersections.push_back(lineseg[0].pointAt(intersection_time)); + } + + if(!show_in_between && intersection_times.size() > 1) { + Geom::Point start = lineseg[0].pointAt(intersection_times[0]); + Geom::Point end = lineseg[0].pointAt(intersection_times[intersection_times.size()-1]); + intersections.clear(); + intersections.push_back(start); + intersections.push_back(end); + } + if (!prefs->getBool("/tools/measure/ignore_1st_and_last", true)) { + intersections.insert(intersections.begin(),lineseg[0].pointAt(0)); + intersections.push_back(lineseg[0].pointAt(1)); + } + std::vector<LabelPlacement> placements; + for (size_t idx = 1; idx < intersections.size(); ++idx) { + LabelPlacement placement; + placement.lengthVal = (intersections[idx] - intersections[idx - 1]).length(); + placement.lengthVal = Inkscape::Util::Quantity::convert(placement.lengthVal, "px", unit_name); + placement.offset = dimension_offset / 2; + placement.start = _desktop->doc2dt((intersections[idx - 1] + intersections[idx]) / 2); + placement.end = placement.start - (normal * placement.offset); + + placements.push_back(placement); + } + int precision = prefs->getInt("/tools/measure/precision", 2); + // Adjust positions + repositionOverlappingLabels(placements, _desktop, windowNormal, fontsize, precision); + for (auto & place : placements) { + setMeasureCanvasText(false, precision, place.lengthVal * scale, fontsize, unit_name, place.end, 0x0000007f, + false, to_item, to_phantom, measure_repr); + } + Geom::Point angleDisplayPt = calcAngleDisplayAnchor(_desktop, angle, baseAngle, start_p, end_p, fontsize); + + setMeasureCanvasText(true, precision, Geom::deg_from_rad(angle), fontsize, unit_name, angleDisplayPt, 0x337f337f, + false, to_item, to_phantom, measure_repr); + + { + double totallengthval = (end_p - start_p).length(); + totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name); + Geom::Point origin = end_p + _desktop->w2d(Geom::Point(3 * fontsize, -fontsize)); + setMeasureCanvasText(false, precision, totallengthval * scale, fontsize, unit_name, origin, 0x3333337f, + true, to_item, to_phantom, measure_repr); + } + + if (intersections.size() > 2) { + double totallengthval = (intersections[intersections.size()-1] - intersections[0]).length(); + totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name); + Geom::Point origin = _desktop->doc2dt((intersections[0] + intersections[intersections.size()-1])/2) + normal * dimension_offset; + setMeasureCanvasText(false, precision, totallengthval * scale, fontsize, unit_name, origin, 0x33337f7f, + false, to_item, to_phantom, measure_repr); + } + + // Initial point + setMeasureCanvasItem(start_p, false, to_phantom, measure_repr); + + // Now that text has been added, we can add lines and controls so that they go underneath + for (size_t idx = 0; idx < intersections.size(); ++idx) { + setMeasureCanvasItem(_desktop->doc2dt(intersections[idx]), to_item, to_phantom, measure_repr); + if(to_guides) { + gchar *cross_number; + if (!prefs->getBool("/tools/measure/ignore_1st_and_last", true)) { + cross_number= g_strdup_printf(_("Crossing %lu"), static_cast<unsigned long>(idx)); + } else { + cross_number= g_strdup_printf(_("Crossing %lu"), static_cast<unsigned long>(idx + 1)); + } + if (!prefs->getBool("/tools/measure/ignore_1st_and_last", true) && idx == 0) { + setGuide(_desktop->doc2dt(intersections[idx]), angle + Geom::rad_from_deg(90), ""); + } else { + setGuide(_desktop->doc2dt(intersections[idx]), angle + Geom::rad_from_deg(90), cross_number); + } + g_free(cross_number); + } + } + // Since adding goes to the bottom, do all lines last. + + // draw main control line + { + setMeasureCanvasControlLine(start_p, end_p, false, to_phantom, Inkscape::CANVAS_ITEM_PRIMARY, measure_repr); + double length = std::abs((end_p - start_p).length()); + Geom::Point anchorEnd = start_p; + anchorEnd[Geom::X] += length; + if (explicit_base) { + anchorEnd *= (Geom::Affine(Geom::Translate(-start_p)) + * Geom::Affine(Geom::Rotate(baseAngle)) + * Geom::Affine(Geom::Translate(start_p))); + } + setMeasureCanvasControlLine(start_p, anchorEnd, to_item, to_phantom, Inkscape::CANVAS_ITEM_SECONDARY, measure_repr); + createAngleDisplayCurve(start_p, end_p, angleDisplayPt, angle, to_phantom, measure_phantom_items, measure_tmp_items, measure_repr); + } + + if (intersections.size() > 2) { + setMeasureCanvasControlLine(_desktop->doc2dt(intersections[0]) + normal * dimension_offset, _desktop->doc2dt(intersections[intersections.size() - 1]) + normal * dimension_offset, to_item, to_phantom, Inkscape::CANVAS_ITEM_PRIMARY , measure_repr); + + setMeasureCanvasControlLine(_desktop->doc2dt(intersections[0]), _desktop->doc2dt(intersections[0]) + normal * dimension_offset, to_item, to_phantom, Inkscape::CANVAS_ITEM_PRIMARY , measure_repr); + + setMeasureCanvasControlLine(_desktop->doc2dt(intersections[intersections.size() - 1]), _desktop->doc2dt(intersections[intersections.size() - 1]) + normal * dimension_offset, to_item, to_phantom, Inkscape::CANVAS_ITEM_PRIMARY , measure_repr); + } + + // call-out lines + for (auto & place : placements) { + setMeasureCanvasControlLine(place.start, place.end, to_item, to_phantom, Inkscape::CANVAS_ITEM_SECONDARY, measure_repr); + } + + { + for (size_t idx = 1; idx < intersections.size(); ++idx) { + Geom::Point measure_text_pos = (intersections[idx - 1] + intersections[idx]) / 2; + setMeasureCanvasControlLine(_desktop->doc2dt(measure_text_pos), _desktop->doc2dt(measure_text_pos) - (normal * dimension_offset / 2), to_item, to_phantom, Inkscape::CANVAS_ITEM_SECONDARY, measure_repr); + } + } +} + +/** + * Create a measure item in current document. + * + * @param pathv the path to create. + * @param markers if the path results get markers. + * @param color of the stroke. + * @param measure_repr container element. + */ +void MeasureTool::setMeasureItem(Geom::PathVector pathv, bool is_curve, bool markers, guint32 color, Inkscape::XML::Node *measure_repr) +{ + if(!_desktop) { + return; + } + SPDocument *doc = _desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr; + repr = xml_doc->createElement("svg:path"); + auto str = sp_svg_write_path(pathv); + SPCSSAttr *css = sp_repr_css_attr_new(); + auto layer = _desktop->layerManager().currentLayer(); + Geom::Coord strokewidth = layer->i2doc_affine().inverse().expansionX(); + std::stringstream stroke_width; + stroke_width.imbue(std::locale::classic()); + if(measure_repr) { + stroke_width << strokewidth / _desktop->current_zoom(); + } else { + stroke_width << strokewidth; + } + sp_repr_css_set_property (css, "stroke-width", stroke_width.str().c_str()); + sp_repr_css_set_property (css, "fill", "none"); + if(color) { + gchar color_line[64]; + sp_svg_write_color (color_line, sizeof(color_line), color); + sp_repr_css_set_property (css, "stroke", color_line); + } else { + sp_repr_css_set_property (css, "stroke", "#ff0000"); + } + char const * stroke_linecap = is_curve ? "butt" : "square"; + sp_repr_css_set_property (css, "stroke-linecap", stroke_linecap); + sp_repr_css_set_property (css, "stroke-linejoin", "miter"); + sp_repr_css_set_property (css, "stroke-miterlimit", "4"); + sp_repr_css_set_property (css, "stroke-dasharray", "none"); + if(measure_repr) { + sp_repr_css_set_property (css, "stroke-opacity", "0.5"); + } else { + sp_repr_css_set_property (css, "stroke-opacity", "1"); + } + if(markers) { + sp_repr_css_set_property (css, "marker-start", "url(#Arrow2Sstart)"); + sp_repr_css_set_property (css, "marker-end", "url(#Arrow2Send)"); + } + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + repr->setAttribute("style", css_str); + sp_repr_css_attr_unref (css); + repr->setAttribute("d", str); + if(measure_repr) { + measure_repr->addChild(repr, nullptr); + Inkscape::GC::release(repr); + } else { + SPItem *item = SP_ITEM(layer->appendChildRepr(repr)); + Inkscape::GC::release(repr); + item->updateRepr(); + _desktop->getSelection()->clear(); + _desktop->getSelection()->add(item); + } +} +} +} +} + +/* + 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/tools/measure-tool.h b/src/ui/tools/measure-tool.h new file mode 100644 index 0000000..bdaf53a --- /dev/null +++ b/src/ui/tools/measure-tool.h @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MEASURING_CONTEXT_H +#define SEEN_SP_MEASURING_CONTEXT_H + +/* + * Our fine measuring tool + * + * Authors: + * Felipe Correa da Silva Sanches <juca@members.fsf.org> + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * Copyright (C) 2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <boost/optional.hpp> +#include <optional> + +#include <sigc++/sigc++.h> + +#include <2geom/point.h> + +#include "ui/tools/tool-base.h" + +#include "display/control/canvas-temporary-item.h" +#include "display/control/canvas-item-enums.h" + +class SPKnot; + +namespace Inkscape { + +class CanvasItemCurve; + +namespace UI { +namespace Tools { + +class MeasureTool : public ToolBase { +public: + MeasureTool(SPDesktop *desktop); + ~MeasureTool() override; + + bool root_handler(GdkEvent* event) override; + virtual void showCanvasItems(bool to_guides = false, bool to_item = false, bool to_phantom = false, Inkscape::XML::Node *measure_repr = nullptr); + virtual void reverseKnots(); + virtual void toGuides(); + virtual void toPhantom(); + virtual void toMarkDimension(); + virtual void toItem(); + virtual void reset(); + virtual void setMarkers(); + virtual void setMarker(bool isStart); + Geom::Point readMeasurePoint(bool is_start); + + void showInfoBox(Geom::Point cursor, bool into_groups); + void showItemInfoText(Geom::Point pos, Glib::ustring const &measure_str, double fontsize); + void writeMeasurePoint(Geom::Point point, bool is_start); + void setGuide(Geom::Point origin, double angle, const char *label); + void setPoint(Geom::Point origin, Inkscape::XML::Node *measure_repr); + void setLine(Geom::Point start_point,Geom::Point end_point, bool markers, guint32 color, + Inkscape::XML::Node *measure_repr = nullptr); + void setMeasureCanvasText(bool is_angle, double precision, double amount, double fontsize, + Glib::ustring unit_name, Geom::Point position, guint32 background, + bool to_left, bool to_item, bool to_phantom, + Inkscape::XML::Node *measure_repr); + void setMeasureCanvasItem(Geom::Point position, bool to_item, bool to_phantom, + Inkscape::XML::Node *measure_repr); + void setMeasureCanvasControlLine(Geom::Point start, Geom::Point end, bool to_item, bool to_phantom, + Inkscape::CanvasItemColor color, Inkscape::XML::Node *measure_repr); + void setLabelText(Glib::ustring const &value, Geom::Point pos, double fontsize, Geom::Coord angle, + guint32 background, + Inkscape::XML::Node *measure_repr = nullptr); + + void knotStartMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state); + void knotEndMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state); + void knotClickHandler(SPKnot *knot, guint state); + void knotUngrabbedHandler(SPKnot */*knot*/, unsigned int /*state*/); + void setMeasureItem(Geom::PathVector pathv, bool is_curve, bool markers, guint32 color, Inkscape::XML::Node *measure_repr); + void createAngleDisplayCurve(Geom::Point const ¢er, Geom::Point const &end, Geom::Point const &anchor, + double angle, bool to_phantom, + std::vector<Inkscape::CanvasItem *> &measure_phantom_items, + std::vector<Inkscape::CanvasItem *> &measure_tmp_items, + Inkscape::XML::Node *measure_repr = nullptr); + +private: + std::optional<Geom::Point> explicit_base; + std::optional<Geom::Point> last_end; + SPKnot *knot_start = nullptr; + SPKnot *knot_end = nullptr; + gint dimension_offset = 20; + Geom::Point start_p; + Geom::Point end_p; + Geom::Point last_pos; + + std::vector<Inkscape::CanvasItem *> measure_tmp_items; + std::vector<Inkscape::CanvasItem *> measure_phantom_items; + std::vector<Inkscape::CanvasItem *> measure_item; + + double item_width; + double item_height; + double item_x; + double item_y; + double item_length; + SPItem *over; + sigc::connection _knot_start_moved_connection; + sigc::connection _knot_start_ungrabbed_connection; + sigc::connection _knot_start_click_connection; + sigc::connection _knot_end_moved_connection; + sigc::connection _knot_end_click_connection; + sigc::connection _knot_end_ungrabbed_connection; +}; + +} +} +} + +#endif // SEEN_SP_MEASURING_CONTEXT_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/tools/mesh-tool.cpp b/src/ui/tools/mesh-tool.cpp new file mode 100644 index 0000000..deff50c --- /dev/null +++ b/src/ui/tools/mesh-tool.cpp @@ -0,0 +1,973 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Mesh drawing and editing tool + * + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Abhishek Sharma + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2012 Tavmjong Bah + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +//#define DEBUG_MESH + +#include "mesh-tool.h" + +// Libraries +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +// General +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "gradient-drag.h" +#include "gradient-chemistry.h" +#include "include/macros.h" +#include "message-context.h" +#include "message-stack.h" +#include "rubberband.h" +#include "selection.h" +#include "snap.h" + +#include "display/control/canvas-item-curve.h" +#include "display/curve.h" + +#include "object/sp-defs.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-namedview.h" +#include "object/sp-text.h" +#include "style.h" + +#include "ui/icon-names.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +// TODO: The gradient tool class looks like a 1:1 copy. + +MeshTool::MeshTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/mesh", "mesh.svg") +// TODO: Why are these connections stored as pointers? + , selcon(nullptr) + , subselcon(nullptr) + , cursor_addnode(false) + , show_handles(true) + , edit_fill(true) + , edit_stroke(true) +{ + // TODO: This value is overwritten in the root handler + this->tolerance = 6; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/mesh/selcue", true)) { + this->enableSelectionCue(); + } + + this->enableGrDrag(); + Inkscape::Selection *selection = desktop->getSelection(); + + this->selcon = new sigc::connection(selection->connectChanged( + sigc::mem_fun(this, &MeshTool::selection_changed) + )); + + this->subselcon = new sigc::connection(desktop->connectToolSubselectionChanged( + sigc::hide(sigc::bind( + sigc::mem_fun(*this, &MeshTool::selection_changed), + (Inkscape::Selection*)nullptr) + ) + )); + + sp_event_context_read(this, "show_handles"); + sp_event_context_read(this, "edit_fill"); + sp_event_context_read(this, "edit_stroke"); + + this->selection_changed(selection); +} + +MeshTool::~MeshTool() { + this->enableGrDrag(false); + + this->selcon->disconnect(); + delete this->selcon; + + this->subselcon->disconnect(); + delete this->subselcon; +} + +// This must match GrPointType enum sp-gradient.h +// We should move this to a shared header (can't simply move to gradient.h since that would require +// including <glibmm/i18n.h> which messes up "N_" in extensions... argh!). +const gchar *ms_handle_descr [] = { + N_("Linear gradient <b>start</b>"), //POINT_LG_BEGIN + N_("Linear gradient <b>end</b>"), + N_("Linear gradient <b>mid stop</b>"), + N_("Radial gradient <b>center</b>"), + N_("Radial gradient <b>radius</b>"), + N_("Radial gradient <b>radius</b>"), + N_("Radial gradient <b>focus</b>"), // POINT_RG_FOCUS + N_("Radial gradient <b>mid stop</b>"), + N_("Radial gradient <b>mid stop</b>"), + N_("Mesh gradient <b>corner</b>"), + N_("Mesh gradient <b>handle</b>"), + N_("Mesh gradient <b>tensor</b>") +}; + +void MeshTool::selection_changed(Inkscape::Selection* /*sel*/) { + Inkscape::Selection *selection = _desktop->getSelection(); + + if (selection == nullptr) { + return; + } + + guint n_obj = (guint) boost::distance(selection->items()); + + if (!_grdrag->isNonEmpty() || selection->isEmpty()) { + return; + } + + guint n_tot = _grdrag->numDraggers(); + guint n_sel = _grdrag->numSelected(); + + //The use of ngettext in the following code is intentional even if the English singular form would never be used + if (n_sel == 1) { + if (_grdrag->singleSelectedDraggerNumDraggables() == 1) { + gchar * message = g_strconcat( + //TRANSLATORS: %s will be substituted with the point name (see previous messages); This is part of a compound message + _("%s selected"), + //TRANSLATORS: Mind the space in front. This is part of a compound message + ngettext(" out of %d mesh handle"," out of %d mesh handles",n_tot), + ngettext(" on %d selected object"," on %d selected objects",n_obj),nullptr); + this->message_context->setF(Inkscape::NORMAL_MESSAGE, message, + _(ms_handle_descr[_grdrag->singleSelectedDraggerSingleDraggableType()]), n_tot, n_obj); + } else { + gchar * message = + g_strconcat( + //TRANSLATORS: This is a part of a compound message (out of two more indicating: grandint handle count & object count) + ngettext("One handle merging %d stop (drag with <b>Shift</b> to separate) selected", + "One handle merging %d stops (drag with <b>Shift</b> to separate) selected", + _grdrag->singleSelectedDraggerNumDraggables()), + ngettext(" out of %d mesh handle"," out of %d mesh handles",n_tot), + ngettext(" on %d selected object"," on %d selected objects",n_obj),nullptr); + this->message_context->setF(Inkscape::NORMAL_MESSAGE, message, _grdrag->singleSelectedDraggerNumDraggables(), n_tot, n_obj); + } + } else if (n_sel > 1) { + //TRANSLATORS: The plural refers to number of selected mesh handles. This is part of a compound message (part two indicates selected object count) + gchar * message = + g_strconcat(ngettext("<b>%d</b> mesh handle selected out of %d","<b>%d</b> mesh handles selected out of %d",n_sel), + //TRANSLATORS: Mind the space in front. (Refers to gradient handles selected). This is part of a compound message + ngettext(" on %d selected object"," on %d selected objects",n_obj),nullptr); + this->message_context->setF(Inkscape::NORMAL_MESSAGE, message, n_sel, n_tot, n_obj); + } else if (n_sel == 0) { + this->message_context->setF(Inkscape::NORMAL_MESSAGE, + //TRANSLATORS: The plural refers to number of selected objects + ngettext("<b>No</b> mesh handles selected out of %d on %d selected object", + "<b>No</b> mesh handles selected out of %d on %d selected objects",n_obj), n_tot, n_obj); + } + + // FIXME + // We need to update mesh gradient handles. + // Get gradient this drag belongs too.. +} + +void MeshTool::set(const Inkscape::Preferences::Entry& value) { + Glib::ustring entry_name = value.getEntryName(); + if (entry_name == "show_handles") { + this->show_handles = value.getBool(true); + } else if (entry_name == "edit_fill") { + this->edit_fill = value.getBool(true); + } else if (entry_name == "edit_stroke") { + this->edit_stroke = value.getBool(true); + } else { + ToolBase::set(value); + } +} + +void MeshTool::select_next() +{ + g_assert(_grdrag); + GrDragger *d = _grdrag->select_next(); + _desktop->scroll_to_point(d->point, 1.0); +} + +void MeshTool::select_prev() +{ + g_assert(_grdrag); + GrDragger *d = _grdrag->select_prev(); + _desktop->scroll_to_point(d->point, 1.0); +} + +/** + * Returns vector of control curves mouse is over. Returns only first if 'first' is true. + * event_p is in canvas (world) units. + */ +std::vector<CanvasItemCurve *> MeshTool::over_curve(Geom::Point event_p, bool first) +{ + //Translate mouse point into proper coord system: needed later. + mousepoint_doc = _desktop->w2d(event_p); + std::vector<CanvasItemCurve *> selected; + + for (auto curve : _grdrag->item_curves) { + if (curve->contains(event_p, tolerance)) { + selected.push_back(&*curve); + if (first) { + break; + } + } + } + return selected; +} + + +/** +Split row/column near the mouse point. +*/ +void MeshTool::split_near_point(SPItem *item, Geom::Point mouse_p, guint32 /*etime*/) +{ +#ifdef DEBUG_MESH + std::cout << "split_near_point: entrance: " << mouse_p << std::endl; +#endif + + // item is the selected item. mouse_p the location in doc coordinates of where to add the stop + get_drag()->addStopNearPoint(item, mouse_p, tolerance / _desktop->current_zoom()); + DocumentUndo::done(_desktop->getDocument(), _("Split mesh row/column"), INKSCAPE_ICON("mesh-gradient")); + get_drag()->updateDraggers(); +} + +/** +Wrapper for various mesh operations that require a list of selected corner nodes. + */ +void MeshTool::corner_operation(MeshCornerOperation operation) +{ + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_corner_operation: entrance: " << operation << std::endl; +#endif + + SPDocument *doc = nullptr; + + std::map<SPMeshGradient*, std::vector<guint> > points; + std::map<SPMeshGradient*, SPItem*> items; + std::map<SPMeshGradient*, Inkscape::PaintTarget> fill_or_stroke; + + // Get list of selected draggers for each mesh. + // For all selected draggers (a dragger may include draggerables from different meshes). + for (auto dragger : _grdrag->selected) { + // For all draggables of dragger (a draggable corresponds to a unique mesh). + for (auto d : dragger->draggables) { + // Only mesh corners + if( d->point_type != POINT_MG_CORNER ) continue; + + // Find the gradient + SPMeshGradient *gradient = SP_MESHGRADIENT( getGradient (d->item, d->fill_or_stroke) ); + + // Collect points together for same gradient + points[gradient].push_back( d->point_i ); + items[gradient] = d->item; + fill_or_stroke[gradient] = d->fill_or_stroke ? Inkscape::FOR_FILL: Inkscape::FOR_STROKE; + } + } + + // Loop over meshes. + for( std::map<SPMeshGradient*, std::vector<guint> >::const_iterator iter = points.begin(); iter != points.end(); ++iter) { + SPMeshGradient *mg = iter->first; + if( iter->second.size() > 0 ) { + guint noperation = 0; + switch (operation) { + + case MG_CORNER_SIDE_TOGGLE: + // std::cout << "SIDE_TOGGLE" << std::endl; + noperation += mg->array.side_toggle( iter->second ); + break; + + case MG_CORNER_SIDE_ARC: + // std::cout << "SIDE_ARC" << std::endl; + noperation += mg->array.side_arc( iter->second ); + break; + + case MG_CORNER_TENSOR_TOGGLE: + // std::cout << "TENSOR_TOGGLE" << std::endl; + noperation += mg->array.tensor_toggle( iter->second ); + break; + + case MG_CORNER_COLOR_SMOOTH: + // std::cout << "COLOR_SMOOTH" << std::endl; + noperation += mg->array.color_smooth( iter->second ); + break; + + case MG_CORNER_COLOR_PICK: + // std::cout << "COLOR_PICK" << std::endl; + noperation += mg->array.color_pick( iter->second, items[iter->first] ); + break; + + case MG_CORNER_INSERT: + // std::cout << "INSERT" << std::endl; + noperation += mg->array.insert( iter->second ); + break; + + default: + std::cout << "sp_mesh_corner_operation: unknown operation" << std::endl; + } + + if( noperation > 0 ) { + mg->array.write( mg ); + mg->requestModified(SP_OBJECT_MODIFIED_FLAG); + doc = mg->document; + + switch (operation) { + + case MG_CORNER_SIDE_TOGGLE: + DocumentUndo::done(doc, _("Toggled mesh path type."), INKSCAPE_ICON("mesh-gradient")); + _grdrag->local_change = true; // Don't create new draggers. + break; + + case MG_CORNER_SIDE_ARC: + DocumentUndo::done(doc, _("Approximated arc for mesh side."), INKSCAPE_ICON("mesh-gradient")); + _grdrag->local_change = true; // Don't create new draggers. + break; + + case MG_CORNER_TENSOR_TOGGLE: + DocumentUndo::done(doc, _("Toggled mesh tensors."), INKSCAPE_ICON("mesh-gradient")); + _grdrag->local_change = true; // Don't create new draggers. + break; + + case MG_CORNER_COLOR_SMOOTH: + DocumentUndo::done(doc, _("Smoothed mesh corner color."), INKSCAPE_ICON("mesh-gradient")); + _grdrag->local_change = true; // Don't create new draggers. + break; + + case MG_CORNER_COLOR_PICK: + DocumentUndo::done(doc, _("Picked mesh corner color."), INKSCAPE_ICON("mesh-gradient")); + _grdrag->local_change = true; // Don't create new draggers. + break; + + case MG_CORNER_INSERT: + DocumentUndo::done(doc, _("Inserted new row or column."), INKSCAPE_ICON("mesh-gradient")); + break; + + default: + std::cout << "sp_mesh_corner_operation: unknown operation" << std::endl; + } + } + } + } +} + + +/** + * Scale mesh to just fit into bbox of selected items. + */ +void MeshTool::fit_mesh_in_bbox() +{ + +#ifdef DEBUG_MESH + std::cout << "fit_mesh_in_bbox: entrance: Entrance" << std::endl; +#endif + + Inkscape::Selection *selection = _desktop->getSelection(); + if (selection == nullptr) { + return; + } + + bool changed = false; + auto itemlist = selection->items(); + for (auto i=itemlist.begin(); i!=itemlist.end(); ++i) { + + SPItem *item = *i; + SPStyle *style = item->style; + + if (style) { + + if (style->fill.isPaintserver()) { + SPPaintServer *server = item->style->getFillPaintServer(); + if ( SP_IS_MESHGRADIENT(server) ) { + + Geom::OptRect item_bbox = item->geometricBounds(); + SPMeshGradient *gradient = SP_MESHGRADIENT(server); + if (gradient->array.fill_box( item_bbox )) { + changed = true; + } + } + } + + if (style->stroke.isPaintserver()) { + SPPaintServer *server = item->style->getStrokePaintServer(); + if ( SP_IS_MESHGRADIENT(server) ) { + + Geom::OptRect item_bbox = item->visualBounds(); + SPMeshGradient *gradient = SP_MESHGRADIENT(server); + if (gradient->array.fill_box( item_bbox )) { + changed = true; + } + } + } + + } + } + if (changed) { + DocumentUndo::done(_desktop->getDocument(), _("Fit mesh inside bounding box"), INKSCAPE_ICON("mesh-gradient")); + } +} + + +/** +Handles all keyboard and mouse input for meshs. +Note: node/handle events are take care of elsewhere. +*/ +bool MeshTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = _desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + // Get value of fill or stroke preference + Inkscape::PaintTarget fill_or_stroke_pref = + static_cast<Inkscape::PaintTarget>(prefs->getInt("/tools/mesh/newfillorstroke")); + + g_assert(_grdrag); + gint ret = FALSE; + + switch (event->type) { + case GDK_2BUTTON_PRESS: + +#ifdef DEBUG_MESH + std::cout << "root_handler: GDK_2BUTTON_PRESS" << std::endl; +#endif + + // Double click: + // If over a mesh line, divide mesh row/column + // If not over a line and no mesh, create new mesh for top selected object. + + if ( event->button.button == 1 ) { + + // Are we over a mesh line? (Should replace by CanvasItem event.) + auto over_curve = this->over_curve(Geom::Point(event->motion.x, event->motion.y)); + + if (!over_curve.empty()) { + // We take the first item in selection, because with doubleclick, the first click + // always resets selection to the single object under cursor + split_near_point(selection->items().front(), this->mousepoint_doc, event->button.time); + } else { + // Create a new gradient with default coordinates. + + // Check if object already has mesh... if it does, + // don't create new mesh with click-drag. + bool has_mesh = false; + if (!selection->isEmpty()) { + SPStyle *style = selection->items().front()->style; + if (style) { + SPPaintServer *server = + (fill_or_stroke_pref == Inkscape::FOR_FILL) ? + style->getFillPaintServer(): + style->getStrokePaintServer(); + if (server && SP_IS_MESHGRADIENT(server)) + has_mesh = true; + } + } + + if (!has_mesh) { + new_default(); + } + } + + ret = TRUE; + } + break; + + case GDK_BUTTON_PRESS: + +#ifdef DEBUG_MESH + std::cout << "root_handler: GDK_BUTTON_PRESS" << std::endl; +#endif + + // Button down + // If mesh already exists, do rubber band selection. + // Else set origin for drag which will create a new gradient. + if ( event->button.button == 1 ) { + + // Are we over a mesh curve? + auto over_curve = this->over_curve(Geom::Point(event->motion.x, event->motion.y), false); + + if (!over_curve.empty()) { + for (auto it : over_curve) { + SPItem *item = it->get_item(); + Inkscape::PaintTarget fill_or_stroke = + it->get_is_fill() ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + GrDragger* dragger0 = _grdrag->getDraggerFor(item, POINT_MG_CORNER, it->get_corner0(), fill_or_stroke); + GrDragger* dragger1 = _grdrag->getDraggerFor(item, POINT_MG_CORNER, it->get_corner1(), fill_or_stroke); + bool add = (event->button.state & GDK_SHIFT_MASK); + bool toggle = (event->button.state & GDK_CONTROL_MASK); + if ( !add && !toggle ) { + _grdrag->deselectAll(); + } + _grdrag->setSelected( dragger0, true, !toggle ); + _grdrag->setSelected( dragger1, true, !toggle ); + } + ret = true; + break; // To avoid putting the following code in an else block. + } + + Geom::Point button_w(event->button.x, event->button.y); + + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + dragging = true; + + Geom::Point button_dt = _desktop->w2d(button_w); + // Check if object already has mesh... if it does, + // don't create new mesh with click-drag. + bool has_mesh = false; + if (!selection->isEmpty()) { + SPStyle *style = selection->items().front()->style; + if (style) { + SPPaintServer *server = + (fill_or_stroke_pref == Inkscape::FOR_FILL) ? + style->getFillPaintServer(): + style->getStrokePaintServer(); + if (server && SP_IS_MESHGRADIENT(server)) + has_mesh = true; + } + } + + if (has_mesh) { + Inkscape::Rubberband::get(_desktop)->start(_desktop, button_dt); + } + + // remember clicked item, disregarding groups, honoring Alt; do nothing with Crtl to + // enable Ctrl+doubleclick of exactly the selected item(s) + if (!(event->button.state & GDK_CONTROL_MASK)) { + this->item_to_select = sp_event_context_find_item (_desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + } + + if (!selection->isEmpty()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + } + + this->origin = button_dt; + + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + // Mouse move + if ( dragging && ( event->motion.state & GDK_BUTTON1_MASK ) ) { + +#ifdef DEBUG_MESH + std::cout << "root_handler: GDK_MOTION_NOTIFY: Dragging" << std::endl; +#endif + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->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 draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point const motion_dt = _desktop->w2d(motion_w); + + if (Inkscape::Rubberband::get(_desktop)->is_started()) { + Inkscape::Rubberband::get(_desktop)->move(motion_dt); + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw around</b> handles to select them")); + } else { + // Do nothing. For a linear/radial gradient we follow the drag, updating the + // gradient as the end node is dragged. For a mesh gradient, the gradient is always + // created to fill the object when the drag ends. + } + + gobble_motion_events(GDK_BUTTON1_MASK); + + ret = TRUE; + } else { + // Not dragging + + // Do snapping + if (!_grdrag->mouseOver() && !selection->isEmpty()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt = _desktop->w2d(motion_w); + + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + + // Highlight corner node corresponding to side or tensor node + if (_grdrag->mouseOver()) { + // MESH FIXME: Light up corresponding corner node corresponding to node we are over. + // See "pathflash" in ui/tools/node-tool.cpp for ideas. + // Use _desktop->add_temporary_canvasitem( SPCanvasItem, milliseconds ); + } + + // Change cursor shape if over line + auto over_curve = this->over_curve(Geom::Point(event->motion.x, event->motion.y)); + + if (this->cursor_addnode && over_curve.empty()) { + this->set_cursor("mesh.svg"); + this->cursor_addnode = false; + } else if (!this->cursor_addnode && !over_curve.empty()) { + this->set_cursor("mesh-add.svg"); + this->cursor_addnode = true; + } + } + break; + + case GDK_BUTTON_RELEASE: + +#ifdef DEBUG_MESH + std::cout << "root_handler: GDK_BUTTON_RELEASE" << std::endl; +#endif + + this->xp = this->yp = 0; + + if ( event->button.button == 1 ) { + + // Check if over line + auto over_curve = this->over_curve(Geom::Point(event->motion.x, event->motion.y)); + + if ( (event->button.state & GDK_CONTROL_MASK) && (event->button.state & GDK_MOD1_MASK ) ) { + if (!over_curve.empty()) { + split_near_point(over_curve[0]->get_item(), this->mousepoint_doc, 0); + ret = TRUE; + } + } else { + dragging = false; + + // unless clicked with Ctrl (to enable Ctrl+doubleclick). + if (event->button.state & GDK_CONTROL_MASK) { + ret = TRUE; + break; + } + + if (!this->within_tolerance) { + + // Check if object already has mesh... if it does, + // don't create new mesh with click-drag. + bool has_mesh = false; + if (!selection->isEmpty()) { + SPStyle *style = selection->items().front()->style; + if (style) { + SPPaintServer *server = + (fill_or_stroke_pref == Inkscape::FOR_FILL) ? + style->getFillPaintServer(): + style->getStrokePaintServer(); + if (server && SP_IS_MESHGRADIENT(server)) + has_mesh = true; + } + } + + if (!has_mesh) { + new_default(); + } else { + + // we've been dragging, either create a new gradient + // or rubberband-select if we have rubberband + Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop); + + if (r->is_started() && !this->within_tolerance) { + // this was a rubberband drag + if (r->getMode() == RUBBERBAND_MODE_RECT) { + Geom::OptRect const b = r->getRectangle(); + if (!(event->button.state & GDK_SHIFT_MASK)) { + _grdrag->deselectAll(); + } + _grdrag->selectRect(*b); + } + } + } + + } else if (this->item_to_select) { + if (!over_curve.empty()) { + // Clicked on an existing mesh line, don't change selection. This stops + // possible change in selection during a double click with overlapping objects + } else { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + _grdrag->deselectAll(); + selection->set(this->item_to_select); + } + } + } else { + if (!over_curve.empty()) { + // Clicked on an existing mesh line, don't change selection. This stops + // possible change in selection during a double click with overlapping objects + } else { + // click in an empty space; do the same as Esc + if (!_grdrag->selected.empty()) { + _grdrag->deselectAll(); + } else { + selection->clear(); + } + } + } + + this->item_to_select = nullptr; + ret = TRUE; + } + + Inkscape::Rubberband::get(_desktop)->stop(); + } + break; + + case GDK_KEY_PRESS: + +#ifdef DEBUG_MESH + std::cout << "root_handler: GDK_KEY_PRESS" << std::endl; +#endif + + // FIXME: tip + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + + // sp_event_show_modifier_tip (this->defaultMessageContext(), event, + // _("FIXME<b>Ctrl</b>: snap mesh angle"), + // _("FIXME<b>Shift</b>: draw mesh around the starting point"), + // NULL); + break; + + case GDK_KEY_A: + case GDK_KEY_a: + if (MOD__CTRL_ONLY(event) && _grdrag->isNonEmpty()) { + _grdrag->selectAll(); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (!_grdrag->selected.empty()) { + _grdrag->deselectAll(); + } else { + selection->clear(); + } + + ret = TRUE; + //TODO: make dragging escapable by Esc + break; + + // Mesh Operations -------------------------------------------- + + case GDK_KEY_Insert: + case GDK_KEY_KP_Insert: + // with any modifiers: + this->corner_operation(MG_CORNER_INSERT); + ret = TRUE; + break; + + case GDK_KEY_i: + case GDK_KEY_I: + if (MOD__SHIFT_ONLY(event)) { + // Shift+I - insert corners (alternate keybinding for keyboards + // that don't have the Insert key) + this->corner_operation(MG_CORNER_INSERT); + ret = TRUE; + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + if (!_grdrag->selected.empty()) { + ret = TRUE; + } + break; + + case GDK_KEY_b: // Toggle mesh side between lineto and curveto. + case GDK_KEY_B: + if (MOD__ALT(event) && _grdrag->isNonEmpty() && _grdrag->hasSelection()) { + this->corner_operation(MG_CORNER_SIDE_TOGGLE); + ret = TRUE; + } + break; + + case GDK_KEY_c: // Convert mesh side from generic Bezier to Bezier approximating arc, + case GDK_KEY_C: // preserving handle direction. + if (MOD__ALT(event) && _grdrag->isNonEmpty() && _grdrag->hasSelection()) { + this->corner_operation(MG_CORNER_SIDE_ARC); + ret = TRUE; + } + break; + + case GDK_KEY_g: // Toggle mesh tensor points on/off + case GDK_KEY_G: + if (MOD__ALT(event) && _grdrag->isNonEmpty() && _grdrag->hasSelection()) { + this->corner_operation(MG_CORNER_TENSOR_TOGGLE); + ret = TRUE; + } + break; + + case GDK_KEY_j: // Smooth corner color + case GDK_KEY_J: + if (MOD__ALT(event) && _grdrag->isNonEmpty() && _grdrag->hasSelection()) { + this->corner_operation(MG_CORNER_COLOR_SMOOTH); + ret = TRUE; + } + break; + + case GDK_KEY_k: // Pick corner color + case GDK_KEY_K: + if (MOD__ALT(event) && _grdrag->isNonEmpty() && _grdrag->hasSelection()) { + this->corner_operation(MG_CORNER_COLOR_PICK); + ret = TRUE; + } + break; + + default: + ret = _grdrag->key_press_handler(event); + break; + } + + break; + + case GDK_KEY_RELEASE: + +#ifdef DEBUG_MESH + std::cout << "root_handler: GDK_KEY_RELEASE" << std::endl; +#endif + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + default: + break; + } + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +// Creates a new mesh gradient. +void MeshTool::new_default() +{ + Inkscape::Selection *selection = _desktop->getSelection(); + SPDocument *document = _desktop->getDocument(); + + if (!selection->isEmpty()) { + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::PaintTarget fill_or_stroke_pref = + static_cast<Inkscape::PaintTarget>(prefs->getInt("/tools/mesh/newfillorstroke")); + + // Ensure mesh is immediately editable. + // Editing both fill and stroke at same time doesn't work well so avoid. + if (fill_or_stroke_pref == Inkscape::FOR_FILL) { + prefs->setBool("/tools/mesh/edit_fill", true ); + prefs->setBool("/tools/mesh/edit_stroke", false); + } else { + prefs->setBool("/tools/mesh/edit_fill", false); + prefs->setBool("/tools/mesh/edit_stroke", true ); + } + +// HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPDefs *defs = document->getDefs(); + + auto items= selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + + //FIXME: see above + sp_repr_css_change_recursive((*i)->getRepr(), css, "style"); + + // Create mesh element + Inkscape::XML::Node *repr = xml_doc->createElement("svg:meshgradient"); + + // privates are garbage-collectable + repr->setAttribute("inkscape:collect", "always"); + + // Attach to document + defs->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // Get corresponding object + SPMeshGradient *mg = static_cast<SPMeshGradient *>(document->getObjectByRepr(repr)); + mg->array.create(mg, *i, (fill_or_stroke_pref == Inkscape::FOR_FILL) ? + (*i)->geometricBounds() : (*i)->visualBounds()); + + bool isText = SP_IS_TEXT(*i); + sp_style_set_property_url(*i, + ((fill_or_stroke_pref == Inkscape::FOR_FILL) ? "fill":"stroke"), + mg, isText); + + (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG|SP_OBJECT_STYLE_MODIFIED_FLAG); + } + + if (css) { + sp_repr_css_attr_unref(css); + css = nullptr; + } + + DocumentUndo::done(_desktop->getDocument(), _("Create mesh"), INKSCAPE_ICON("mesh-gradient")); + + // status text; we do not track coords because this branch is run once, not all the time + // during drag + int n_objects = (int) boost::distance(selection->items()); + message_context->setF(Inkscape::NORMAL_MESSAGE, + ngettext("<b>Gradient</b> for %d object; with <b>Ctrl</b> to snap angle", + "<b>Gradient</b> for %d objects; with <b>Ctrl</b> to snap angle", n_objects), + n_objects); + } else { + _desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>objects</b> on which to create gradient.")); + } +} + +} +} +} + +/* + 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/tools/mesh-tool.h b/src/ui/tools/mesh-tool.h new file mode 100644 index 0000000..de13eb1 --- /dev/null +++ b/src/ui/tools/mesh-tool.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MESH_CONTEXT_H +#define SEEN_SP_MESH_CONTEXT_H + +/* + * Mesh drawing and editing tool + * + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org. + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2012 Tavmjong Bah + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005,2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include "ui/tools/tool-base.h" + +#include "object/sp-mesh-array.h" + +#define SP_MESH_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::MeshTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_MESH_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::MeshTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +class GrDrag; + +namespace Inkscape { + +class Selection; +class CanvasItemCurve; + +namespace UI { +namespace Tools { + +class MeshTool : public ToolBase { +public: + MeshTool(SPDesktop *desktop); + ~MeshTool() override; + + Geom::Point origin; + + Geom::Point mousepoint_doc; // stores mousepoint when over_line in doc coords + + sigc::connection *selcon; + sigc::connection *subselcon; + + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + void fit_mesh_in_bbox(); + void corner_operation(MeshCornerOperation operation); + +private: + bool cursor_addnode; + bool show_handles; + bool edit_fill; + bool edit_stroke; + + void selection_changed(Inkscape::Selection *sel); + void select_next(); + void select_prev(); + void new_default(); + void split_near_point(SPItem *item, Geom::Point mouse_p, guint32 /*etime*/); + std::vector<CanvasItemCurve *> over_curve(Geom::Point event_p, bool first = true); +}; + +} +} +} + +#endif // SEEN_SP_MESH_CONTEXT_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/tools/node-tool.cpp b/src/ui/tools/node-tool.cpp new file mode 100644 index 0000000..92b6e27 --- /dev/null +++ b/src/ui/tools/node-tool.cpp @@ -0,0 +1,808 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * New node tool - implementation. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iomanip> + +#include <glibmm/ustring.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + + + +#include "desktop.h" +#include "document.h" +#include "message-context.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "snap.h" + +#include "display/curve.h" +#include "display/control/canvas-item-bpath.h" +#include "display/control/canvas-item-group.h" + +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" + +#include "include/macros.h" + +#include "object/sp-clippath.h" +#include "object/sp-item-group.h" +#include "object/sp-mask.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "ui/shape-editor.h" // temporary! +#include "ui/tool/control-point-selection.h" +#include "ui/tool/curve-drag-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tool/selector.h" +#include "ui/tools/node-tool.h" + +/** @struct NodeTool + * + * Node tool event context. + * + * @par Architectural overview of the tool + * @par + * Here's a breakdown of what each object does. + * - Handle: shows a handle and keeps the node type constraint (smooth / symmetric) by updating + * the other handle's position when dragged. Its move() method cannot violate the constraints. + * - Node: keeps node type constraints for auto nodes and smooth nodes at ends of linear segments. + * Its move() method cannot violate constraints. Handles linear grow and dispatches spatial grow + * to MultiPathManipulator. Keeps a reference to its NodeList. + * - NodeList: exposes an iterator-based interface to nodes. It is possible to obtain an iterator + * to a node from the node. Keeps a reference to its SubpathList. + * - SubpathList: list of NodeLists that represents an editable pathvector. Keeps a reference + * to its PathManipulator. + * - PathManipulator: performs most of the single-path actions like reverse subpaths, + * delete segment, shift selection, etc. Keeps a reference to MultiPathManipulator. + * - MultiPathManipulator: performs additional operations for actions that are not per-path, + * for example node joins and segment joins. Tracks the control transforms for PMs that edit + * clipping paths and masks. It is more or less equivalent to ShapeEditor and in the future + * it might handle all shapes. Handles XML commit of actions that affect all paths or + * the node selection and removes PathManipulators that have no nodes left after e.g. node + * deletes. + * - ControlPointSelection: keeps track of node selection and a set of nodes that can potentially + * be selected. There can be more than one selection. Performs actions that require no + * knowledge about the path, only about the nodes, like dragging and transforms. It is not + * specific to nodes and can accommodate any control point derived from SelectableControlPoint. + * Transforms nodes in response to transform handle events. + * - TransformHandleSet: displays nodeset transform handles and emits transform events. The aim + * is to eventually use a common class for object and control point transforms. + * - SelectableControlPoint: base for any type of selectable point. It can belong to only one + * selection. + * + * @par Functionality that resides in weird places + * @par + * + * This list is probably incomplete. + * - Curve dragging: CurveDragPoint, controlled by PathManipulator + * - Single handle shortcuts: MultiPathManipulator::event(), ModifierTracker + * - Linear and spatial grow: Node, spatial grow routed to ControlPointSelection + * - Committing handle actions performed with the mouse: PathManipulator + * - Sculpting: ControlPointSelection + * + * @par Plans for the future + * @par + * - MultiPathManipulator should become a generic shape editor that manages all active manipulator, + * more or less like the old ShapeEditor. + * - Knotholder should be rewritten into one manipulator class per shape, using the control point + * classes. Interesting features like dragging rectangle sides could be added along the way. + * - Better handling of clip and mask editing, particularly in response to undo. + * - High level refactoring of the event context hierarchy. All aspects of tools, like toolbox + * controls, icons, event handling should be collected in one class, though each aspect + * of a tool might be in an separate class for better modularity. The long term goal is to allow + * tools to be defined in extensions or shared library plugins. + */ + + +namespace Inkscape { +namespace UI { +namespace Tools { + +Inkscape::CanvasItemGroup *create_control_group(SPDesktop *desktop) +{ + auto group = new Inkscape::CanvasItemGroup(desktop->getCanvasControls()); + group->set_name("CanvasItemGroup:NodeTool"); + return group; +} + +NodeTool::NodeTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/nodes", "node.svg") +{ + this->_path_data = new Inkscape::UI::PathSharedData(); + + Inkscape::UI::PathSharedData &data = *this->_path_data; + data.node_data.desktop = desktop; + + // selector has to be created here, so that its hidden control point is on the bottom + this->_selector = new Inkscape::UI::Selector(desktop); + + // Prepare canvas groups for controls. This guarantees correct z-order, so that + // for example a dragpoint won't obscure a node + data.outline_group = create_control_group(desktop); + data.node_data.handle_line_group = new Inkscape::CanvasItemGroup(desktop->getCanvasControls()); + data.dragpoint_group = create_control_group(desktop); + _transform_handle_group = create_control_group(desktop); + data.node_data.node_group = create_control_group(desktop); + data.node_data.handle_group = create_control_group(desktop); + + data.node_data.handle_line_group->set_name("CanvasItemGroup:NodeTool:handle_line_group"); + + Inkscape::Selection *selection = desktop->getSelection(); + + this->_selection_changed_connection.disconnect(); + this->_selection_changed_connection = + selection->connectChanged(sigc::mem_fun(this, &NodeTool::selection_changed)); + + this->_mouseover_changed_connection.disconnect(); + this->_mouseover_changed_connection = + Inkscape::UI::ControlPoint::signal_mouseover_change.connect(sigc::mem_fun(this, &NodeTool::mouseover_changed)); + + if (this->_transform_handle_group) { + this->_selected_nodes = new Inkscape::UI::ControlPointSelection(desktop, this->_transform_handle_group); + } + data.node_data.selection = this->_selected_nodes; + + this->_multipath = new Inkscape::UI::MultiPathManipulator(data, this->_selection_changed_connection); + + this->_selector->signal_point.connect(sigc::mem_fun(this, &NodeTool::select_point)); + this->_selector->signal_area.connect(sigc::mem_fun(this, &NodeTool::select_area)); + + this->_multipath->signal_coords_changed.connect([=](){ + desktop->emit_control_point_selected(this, _selected_nodes); + }); + + this->_selected_nodes->signal_selection_changed.connect( + // Hide both signal parameters and bind the function parameter to 0 + // sigc::signal<void, SelectableControlPoint *, bool> + // <=> + // void update_tip(GdkEvent *event) + sigc::hide(sigc::hide(sigc::bind( + sigc::mem_fun(this, &NodeTool::update_tip), + (GdkEvent*)nullptr + ))) + ); + + this->cursor_drag = false; + this->show_transform_handles = true; + this->single_node_transform_handles = false; + this->flash_tempitem = nullptr; + this->flashed_item = nullptr; + this->_last_over = nullptr; + + // read prefs before adding items to selection to prevent momentarily showing the outline + sp_event_context_read(this, "show_handles"); + sp_event_context_read(this, "show_outline"); + sp_event_context_read(this, "live_outline"); + sp_event_context_read(this, "live_objects"); + sp_event_context_read(this, "show_path_direction"); + sp_event_context_read(this, "show_transform_handles"); + sp_event_context_read(this, "single_node_transform_handles"); + sp_event_context_read(this, "edit_clipping_paths"); + sp_event_context_read(this, "edit_masks"); + + this->selection_changed(selection); + this->update_tip(nullptr); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/nodes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/nodes/gradientdrag")) { + this->enableGrDrag(); + } + + desktop->emit_control_point_selected(this, _selected_nodes); // sets the coord entry fields to inactive + sp_update_helperpath(desktop); +} + +NodeTool::~NodeTool() +{ + this->_selected_nodes->clear(); + + this->enableGrDrag(false); + + if (this->flash_tempitem) { + _desktop->remove_temporary_canvasitem(this->flash_tempitem); + } + for (auto hp : this->_helperpath_tmpitem) { + _desktop->remove_temporary_canvasitem(hp); + } + this->_selection_changed_connection.disconnect(); + // this->_selection_modified_connection.disconnect(); + this->_mouseover_changed_connection.disconnect(); + + delete this->_multipath; + delete this->_selected_nodes; + delete this->_selector; + + Inkscape::UI::PathSharedData &data = *this->_path_data; + delete data.node_data.node_group; + delete data.node_data.handle_group; + delete data.node_data.handle_line_group; + delete data.outline_group; + delete data.dragpoint_group; + delete _transform_handle_group; +} + +void NodeTool::deleteSelected() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // This takes care of undo internally + _multipath->deleteNodes(prefs->getBool("/tools/nodes/delete_preserves_shape", true)); +} + +// show helper paths of the applied LPE, if any +void sp_update_helperpath(SPDesktop *desktop) +{ + if (!desktop) { + return; + } + + Inkscape::UI::Tools::NodeTool *nt = dynamic_cast<Inkscape::UI::Tools::NodeTool*>(desktop->event_context); + if (!nt) { + // We remove this warning and just stop execution + // because we are updating helper paths also from LPE dialog so we not unsure the tool used + // std::cerr << "sp_update_helperpath called when Node Tool not active!" << std::endl; + return; + } + + Inkscape::Selection *selection = desktop->getSelection(); + for (auto hp : nt->_helperpath_tmpitem) { + desktop->remove_temporary_canvasitem(hp); + } + nt->_helperpath_tmpitem.clear(); + std::vector<SPItem *> vec(selection->items().begin(), selection->items().end()); + std::vector<std::pair<Geom::PathVector, Geom::Affine>> cs; + for (auto item : vec) { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem && lpeitem->hasPathEffectRecursive()) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + if (lpe && lpe->isVisible()/* && lpe->showOrigPath()*/) { + std::vector<Geom::Point> selectedNodesPositions; + if (nt->_selected_nodes) { + Inkscape::UI::ControlPointSelection *selectionNodes = nt->_selected_nodes; + for (auto selectionNode : *selectionNodes) { + Inkscape::UI::Node *n = dynamic_cast<Inkscape::UI::Node *>(selectionNode); + selectedNodesPositions.push_back(n->position()); + } + } + lpe->setSelectedNodePoints(selectedNodesPositions); + lpe->setCurrentZoom(desktop->current_zoom()); + auto c = std::make_unique<SPCurve>(); + std::vector<Geom::PathVector> cs = lpe->getCanvasIndicators(lpeitem); + for (auto &p : cs) { + p *= desktop->dt2doc(); + c->append(p); + } + if (!c->is_empty()) { + auto helperpath = new Inkscape::CanvasItemBpath(desktop->getCanvasTemp(), c.get(), true); + helperpath->set_stroke(0x0000ff9a); + helperpath->set_fill(0x0, SP_WIND_RULE_NONZERO); // No fill + + nt->_helperpath_tmpitem.emplace_back(desktop->add_temporary_canvasitem(helperpath, 0)); + } + } + } + } +} + +void NodeTool::set(const Inkscape::Preferences::Entry& value) { + Glib::ustring entry_name = value.getEntryName(); + + if (entry_name == "show_handles") { + this->show_handles = value.getBool(true); + this->_multipath->showHandles(this->show_handles); + } else if (entry_name == "show_outline") { + this->show_outline = value.getBool(); + this->_multipath->showOutline(this->show_outline); + } else if (entry_name == "live_outline") { + this->live_outline = value.getBool(); + this->_multipath->setLiveOutline(this->live_outline); + } else if (entry_name == "live_objects") { + this->live_objects = value.getBool(); + this->_multipath->setLiveObjects(this->live_objects); + } else if (entry_name == "show_path_direction") { + this->show_path_direction = value.getBool(); + this->_multipath->showPathDirection(this->show_path_direction); + } else if (entry_name == "show_transform_handles") { + this->show_transform_handles = value.getBool(true); + this->_selected_nodes->showTransformHandles( + this->show_transform_handles, this->single_node_transform_handles); + } else if (entry_name == "single_node_transform_handles") { + this->single_node_transform_handles = value.getBool(); + this->_selected_nodes->showTransformHandles( + this->show_transform_handles, this->single_node_transform_handles); + } else if (entry_name == "edit_clipping_paths") { + this->edit_clipping_paths = value.getBool(); + this->selection_changed(_desktop->selection); + } else if (entry_name == "edit_masks") { + this->edit_masks = value.getBool(); + this->selection_changed(_desktop->selection); + } else { + ToolBase::set(value); + } +} + +/** Recursively collect ShapeRecords */ +static +void gather_items(NodeTool *nt, SPItem *base, SPObject *obj, Inkscape::UI::ShapeRole role, + std::set<Inkscape::UI::ShapeRecord> &s) +{ + using namespace Inkscape::UI; + + if (!obj) { + return; + } + + //XML Tree being used directly here while it shouldn't be. + if (role != SHAPE_ROLE_NORMAL && (SP_IS_GROUP(obj) || SP_IS_OBJECTGROUP(obj))) { + for (auto& c: obj->children) { + gather_items(nt, base, &c, role, s); + } + } else if (SP_IS_ITEM(obj)) { + SPObject *object = obj; + SPItem *item = dynamic_cast<SPItem *>(obj); + ShapeRecord r; + r.object = object; + // TODO add support for objectBoundingBox + r.edit_transform = base ? base->i2doc_affine() : Geom::identity(); + r.role = role; + + if (s.insert(r).second) { + // this item was encountered the first time + if (nt->edit_clipping_paths) { + gather_items(nt, item, item->getClipObject(), SHAPE_ROLE_CLIPPING_PATH, s); + } + + if (nt->edit_masks) { + gather_items(nt, item, item->getMaskObject(), SHAPE_ROLE_MASK, s); + } + } + } +} + +void NodeTool::selection_changed(Inkscape::Selection *sel) { + using namespace Inkscape::UI; + + std::set<ShapeRecord> shapes; + + auto items= sel->items(); + for(auto i=items.begin();i!=items.end();++i){ + SPItem *item = *i; + if (item) { + gather_items(this, nullptr, item, SHAPE_ROLE_NORMAL, shapes); + } + } + + // use multiple ShapeEditors for now, to allow editing many shapes at once + // needs to be rethought + for (auto i = this->_shape_editors.begin(); i != this->_shape_editors.end();) { + ShapeRecord s; + s.object = dynamic_cast<SPObject *>(i->first); + + if (shapes.find(s) == shapes.end()) { + this->_shape_editors.erase(i++); + } else { + ++i; + } + } + + for (const auto & r : shapes) { + if (this->_shape_editors.find(SP_ITEM(r.object)) == this->_shape_editors.end()) { + auto si = std::make_unique<ShapeEditor>(_desktop, r.edit_transform); + SPItem *item = SP_ITEM(r.object); + si->set_item(item); + this->_shape_editors.insert({item, std::move(si)}); + } + } + + std::vector<SPItem *> vec(sel->items().begin(), sel->items().end()); + _previous_selection = _current_selection; + _current_selection = vec; + this->_multipath->setItems(shapes); + this->update_tip(nullptr); + sp_update_helperpath(_desktop); + // This not need to be called canvas is updated on selection change on setItems + // _desktop->updateNow(); +} + +bool NodeTool::root_handler(GdkEvent* event) { + /* things to handle here: + * 1. selection of items + * 2. passing events to manipulators + * 3. some keybindings + */ + using namespace Inkscape::UI; // pull in event helpers + + Inkscape::Selection *selection = _desktop->selection; + static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (this->_multipath->event(this, event)) { + return true; + } + + if (this->_selector->event(this, event)) { + return true; + } + + if (this->_selected_nodes->event(this, event)) { + return true; + } + + switch (event->type) + { + + case GDK_MOTION_NOTIFY: { + sp_update_helperpath(_desktop); + SPItem *over_item = nullptr; + over_item = sp_event_context_find_item(_desktop, event_point(event->button), FALSE, TRUE); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + + SnapManager &m = _desktop->namedview->snap_manager; + + // We will show a pre-snap indication for when the user adds a node through double-clicking + // Adding a node will only work when a path has been selected; if that's not the case then snapping is useless + if (!_desktop->selection->isEmpty()) { + if (!(event->motion.state & GDK_SHIFT_MASK)) { + m.setup(_desktop); + Inkscape::SnapCandidatePoint scp(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.preSnap(scp, true); + m.unSetup(); + } + } + + if (over_item && over_item != this->_last_over) { + this->_last_over = over_item; + //ink_node_tool_update_tip(nt, event); + this->update_tip(event); + } + // create pathflash outline + + if (prefs->getBool("/tools/nodes/pathflash_enabled")) { + // We want to reset flashed item to can highligh again previous one + if (!over_item && this->flashed_item) { + this->flashed_item = nullptr; + break; + } + if (!over_item || over_item == this->flashed_item) { + break; + } + + if (!prefs->getBool("/tools/nodes/pathflash_selected") && over_item && selection->includes(over_item)) { + break; + } + + if (this->flash_tempitem) { + _desktop->remove_temporary_canvasitem(this->flash_tempitem); + this->flash_tempitem = nullptr; + this->flashed_item = nullptr; + } + + auto shape = dynamic_cast<SPShape const *>(over_item); + if (!shape) { + break; // for now, handle only shapes + } + + this->flashed_item = over_item; + auto c = SPCurve::copy(shape->curveForEdit()); + + if (!c) { + break; // break out when curve doesn't exist + } + + c->transform(over_item->i2dt_affine()); + + auto flash = new Inkscape::CanvasItemBpath(_desktop->getCanvasTemp(), c.get(), true); + flash->set_stroke(over_item->highlight_color()); + flash->set_fill(0x0, SP_WIND_RULE_NONZERO); // No fill. + flash_tempitem = + _desktop->add_temporary_canvasitem(flash, prefs->getInt("/tools/nodes/pathflash_timeout", 500)); + } + break; // do not return true, because we need to pass this event to the parent context + // otherwise some features cease to work + } + + case GDK_KEY_PRESS: + switch (get_latin_keyval(&event->key)) + { + case GDK_KEY_Escape: // deselect everything + if (this->_selected_nodes->empty()) { + Inkscape::SelectionHelper::selectNone(_desktop); + } else { + this->_selected_nodes->clear(); + } + //ink_node_tool_update_tip(nt, event); + this->update_tip(event); + return TRUE; + + case GDK_KEY_a: + case GDK_KEY_A: + if (held_control(event->key) && held_alt(event->key)) { + this->_selected_nodes->selectAll(); + // Ctrl+A is handled in selection-chemistry.cpp via verb + //ink_node_tool_update_tip(nt, event); + this->update_tip(event); + return TRUE; + } + break; + + case GDK_KEY_h: + case GDK_KEY_H: + if (held_only_control(event->key)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/nodes/show_handles", !this->show_handles); + return TRUE; + } + break; + + case GDK_KEY_Tab: + _multipath->shiftSelection(1); + return TRUE; + break; + case GDK_KEY_ISO_Left_Tab: + _multipath->shiftSelection(-1); + return TRUE; + break; + + default: + break; + } + //ink_node_tool_update_tip(nt, event); + this->update_tip(event); + break; + + case GDK_KEY_RELEASE: + //ink_node_tool_update_tip(nt, event); + this->update_tip(event); + break; + + case GDK_BUTTON_RELEASE: + if (this->_selector->doubleClicked()) { + // If the selector received the doubleclick event, then we're at some distance from + // the path; otherwise, the doubleclick event would have been received by + // CurveDragPoint; we will insert nodes into the path anyway but only if we can snap + // to the path. Otherwise the position would not be very well defined. + if (!(event->motion.state & GDK_SHIFT_MASK)) { + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + Inkscape::SnapCandidatePoint scp(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE); + Inkscape::SnappedPoint sp = m.freeSnap(scp, Geom::OptRect(), true); + m.unSetup(); + + if (sp.getSnapped()) { + // The first click of the double click will have cleared the path selection, because + // we clicked aside of the path. We need to undo this on double click + Inkscape::Selection *selection = _desktop->getSelection(); + selection->addList(_previous_selection); + + // The selection has been restored, and the signal selection_changed has been emitted, + // which has again forced a restore of the _mmap variable of the MultiPathManipulator (this->_multipath) + // Now we can insert the new nodes as if nothing has happened! + this->_multipath->insertNode(_desktop->d2w(sp.getPoint())); + } + } + } + break; + + default: + break; + } + // we really dont want to stop any node operation we want to success all even the time consume it + + return ToolBase::root_handler(event); +} + +void NodeTool::update_tip(GdkEvent *event) { + using namespace Inkscape::UI; + if (event && (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)) { + unsigned new_state = state_after_event(event); + + if (new_state == event->key.state) { + return; + } + + if (state_held_shift(new_state)) { + if (this->_last_over) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, + C_("Node tool tip", "<b>Shift</b>: drag to add nodes to the selection, " + "click to toggle object selection")); + } else { + this->message_context->set(Inkscape::NORMAL_MESSAGE, + C_("Node tool tip", "<b>Shift</b>: drag to add nodes to the selection")); + } + + return; + } + } + + unsigned sz = this->_selected_nodes->size(); + unsigned total = this->_selected_nodes->allPoints().size(); + + if (sz != 0) { + // TODO: Use Glib::ustring::compose and remove the useless copy after string freeze + char *nodestring_temp = g_strdup_printf( + ngettext("<b>%u of %u</b> node selected.", "<b>%u of %u</b> nodes selected.", total), + sz, total); + Glib::ustring nodestring(nodestring_temp); + g_free(nodestring_temp); + + if (sz == 2) { + // if there are only two nodes selected, display the angle + // of a line going through them relative to the X axis. + Inkscape::UI::ControlPointSelection::Set &selection_nodes = this->_selected_nodes->allPoints(); + std::vector<Geom::Point> positions; + for (auto selection_node : selection_nodes) { + if (selection_node->selected()) { + Inkscape::UI::Node *n = dynamic_cast<Inkscape::UI::Node *>(selection_node); + positions.push_back(n->position()); + } + } + g_assert(positions.size() == 2); + const double angle = Geom::deg_from_rad(Geom::Line(positions[0], positions[1]).angle()); + nodestring += " "; + nodestring += Glib::ustring::compose(_("Angle: %1°."), + Glib::ustring::format(std::fixed, std::setprecision(2), angle)); + } + + if (this->_last_over) { + // TRANSLATORS: The %s below is where the "%u of %u nodes selected" sentence gets put + char *dyntip = g_strdup_printf(C_("Node tool tip", + "%s Drag to select nodes, click to edit only this object (more: Shift)"), + nodestring.c_str()); + this->message_context->set(Inkscape::NORMAL_MESSAGE, dyntip); + g_free(dyntip); + } else { + char *dyntip = g_strdup_printf(C_("Node tool tip", + "%s Drag to select nodes, click clear the selection"), + nodestring.c_str()); + this->message_context->set(Inkscape::NORMAL_MESSAGE, dyntip); + g_free(dyntip); + } + } else if (!this->_multipath->empty()) { + if (this->_last_over) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select nodes, click to edit only this object")); + } else { + this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select nodes, click to clear the selection")); + } + } else { + if (this->_last_over) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select objects to edit, click to edit this object (more: Shift)")); + } else { + this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select objects to edit")); + } + } +} + +/** + * @param sel Area in desktop coordinates + */ +void NodeTool::select_area(Geom::Rect const &sel, GdkEventButton *event) { + using namespace Inkscape::UI; + + if (this->_multipath->empty()) { + // if multipath is empty, select rubberbanded items rather than nodes + Inkscape::Selection *selection = _desktop->selection; + auto sel_doc = _desktop->dt2doc() * sel; + std::vector<SPItem *> items = _desktop->getDocument()->getItemsInBox(_desktop->dkey, sel_doc); + selection->setList(items); + } else { + bool shift = held_shift(*event); + bool ctrl = held_control(*event); + + if (!shift) { + // A/C. No modifier, selects all nodes, or selects all other nodes. + this->_selected_nodes->clear(); + } + if (shift && ctrl) { + // D. Shift+Ctrl pressed, removes nodes under box from existing selection. + this->_selected_nodes->selectArea(sel, true); + } else { + // A/B/C. Adds nodes under box to existing selection. + this->_selected_nodes->selectArea(sel); + if (ctrl) { + // C. Selects the inverse of all nodes under the box. + this->_selected_nodes->invertSelection(); + } + } + } +} + +void NodeTool::select_point(Geom::Point const &/*sel*/, GdkEventButton *event) { + using namespace Inkscape::UI; // pull in event helpers + + if (!event) { + return; + } + + if (event->button != 1) { + return; + } + + Inkscape::Selection *selection = _desktop->selection; + + SPItem *item_clicked = sp_event_context_find_item (_desktop, event_point(*event), + (event->state & GDK_MOD1_MASK) && !(event->state & GDK_CONTROL_MASK), TRUE); + + if (item_clicked == nullptr) { // nothing under cursor + // if no Shift, deselect + // if there are nodes selected, the first click should deselect the nodes + // and the second should deselect the items + if (!state_held_shift(event->state)) { + if (this->_selected_nodes->empty()) { + selection->clear(); + } else { + this->_selected_nodes->clear(); + } + } + } else { + if (held_shift(*event)) { + selection->toggle(item_clicked); + } else { + selection->set(item_clicked); + } + // This not need to be called canvas is updated on selection change + // _desktop->updateNow(); + } +} + +void NodeTool::mouseover_changed(Inkscape::UI::ControlPoint *p) { + using Inkscape::UI::CurveDragPoint; + + CurveDragPoint *cdp = dynamic_cast<CurveDragPoint*>(p); + + if (cdp && !this->cursor_drag) { + this->set_cursor("node-mouseover.svg"); + this->cursor_drag = true; + } else if (!cdp && this->cursor_drag) { + this->set_cursor("node.svg"); + this->cursor_drag = false; + } +} + +void NodeTool::handleControlUiStyleChange() { + this->_multipath->updateHandles(); +} + +} +} +} + +//} // anonymous namespace + +/* + 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/tools/node-tool.h b/src/ui/tools/node-tool.h new file mode 100644 index 0000000..8a4fbcc --- /dev/null +++ b/src/ui/tools/node-tool.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New node tool with support for multiple path editing + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_NODE_TOOL_H +#define SEEN_UI_TOOL_NODE_TOOL_H + +#include <glib.h> +#include "ui/tools/tool-base.h" + +// we need it to call it from Live Effect +#include "selection.h" + +namespace Inkscape { + namespace Display { + class TemporaryItem; + } + + namespace UI { + class MultiPathManipulator; + class ControlPointSelection; + class Selector; + class ControlPoint; + + struct PathSharedData; + } +} + +struct SPCanvasGroup; + +#define INK_NODE_TOOL(obj) (dynamic_cast<Inkscape::UI::Tools::NodeTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define INK_IS_NODE_TOOL(obj) (dynamic_cast<const Inkscape::UI::Tools::NodeTool*>((const Inkscape::UI::Tools::ToolBase*)obj)) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class NodeTool : public ToolBase { +public: + NodeTool(SPDesktop *desktop); + ~NodeTool() override; + + Inkscape::UI::ControlPointSelection* _selected_nodes = nullptr; + Inkscape::UI::MultiPathManipulator* _multipath = nullptr; + std::vector<Inkscape::Display::TemporaryItem *> _helperpath_tmpitem; + std::map<SPItem *, std::unique_ptr<ShapeEditor>> _shape_editors; + + bool edit_clipping_paths = false; + bool edit_masks = false; + + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + void deleteSelected(); +private: + sigc::connection _selection_changed_connection; + sigc::connection _mouseover_changed_connection; + + SPItem *flashed_item = nullptr; + + Inkscape::Display::TemporaryItem *flash_tempitem = nullptr; + Inkscape::UI::Selector* _selector = nullptr; + Inkscape::UI::PathSharedData* _path_data = nullptr; + Inkscape::CanvasItemGroup *_transform_handle_group = nullptr; + SPItem *_last_over = nullptr; + + bool cursor_drag = false; + bool show_handles = false; + bool show_outline =false; + bool live_outline = false; + bool live_objects = false; + bool show_path_direction = false; + bool show_transform_handles = false; + bool single_node_transform_handles = false; + + std::vector<SPItem*> _current_selection; + std::vector<SPItem*> _previous_selection; + + void selection_changed(Inkscape::Selection *sel); + + void select_area(Geom::Rect const &sel, GdkEventButton *event); + void select_point(Geom::Point const &sel, GdkEventButton *event); + void mouseover_changed(Inkscape::UI::ControlPoint *p); + void update_tip(GdkEvent *event); + void handleControlUiStyleChange(); +}; +void sp_update_helperpath(SPDesktop *desktop); +} + +} +} + +#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/tools/pages-tool.cpp b/src/ui/tools/pages-tool.cpp new file mode 100644 index 0000000..095ca96 --- /dev/null +++ b/src/ui/tools/pages-tool.cpp @@ -0,0 +1,593 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Page editing tool + * + * Authors: + * Martin Owens <doctormo@geek-2.com> + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "pages-tool.h" + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "display/control/canvas-item-bpath.h" +#include "display/control/canvas-item-curve.h" +#include "display/control/canvas-item-group.h" +#include "display/control/canvas-item-rect.h" +#include "display/control/snap-indicator.h" +#include "document-undo.h" +#include "include/macros.h" +#include "object/sp-page.h" +#include "path/path-outline.h" +#include "pure-transform.h" +#include "rubberband.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "snap-preferences.h" +#include "snap.h" +#include "ui/icon-names.h" +#include "ui/knot/knot.h" +#include "ui/widget/canvas.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +PagesTool::PagesTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/pages", "select.svg") +{ + // Stash the regular object selection so we don't modify them in base-tools root handler. + desktop->selection->setBackup(); + desktop->selection->clear(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + if (resize_knots.empty()) { + for (int i = 0; i < 4; i++) { + auto knot = new SPKnot(desktop, _("Resize page"), Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "PageTool:Resize"); + knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE); + knot->setFill(0xffffff00, 0x0000ff00, 0x000000ff, 0x000000ff); + knot->setSize(9); + knot->setAnchor(SP_ANCHOR_CENTER); + knot->updateCtrl(); + knot->hide(); + knot->moved_signal.connect(sigc::mem_fun(*this, &PagesTool::resizeKnotMoved)); + knot->ungrabbed_signal.connect(sigc::mem_fun(*this, &PagesTool::resizeKnotFinished)); + if (auto window = desktop->getCanvas()->get_window()) { + knot->setCursor(SP_KNOT_STATE_DRAGGING, this->get_cursor(window, "page-resizing.svg")); + knot->setCursor(SP_KNOT_STATE_MOUSEOVER, this->get_cursor(window, "page-resize.svg")); + } + resize_knots.push_back(knot); + } + } + + if (!visual_box) { + visual_box = new Inkscape::CanvasItemRect(desktop->getCanvasControls()); + visual_box->set_stroke(0x0000ff7f); + visual_box->hide(); + } + if (!drag_group) { + drag_group = new Inkscape::CanvasItemGroup(desktop->getCanvasTemp()); + drag_group->set_name("CanvasItemGroup:PagesDragShapes"); + } + + _doc_replaced_connection = desktop->connectDocumentReplaced([=](SPDesktop *desktop, SPDocument *doc) { + connectDocument(desktop->getDocument()); + }); + connectDocument(desktop->getDocument()); + + _zoom_connection = desktop->signal_zoom_changed.connect([=](double) { + // This readjusts the knot on zoom because the viewbox position + // becomes detached on zoom, likely a precision problem. + if (!desktop->getDocument()->getPageManager().hasPages()) { + selectionChanged(desktop->getDocument(), nullptr); + } + }); +} + + +PagesTool::~PagesTool() +{ + connectDocument(nullptr); + + ungrabCanvasEvents(); + + _desktop->selection->restoreBackup(); + + if (visual_box) { + delete visual_box; + visual_box = nullptr; + } + + for (auto knot : resize_knots) { + delete knot; + } + resize_knots.clear(); + + if (drag_group) { + delete drag_group; + drag_group = nullptr; + drag_shapes.clear(); // Already deleted by group + } + + _doc_replaced_connection.disconnect(); + _zoom_connection.disconnect(); +} + +void PagesTool::resizeKnotSet(Geom::Rect rect) +{ + for (int i = 0; i < resize_knots.size(); i++) { + resize_knots[i]->moveto(rect.corner(i)); + resize_knots[i]->show(); + } +} + +void PagesTool::resizeKnotMoved(SPKnot *knot, Geom::Point const &ppointer, guint state) +{ + Geom::Rect rect; + + auto page = _desktop->getDocument()->getPageManager().getSelected(); + if (page) { + // Resizing a specific selected page + rect = page->getDesktopRect(); + } else if (auto document = _desktop->getDocument()) { + // Resizing the naked viewBox + rect = *(document->preferredBounds()); + } + + int index; + for (index = 0; index < 4; index++) { + if (knot == resize_knots[index]) { + break; + } + } + Geom::Point start = rect.corner(index); + Geom::Point point = getSnappedResizePoint(knot->position(), state, start, page); + + if (point != start) { + if (index % 3 == 0) + rect[Geom::X].setMin(point[Geom::X]); + else + rect[Geom::X].setMax(point[Geom::X]); + + if (index < 2) + rect[Geom::Y].setMin(point[Geom::Y]); + else + rect[Geom::Y].setMax(point[Geom::Y]); + + visual_box->show(); + visual_box->set_rect(rect); + on_screen_rect = Geom::Rect(rect); + mouse_is_pressed = true; + } +} + +/** + * Resize snapping allows knot and tool point snapping consistency. + */ +Geom::Point PagesTool::getSnappedResizePoint(Geom::Point point, guint state, Geom::Point origin, SPObject *target) +{ + if (!(state & GDK_SHIFT_MASK)) { + SnapManager &snap_manager = _desktop->namedview->snap_manager; + snap_manager.setup(_desktop, true, target); + Inkscape::SnapCandidatePoint scp(point, Inkscape::SNAPSOURCE_PAGE_CORNER); + scp.addOrigin(origin); + Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp); + point = sp.getPoint(); + snap_manager.unSetup(); + } + return point; +} + +void PagesTool::resizeKnotFinished(SPKnot *knot, guint state) +{ + auto document = _desktop->getDocument(); + auto page = document->getPageManager().getSelected(); + if (on_screen_rect) { + if (!page || page->isViewportPage()) { + // Adjust viewport so it's scroll adjustment is correct + *on_screen_rect *= document->dt2doc(); + } + document->getPageManager().fitToRect(*on_screen_rect, page); + Inkscape::DocumentUndo::done(document, "Resize page", INKSCAPE_ICON("tool-pages")); + on_screen_rect = {}; + } + visual_box->hide(); + mouse_is_pressed = false; +} + +bool PagesTool::root_handler(GdkEvent *event) +{ + bool ret = false; + auto &page_manager = _desktop->getDocument()->getPageManager(); + + switch (event->type) { + case GDK_BUTTON_PRESS: { + if (event->button.button == 1) { + mouse_is_pressed = true; + drag_origin_w = Geom::Point(event->button.x, event->button.y); + drag_origin_dt = _desktop->w2d(drag_origin_w); + ret = true; + if (auto page = pageUnder(drag_origin_dt, false)) { + // Select the clicked on page. Manager ignores the same-page. + _desktop->getDocument()->getPageManager().selectPage(page); + this->set_cursor("page-dragging.svg"); + } else if (viewboxUnder(drag_origin_dt)) { + dragging_viewbox = true; + this->set_cursor("page-dragging.svg"); + } else { + drag_origin_dt = getSnappedResizePoint(drag_origin_dt, event->button.state, Geom::Point(0, 0)); + } + } + break; + } + case GDK_MOTION_NOTIFY: { + + auto point_w = Geom::Point(event->motion.x, event->motion.y); + auto point_dt = _desktop->w2d(point_w); + bool snap = !(event->motion.state & GDK_SHIFT_MASK); + + if (event->motion.state & GDK_BUTTON1_MASK) { + if (!mouse_is_pressed) { + // this sometimes happens if the mouse was off the edge when the event started + drag_origin_w = point_w; + drag_origin_dt = point_dt; + mouse_is_pressed = true; + } + + if (dragging_item || dragging_viewbox) { + // Continue to drag item. + Geom::Affine tr = moveTo(point_dt, snap); + // XXX Moving the existing shapes would be much better, but it has + // a weird bug which stops it from working well. + // drag_group->update(tr * drag_group->get_parent()->get_affine()); + addDragShapes(dragging_item, tr); + } else if (on_screen_rect) { + // Continue to drag new box + point_dt = getSnappedResizePoint(point_dt, event->motion.state, drag_origin_dt); + on_screen_rect = Geom::Rect(drag_origin_dt, point_dt); + } else if (Geom::distance(drag_origin_w, point_w) < drag_tolerance) { + // do not start dragging anything new if we're within tolerance from origin. + // pass + } else if (auto page = pageUnder(drag_origin_dt)) { + // Starting to drag page around the screen, the pageUnder must + // be the drag_origin as small movements can kill the UX feel. + dragging_item = page; + page_manager.selectPage(page); + addDragShapes(page, Geom::Affine()); + grabPage(page); + } else if (viewboxUnder(drag_origin_dt)) { + // Special handling of viewbox dragging + dragging_viewbox = true; + } else { + // Start making a new page. + dragging_item = nullptr; + on_screen_rect = Geom::Rect(drag_origin_dt, drag_origin_dt); + this->set_cursor("page-draw.svg"); + } + } else { + mouse_is_pressed = false; + drag_origin_dt = point_dt; + } + break; + } + case GDK_BUTTON_RELEASE: { + if (event->button.button != 1) { + break; + } + auto point_w = Geom::Point(event->button.x, event->button.y); + auto point_dt = _desktop->w2d(point_w); + bool snap = !(event->button.state & GDK_SHIFT_MASK); + auto document = _desktop->getDocument(); + + if (dragging_viewbox || dragging_item) { + if (dragging_viewbox || dragging_item->isViewportPage()) { + // Move the document's viewport first + auto page_items = page_manager.getOverlappingItems(_desktop, dragging_item); + auto rect = document->preferredBounds(); + auto affine = moveTo(point_dt, snap) * document->dt2doc(); + document->fitToRect(*rect * affine, false); + // Now move the page back to where we expect it. + if (dragging_item) { + dragging_item->movePage(affine, false); + dragging_item->setDesktopRect(*rect); + } + // We have a custom move object because item detection is fubar after fitToRect + if (page_manager.move_objects()) { + SPPage::moveItems(affine, page_items); + } + } else { + // Move the page object on the canvas. + dragging_item->movePage(moveTo(point_dt, snap), page_manager.move_objects()); + } + Inkscape::DocumentUndo::done(_desktop->getDocument(), "Move page position", INKSCAPE_ICON("tool-pages")); + } else if (on_screen_rect) { + // conclude box here (make new page) + page_manager.selectPage(page_manager.newDesktopPage(*on_screen_rect)); + Inkscape::DocumentUndo::done(_desktop->getDocument(), "Create new drawn page", INKSCAPE_ICON("tool-pages")); + } + mouse_is_pressed = false; + drag_origin_dt = point_dt; + ret = true; + + // Clear snap indication on mouse up. + _desktop->snapindicator->remove_snaptarget(); + break; + } + case GDK_KEY_PRESS: { + if (event->key.keyval == GDK_KEY_Escape) { + mouse_is_pressed = false; + ret = true; + } + if (event->key.keyval == GDK_KEY_Delete) { + page_manager.deletePage(page_manager.move_objects()); + + Inkscape::DocumentUndo::done(_desktop->getDocument(), "Delete Page", INKSCAPE_ICON("tool-pages")); + ret = true; + } + } + default: + break; + } + + // Clean up any finished dragging, doesn't matter how it ends + if (!mouse_is_pressed && (dragging_item || on_screen_rect || dragging_viewbox)) { + dragging_viewbox = false; + dragging_item = nullptr; + on_screen_rect = {}; + clearDragShapes(); + visual_box->hide(); + ret = true; + } else if (on_screen_rect) { + visual_box->show(); + visual_box->set_rect(*on_screen_rect); + ret = true; + } + if (!mouse_is_pressed) { + if (pageUnder(drag_origin_dt) || viewboxUnder(drag_origin_dt)) { + // This page under uses the current mouse position (unlike the above) + this->set_cursor("page-mouseover.svg"); + } else { + this->set_cursor("page-draw.svg"); + } + } + + + return ret ? true : ToolBase::root_handler(event); +} + +void PagesTool::menu_popup(GdkEvent *event, SPObject *obj) +{ + auto &page_manager = _desktop->getDocument()->getPageManager(); + SPPage *page = page_manager.getSelected(); + if (event->type != GDK_KEY_PRESS) { + drag_origin_w = Geom::Point(event->button.x, event->button.y); + drag_origin_dt = _desktop->w2d(drag_origin_w); + page = pageUnder(drag_origin_dt); + } + if (page) { + ToolBase::menu_popup(event, page); + } +} + +/** + * Creates the right snapping setup for dragging items around. + */ +void PagesTool::grabPage(SPPage *target) +{ + _bbox_points.clear(); + getBBoxPoints(target->getDesktopRect(), &_bbox_points, false, SNAPSOURCE_PAGE_CORNER, SNAPTARGET_UNDEFINED, + SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED, SNAPSOURCE_PAGE_CENTER, SNAPTARGET_UNDEFINED); +} + +/* + * Generate the movement affine as the page is dragged around (including snapping) + */ +Geom::Affine PagesTool::moveTo(Geom::Point xy, bool snap) +{ + Geom::Point dxy = xy - drag_origin_dt; + + if (snap) { + SnapManager &snap_manager = _desktop->namedview->snap_manager; + snap_manager.setup(_desktop, true, dragging_item); + snap_manager.snapprefs.clearTargetMask(0); // Disable all snapping targets + snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_CATEGORY, -1); + snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_PAGE_CORNER, -1); + snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_PAGE_CENTER, -1); + snap_manager.snapprefs.setTargetMask(SNAPTARGET_PAGE_CORNER, -1); + snap_manager.snapprefs.setTargetMask(SNAPTARGET_PAGE_CENTER, -1); + snap_manager.snapprefs.setTargetMask(SNAPTARGET_GRID_INTERSECTION, -1); + snap_manager.snapprefs.setTargetMask(SNAPTARGET_GUIDE, -1); + snap_manager.snapprefs.setTargetMask(SNAPTARGET_GUIDE_INTERSECTION, -1); + + Inkscape::PureTranslate *bb = new Inkscape::PureTranslate(dxy); + snap_manager.snapTransformed(_bbox_points, drag_origin_dt, (*bb)); + + if (bb->best_snapped_point.getSnapped()) { + dxy = bb->getTranslationSnapped(); + _desktop->snapindicator->set_new_snaptarget(bb->best_snapped_point); + } + + snap_manager.snapprefs.clearTargetMask(-1); // Reset preferences + snap_manager.unSetup(); + } + + return Geom::Translate(dxy); +} + +/** + * Add all the shapes needed to see it being dragged. + */ +void PagesTool::addDragShapes(SPPage *page, Geom::Affine tr) +{ + clearDragShapes(); + auto doc = _desktop->getDocument(); + + if (page) { + addDragShape(Geom::PathVector(Geom::Path(page->getDesktopRect())), tr); + } else { + auto doc_rect = doc->preferredBounds(); + addDragShape(Geom::PathVector(Geom::Path(*doc_rect)), tr); + } + if (Inkscape::Preferences::get()->getBool("/tools/pages/move_objects", true)) { + for (auto &item : doc->getPageManager().getOverlappingItems(_desktop, page)) { + if (item && !item->isLocked()) { + addDragShape(item, tr); + } + } + } +} + +/** + * Add an SPItem to the things being dragged. + */ +void PagesTool::addDragShape(SPItem *item, Geom::Affine tr) +{ + if (auto shape = item_to_outline(item)) { + addDragShape(*shape * item->i2dt_affine(), tr); + } +} + +/** + * Add a shape to the set of dragging shapes, these are deleted when dragging stops. + */ +void PagesTool::addDragShape(Geom::PathVector &&pth, Geom::Affine tr) +{ + auto shape = new CanvasItemBpath(drag_group, pth * tr, false); + shape->set_stroke(0x00ff007f); + shape->set_fill(0x00000000, SP_WIND_RULE_EVENODD); + drag_shapes.push_back(shape); +} + +/** + * Remove all drag shapes from the canvas. + */ +void PagesTool::clearDragShapes() +{ + for (auto &shape : drag_shapes) { + delete shape; + } + drag_shapes.clear(); +} + +/** + * Find a page under the cursor point. + */ +SPPage *PagesTool::pageUnder(Geom::Point pt, bool retain_selected) +{ + auto &pm = _desktop->getDocument()->getPageManager(); + + // If the point is still on the selected, favour that one. + if (auto selected = pm.getSelected()) { + if (retain_selected && selected->getSensitiveRect().contains(pt)) { + return selected; + } + } + // This provides a simple way of selecting a page based on their layering + // Pages which are entirely contained within another are selected before + // their larger parents. + SPPage* ret = nullptr; + for (auto &page : pm.getPages()) { + auto rect = page->getSensitiveRect(); + // If the point is inside the page boundry + if (rect.contains(pt)) { + // If we don't have a page yet, or the new page is inside the old one. + if (!ret || ret->getSensitiveRect().contains(rect)) { + ret = page; + } + } + } + return ret; +} + +/** + * Returns true if the document contains no pages AND the point + * is within the document viewbox. + */ +bool PagesTool::viewboxUnder(Geom::Point pt) +{ + if (auto document = _desktop->getDocument()) { + auto rect = document->preferredBounds(); + rect->expandBy(-0.1); // see sp-page getSensitiveRect + return !document->getPageManager().hasPages() && rect.contains(pt); + } + return true; +} + +void PagesTool::connectDocument(SPDocument *doc) +{ + _selector_changed_connection.disconnect(); + if (doc) { + auto &page_manager = doc->getPageManager(); + _selector_changed_connection = + page_manager.connectPageSelected([=](SPPage *page) { + selectionChanged(doc, page); + }); + selectionChanged(doc, page_manager.getSelected()); + } else { + selectionChanged(doc, nullptr); + } +} + + + +void PagesTool::selectionChanged(SPDocument *doc, SPPage *page) +{ + if (_page_modified_connection) { + _page_modified_connection.disconnect(); + for (auto knot : resize_knots) { + knot->hide(); + } + } + + // Loop existing pages because highlight_item is unsafe. + // Use desktop's document instead of doc, which may be nullptr. + for (auto &possible : _desktop->getDocument()->getPageManager().getPages()) { + if (highlight_item == possible) { + highlight_item->setSelected(false); + } + } + highlight_item = page; + if (doc) { + if (page) { + _page_modified_connection = page->connectModified(sigc::mem_fun(*this, &PagesTool::pageModified)); + page->setSelected(true); + pageModified(page, 0); + } else { + // This is for viewBox editng directly. A special extra feature + _page_modified_connection = doc->connectModified([=](guint){ + resizeKnotSet(*(doc->preferredBounds())); + }); + resizeKnotSet(*(doc->preferredBounds())); + } + } +} + +void PagesTool::pageModified(SPObject *object, guint /*flags*/) +{ + if (auto page = dynamic_cast<SPPage *>(object)) { + resizeKnotSet(page->getDesktopRect()); + } +} + +} // namespace Tools +} // 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 : diff --git a/src/ui/tools/pages-tool.h b/src/ui/tools/pages-tool.h new file mode 100644 index 0000000..29c44d9 --- /dev/null +++ b/src/ui/tools/pages-tool.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __UI_TOOLS_PAGES_CONTEXT_H__ +#define __UI_TOOLS_PAGES_CONTEXT_H__ + +/* + * Page editing tool + * + * Authors: + * Martin Owens <doctormo@geek-2.com> + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/tool-base.h" +#include "2geom/rect.h" + +#define SP_PAGES_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PagesTool *>((Inkscape::UI::Tools::ToolBase *)obj)) +#define SP_IS_PAGES_CONTEXT(obj) \ + (dynamic_cast<const Inkscape::UI::Tools::PagesTool *>((const Inkscape::UI::Tools::ToolBase *)obj) != NULL) + +class SPDocument; +class SPObject; +class SPPage; +class SPKnot; +class SnapManager; + +namespace Inkscape { +class SnapCandidatePoint; +class CanvasItemGroup; +class CanvasItemRect; +class CanvasItemBpath; + +namespace UI { +namespace Tools { + +class PagesTool : public ToolBase +{ +public: + PagesTool(SPDesktop *desktop); + ~PagesTool() override; + + bool root_handler(GdkEvent *event) override; + void menu_popup(GdkEvent *event, SPObject *obj = nullptr) override; +private: + void selectionChanged(SPDocument *doc, SPPage *page); + void connectDocument(SPDocument *doc); + SPPage *pageUnder(Geom::Point pt, bool retain_selected = true); + bool viewboxUnder(Geom::Point pt); + void addDragShapes(SPPage *page, Geom::Affine tr); + void addDragShape(SPItem *item, Geom::Affine tr); + void addDragShape(Geom::PathVector &&pth, Geom::Affine tr); + void clearDragShapes(); + + Geom::Point getSnappedResizePoint(Geom::Point point, guint state, Geom::Point origin, SPObject *target = nullptr); + void resizeKnotSet(Geom::Rect rect); + void resizeKnotMoved(SPKnot *knot, Geom::Point const &ppointer, guint state); + void resizeKnotFinished(SPKnot *knot, guint state); + void pageModified(SPObject *object, guint flags); + + void grabPage(SPPage *target); + Geom::Affine moveTo(Geom::Point xy, bool snap); + + sigc::connection _selector_changed_connection; + sigc::connection _page_modified_connection; + sigc::connection _doc_replaced_connection; + sigc::connection _zoom_connection; + + bool dragging_viewbox = false; + bool mouse_is_pressed = false; + Geom::Point drag_origin_w; + Geom::Point drag_origin_dt; + int drag_tolerance = 5; + + std::vector<SPKnot *> resize_knots; + SPPage *highlight_item = nullptr; + SPPage *dragging_item = nullptr; + std::optional<Geom::Rect> on_screen_rect; + Inkscape::CanvasItemRect *visual_box = nullptr; + Inkscape::CanvasItemGroup *drag_group = nullptr; + std::vector<Inkscape::CanvasItemBpath *> drag_shapes; + + std::vector<Inkscape::SnapCandidatePoint> _bbox_points; +}; + +} // namespace Tools +} // namespace UI +} // namespace Inkscape + +#endif diff --git a/src/ui/tools/pen-tool.cpp b/src/ui/tools/pen-tool.cpp new file mode 100644 index 0000000..d85a3b7 --- /dev/null +++ b/src/ui/tools/pen-tool.cpp @@ -0,0 +1,2027 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Pen event context implementation. + */ + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/curves.h> + +#include "context-fns.h" +#include "desktop.h" +#include "include/macros.h" +#include "inkscape-application.h" // Undo check +#include "message-context.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "selection.h" + +#include "display/curve.h" +#include "display/control/canvas-item-bpath.h" +#include "display/control/canvas-item-ctrl.h" +#include "display/control/canvas-item-curve.h" + +#include "object/sp-path.h" + +#include "ui/draw-anchor.h" +#include "ui/shortcuts.h" +#include "ui/tools/pen-tool.h" + +// we include the necessary files for BSpline & Spiro +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/parameter/path.h" + +#define INKSCAPE_LPE_SPIRO_C +#include "live_effects/lpe-spiro.h" + +#include "helper/geom-nodetype.h" + +// For handling un-continuous paths: +#include "inkscape.h" + +#include "live_effects/spiro.h" + +#define INKSCAPE_LPE_BSPLINE_C +#include "live_effects/lpe-bspline.h" + + +namespace Inkscape { +namespace UI { +namespace Tools { + +static Geom::Point pen_drag_origin_w(0, 0); +static bool pen_within_tolerance = false; +const double HANDLE_CUBIC_GAP = 0.001; + +PenTool::PenTool(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename) + : FreehandBase(desktop, prefs_path, cursor_filename) +{ + tablet_enabled = false; + + // Pen indicators (temporary handles shown when adding a new node). + c0 = new Inkscape::CanvasItemCtrl(desktop->getCanvasControls(), Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE); + c1 = new Inkscape::CanvasItemCtrl(desktop->getCanvasControls(), Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE); + c0->set_fill(0x0); + c1->set_fill(0x0); + c0->hide(); + c1->hide(); + + cl0 = new Inkscape::CanvasItemCurve(desktop->getCanvasControls()); + cl1 = new Inkscape::CanvasItemCurve(desktop->getCanvasControls()); + cl0->hide(); + cl1->hide(); + + sp_event_context_read(this, "mode"); + + this->anchor_statusbar = false; + + this->setPolylineMode(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/freehand/pen/selcue")) { + this->enableSelectionCue(); + } + + _desktop_destroy = _desktop->connectDestroy([=](SPDesktop *) { state = State::DEAD; }); +} + +PenTool::~PenTool() { + _desktop_destroy.disconnect(); + this->discard_delayed_snap_event(); + + if (this->npoints != 0) { + // switching context - finish path + this->ea = nullptr; // unset end anchor if set (otherwise crashes) + if (state != State::DEAD) { + _finish(false); + } + } + + if (this->c0) { + delete c0; + } + if (this->c1) { + delete c1; + } + + if (this->cl0) { + delete cl0; + } + if (this->cl1) { + delete cl1; + } + + if (this->waiting_item && this->expecting_clicks_for_LPE > 0) { + // we received too few clicks to sanely set the parameter path so we remove the LPE from the item + this->waiting_item->removeCurrentPathEffect(false); + } +} + +void PenTool::setPolylineMode() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/tools/freehand/pen/freehand-mode", 0); + // change the nodes to make space for bspline mode + this->polylines_only = (mode == 3 || mode == 4); + this->polylines_paraxial = (mode == 4); + this->spiro = (mode == 1); + this->bspline = (mode == 2); + this->_bsplineSpiroColor(); + if (!this->green_bpaths.empty()) { + this->_redrawAll(); + } +} + + +void PenTool::_cancel() { + this->num_clicks = 0; + this->state = PenTool::STOP; + this->_resetColors(); + c0->hide(); + c1->hide(); + cl0->hide(); + cl1->hide(); + this->message_context->clear(); + this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled")); +} + +/** + * Callback that sets key to value in pen context. + */ +void PenTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring name = val.getEntryName(); + + if (name == "mode") { + if ( val.getString() == "drag" ) { + this->mode = MODE_DRAG; + } else { + this->mode = MODE_CLICK; + } + } +} + +bool PenTool::hasWaitingLPE() { + // note: waiting_LPE_type is defined in SPDrawContext + return (this->waiting_LPE != nullptr || + this->waiting_LPE_type != Inkscape::LivePathEffect::INVALID_LPE); +} + +/** + * Snaps new node relative to the previous node. + */ +void PenTool::_endpointSnap(Geom::Point &p, guint const state) { + // Paraxial kicks in after first line has set the angle (before then it's a free line) + bool poly = this->polylines_paraxial && !this->green_curve->is_unset(); + + if ((state & GDK_CONTROL_MASK) && !poly) { //CTRL enables angular snapping + if (this->npoints > 0) { + spdc_endpoint_snap_rotation(this, p, this->p[0], state); + } else { + std::optional<Geom::Point> origin = std::optional<Geom::Point>(); + spdc_endpoint_snap_free(this, p, origin, state); + } + } else { + // We cannot use shift here to disable snapping because the shift-key is already used + // to toggle the paraxial direction; if the user wants to disable snapping (s)he will + // have to use the %-key, the menu, or the snap toolbar + if ((this->npoints > 0) && poly) { + // snap constrained + this->_setToNearestHorizVert(p, state); + } else { + // snap freely + std::optional<Geom::Point> origin = this->npoints > 0 ? this->p[0] : std::optional<Geom::Point>(); + spdc_endpoint_snap_free(this, p, origin, state); // pass the origin, to allow for perpendicular / tangential snapping + } + } +} + +/** + * Snaps new node's handle relative to the new node. + */ +void PenTool::_endpointSnapHandle(Geom::Point &p, guint const state) { + g_return_if_fail(( this->npoints == 2 || + this->npoints == 5 )); + + if ((state & GDK_CONTROL_MASK)) { //CTRL enables angular snapping + spdc_endpoint_snap_rotation(this, p, this->p[this->npoints - 2], state); + } else { + if (!(state & GDK_SHIFT_MASK)) { //SHIFT disables all snapping, except the angular snapping above + std::optional<Geom::Point> origin = this->p[this->npoints - 2]; + spdc_endpoint_snap_free(this, p, origin, state); + } + } +} + +bool PenTool::item_handler(SPItem* item, GdkEvent* event) { + bool ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = this->_handleButtonPress(event->button); + break; + case GDK_BUTTON_RELEASE: + ret = this->_handleButtonRelease(event->button); + break; + default: + break; + } + + if (!ret) { + ret = FreehandBase::item_handler(item, event); + } + + return ret; +} + +/** + * Callback to handle all pen events. + */ +bool PenTool::root_handler(GdkEvent* event) { + bool ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = this->_handleButtonPress(event->button); + break; + + case GDK_MOTION_NOTIFY: + ret = this->_handleMotionNotify(event->motion); + break; + + case GDK_BUTTON_RELEASE: + ret = this->_handleButtonRelease(event->button); + break; + + case GDK_2BUTTON_PRESS: + ret = this->_handle2ButtonPress(event->button); + break; + + case GDK_KEY_PRESS: + ret = this->_handleKeyPress(event); + break; + + default: + break; + } + + if (!ret) { + ret = FreehandBase::root_handler(event); + } + + return ret; +} + +/** + * Handle mouse button press event. + */ +bool PenTool::_handleButtonPress(GdkEventButton const &bevent) { + if (this->events_disabled) { + // skip event processing if events are disabled + return false; + } + + Geom::Point const event_w(bevent.x, bevent.y); + Geom::Point event_dt(_desktop->w2d(event_w)); + //Test whether we hit any anchor. + SPDrawAnchor * const anchor = spdc_test_inside(this, event_w); + + //with this we avoid creating a new point over the existing one + if(bevent.button != 3 && (this->spiro || this->bspline) && this->npoints > 0 && this->p[0] == this->p[3]){ + if( anchor && anchor == this->sa && this->green_curve->is_unset()){ + //remove the following line to avoid having one node on top of another + _finishSegment(event_dt, bevent.state); + _finish(true); + return true; + } + return false; + } + + bool ret = false; + if (bevent.button == 1 + // make sure this is not the last click for a waiting LPE (otherwise we want to finish the path) + && this->expecting_clicks_for_LPE != 1) { + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return true; + } + + grabCanvasEvents(); + + pen_drag_origin_w = event_w; + pen_within_tolerance = true; + + switch (this->mode) { + + case PenTool::MODE_CLICK: + // In click mode we add point on release + switch (this->state) { + case PenTool::POINT: + case PenTool::CONTROL: + case PenTool::CLOSE: + break; + case PenTool::STOP: + // This is allowed, if we just canceled curve + this->state = PenTool::POINT; + break; + default: + break; + } + break; + case PenTool::MODE_DRAG: + switch (this->state) { + case PenTool::STOP: + // This is allowed, if we just canceled curve + case PenTool::POINT: + if (this->npoints == 0) { + this->_bsplineSpiroColor(); + Geom::Point p; + if ((bevent.state & GDK_CONTROL_MASK) && (this->polylines_only || this->polylines_paraxial)) { + p = event_dt; + if (!(bevent.state & GDK_SHIFT_MASK)) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + } + spdc_create_single_dot(this, p, "/tools/freehand/pen", bevent.state); + ret = true; + break; + } + + // TODO: Perhaps it would be nicer to rearrange the following case + // distinction so that the case of a waiting LPE is treated separately + + // Set start anchor + + this->sa = anchor; + if (anchor) { + //Put the start overwrite curve always on the same direction + if (anchor->start) { + this->sa_overwrited = this->sa->curve->create_reverse(); + } else { + this->sa_overwrited = this->sa->curve->copy(); + } + this->_bsplineSpiroStartAnchor(bevent.state & GDK_SHIFT_MASK); + } + if (anchor && (!this->hasWaitingLPE()|| this->bspline || this->spiro)) { + // Adjust point to anchor if needed; if we have a waiting LPE, we need + // a fresh path to be created so don't continue an existing one + p = anchor->dp; + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Continuing selected path")); + } else { + // This is the first click of a new curve; deselect item so that + // this curve is not combined with it (unless it is drawn from its + // anchor, which is handled by the sibling branch above) + Inkscape::Selection * const selection = _desktop->getSelection(); + if (!(bevent.state & GDK_SHIFT_MASK) || this->hasWaitingLPE()) { + // if we have a waiting LPE, we need a fresh path to be created + // so don't append to an existing one + selection->clear(); + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path")); + } else if (selection->singleItem() && SP_IS_PATH(selection->singleItem())) { + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path")); + } + + // Create green anchor + p = event_dt; + this->_endpointSnap(p, bevent.state); + this->green_anchor.reset(new SPDrawAnchor(this, this->green_curve.get(), true, p)); + } + this->_setInitialPoint(p); + } else { + // Set end anchor + this->ea = anchor; + Geom::Point p; + if (anchor) { + p = anchor->dp; + // we hit an anchor, will finish the curve (either with or without closing) + // in release handler + this->state = PenTool::CLOSE; + + if (this->green_anchor && this->green_anchor->active) { + // we clicked on the current curve start, so close it even if + // we drag a handle away from it + this->green_closed = true; + } + ret = true; + break; + + } else { + p = event_dt; + this->_endpointSnap(p, bevent.state); // Snap node only if not hitting anchor. + this->_setSubsequentPoint(p, true); + } + } + // avoid the creation of a control point so a node is created in the release event + this->state = (this->spiro || this->bspline || this->polylines_only) ? PenTool::POINT : PenTool::CONTROL; + ret = true; + break; + case PenTool::CONTROL: + g_warning("Button down in CONTROL state"); + break; + case PenTool::CLOSE: + g_warning("Button down in CLOSE state"); + break; + default: + break; + } + break; + default: + break; + } + } else if (this->expecting_clicks_for_LPE == 1 && this->npoints != 0) { + // when the last click for a waiting LPE occurs we want to finish the path + this->_finishSegment(event_dt, bevent.state); + if (this->green_closed) { + // finishing at the start anchor, close curve + this->_finish(true); + } else { + // finishing at some other anchor, finish curve but not close + this->_finish(false); + } + + ret = true; + } else if (bevent.button == 3 && this->npoints != 0 && !_button1on) { + // right click - finish path, but only if the left click isn't pressed. + this->ea = nullptr; // unset end anchor if set (otherwise crashes) + this->_finish(false); + ret = true; + } + + if (this->expecting_clicks_for_LPE > 0) { + --this->expecting_clicks_for_LPE; + } + + return ret; +} + +/** + * Handle motion_notify event. + */ +bool PenTool::_handleMotionNotify(GdkEventMotion const &mevent) { + bool ret = false; + + if (mevent.state & GDK_BUTTON2_MASK) { + // allow scrolling + return false; + } + + if (this->events_disabled) { + // skip motion events if pen events are disabled + return false; + } + + Geom::Point const event_w(mevent.x, mevent.y); + + //we take out the function the const "tolerance" because we need it later + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint const tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + if (pen_within_tolerance) { + if ( Geom::LInfty( event_w - pen_drag_origin_w ) < tolerance ) { + return false; // 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) + pen_within_tolerance = false; + + // Find desktop coordinates + Geom::Point p = _desktop->w2d(event_w); + + // Test, whether we hit any anchor + SPDrawAnchor *anchor = spdc_test_inside(this, event_w); + + switch (this->mode) { + case PenTool::MODE_CLICK: + switch (this->state) { + case PenTool::POINT: + if ( this->npoints != 0 ) { + // Only set point, if we are already appending + this->_endpointSnap(p, mevent.state); + this->_setSubsequentPoint(p, true); + ret = true; + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + case PenTool::CONTROL: + case PenTool::CLOSE: + // Placing controls is last operation in CLOSE state + this->_endpointSnap(p, mevent.state); + this->_setCtrl(p, mevent.state); + ret = true; + break; + case PenTool::STOP: + if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + default: + break; + } + break; + case PenTool::MODE_DRAG: + switch (this->state) { + case PenTool::POINT: + if ( this->npoints > 0 ) { + // Only set point, if we are already appending + + if (!anchor) { // Snap node only if not hitting anchor + this->_endpointSnap(p, mevent.state); + this->_setSubsequentPoint(p, true, mevent.state); + } else { + this->_setSubsequentPoint(anchor->dp, false, mevent.state); + } + + if (anchor && !this->anchor_statusbar) { + if(!this->spiro && !this->bspline){ + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to close and finish the path.")); + }else{ + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to close and finish the path. Shift+Click make a cusp node")); + } + this->anchor_statusbar = true; + } else if (!anchor && this->anchor_statusbar) { + this->message_context->clear(); + this->anchor_statusbar = false; + } + + ret = true; + } else { + if (anchor && !this->anchor_statusbar) { + if(!this->spiro && !this->bspline){ + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to continue the path from this point.")); + }else{ + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to continue the path from this point. Shift+Click make a cusp node")); + } + this->anchor_statusbar = true; + } else if (!anchor && this->anchor_statusbar) { + this->message_context->clear(); + this->anchor_statusbar = false; + + } + if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + } + break; + case PenTool::CONTROL: + case PenTool::CLOSE: + // Placing controls is last operation in CLOSE state + + // snap the handle + + this->_endpointSnapHandle(p, mevent.state); + + if (!this->polylines_only) { + this->_setCtrl(p, mevent.state); + } else { + this->_setCtrl(this->p[1], mevent.state); + } + + gobble_motion_events(GDK_BUTTON1_MASK); + ret = true; + break; + case PenTool::STOP: + // Don't break; fall through to default to do preSnapping + default: + if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + } + break; + default: + break; + } + // calls the function "bspline_spiro_motion" when the mouse starts or stops moving + if(this->bspline){ + this->_bsplineSpiroMotion(mevent.state); + }else{ + if ( Geom::LInfty( event_w - pen_drag_origin_w ) > (tolerance/2) || mevent.time == 0) { + this->_bsplineSpiroMotion(mevent.state); + pen_drag_origin_w = event_w; + } + } + + return ret; +} + +/** + * Handle mouse button release event. + */ +bool PenTool::_handleButtonRelease(GdkEventButton const &revent) { + if (this->events_disabled) { + // skip event processing if events are disabled + return false; + } + + bool ret = false; + + if (revent.button == 1) { + Geom::Point const event_w(revent.x, revent.y); + + // Find desktop coordinates + Geom::Point p = _desktop->w2d(event_w); + + // Test whether we hit any anchor. + + SPDrawAnchor *anchor = spdc_test_inside(this, event_w); + // if we try to create a node in the same place as another node, we skip + if((!anchor || anchor == this->sa) && (this->spiro || this->bspline) && this->npoints > 0 && this->p[0] == this->p[3]){ + return true; + } + + switch (this->mode) { + case PenTool::MODE_CLICK: + switch (this->state) { + case PenTool::POINT: + this->ea = anchor; + if (anchor) { + p = anchor->dp; + } + this->state = PenTool::CONTROL; + break; + case PenTool::CONTROL: + // End current segment + this->_endpointSnap(p, revent.state); + this->_finishSegment(p, revent.state); + this->state = PenTool::POINT; + break; + case PenTool::CLOSE: + // End current segment + if (!anchor) { // Snap node only if not hitting anchor + this->_endpointSnap(p, revent.state); + } + this->_finishSegment(p, revent.state); + // hude the guide of the penultimate node when closing the curve + if(this->spiro){ + c1->hide(); + } + this->_finish(true); + this->state = PenTool::POINT; + break; + case PenTool::STOP: + // This is allowed, if we just canceled curve + this->state = PenTool::POINT; + break; + default: + break; + } + break; + case PenTool::MODE_DRAG: + switch (this->state) { + case PenTool::POINT: + case PenTool::CONTROL: + this->_endpointSnap(p, revent.state); + this->_finishSegment(p, revent.state); + break; + case PenTool::CLOSE: + this->_endpointSnap(p, revent.state); + this->_finishSegment(p, revent.state); + // hide the penultimate node guide when closing the curve + if(this->spiro){ + c1->hide(); + } + if (this->green_closed) { + // finishing at the start anchor, close curve + this->_finish(true); + } else { + // finishing at some other anchor, finish curve but not close + this->_finish(false); + } + break; + case PenTool::STOP: + // This is allowed, if we just cancelled curve + break; + default: + break; + } + this->state = PenTool::POINT; + break; + default: + break; + } + + ungrabCanvasEvents(); + + ret = true; + + this->green_closed = false; + } + + // TODO: can we be sure that the path was created correctly? + // TODO: should we offer an option to collect the clicks in a list? + if (this->expecting_clicks_for_LPE == 0 && this->hasWaitingLPE()) { + this->setPolylineMode(); + + Inkscape::Selection *selection = _desktop->getSelection(); + + if (this->waiting_LPE) { + // we have an already created LPE waiting for a path + this->waiting_LPE->acceptParamPath(SP_PATH(selection->singleItem())); + selection->add(this->waiting_item); + this->waiting_LPE = nullptr; + } else { + // the case that we need to create a new LPE and apply it to the just-drawn path is + // handled in spdc_check_for_and_apply_waiting_LPE() in draw-context.cpp + } + } + + return ret; +} + +bool PenTool::_handle2ButtonPress(GdkEventButton const &bevent) { + bool ret = false; + // only end on LMB double click. Otherwise horizontal scrolling causes ending of the path + if (this->npoints != 0 && bevent.button == 1 && this->state != PenTool::CLOSE) { + this->_finish(false); + ret = true; + } + return ret; +} + +void PenTool::_redrawAll() { + // green + if (! this->green_bpaths.empty()) { + // remove old piecewise green canvasitems + for (auto path : this->green_bpaths) { + delete path; + } + this->green_bpaths.clear(); + + // one canvas bpath for all of green_curve + auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), green_curve.get(), true); + canvas_shape->set_stroke(green_color); + canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO); + this->green_bpaths.push_back(canvas_shape); + } + if (this->green_anchor) { + this->green_anchor->ctrl->set_position(this->green_anchor->dp); + } + + this->red_curve->reset(); + this->red_curve->moveto(this->p[0]); + this->red_curve->curveto(this->p[1], this->p[2], this->p[3]); + red_bpath->set_bpath(red_curve.get(), true); + + // handles + // hide the handlers in bspline and spiro modes + if (this->p[0] != this->p[1] && !this->spiro && !this->bspline) { + c1->set_position(p[1]); + c1->show(); + cl1->set_coords(p[0], p[1]); + cl1->show(); + } else { + c1->hide(); + cl1->hide(); + } + + Geom::Curve const * last_seg = this->green_curve->last_segment(); + if (last_seg) { + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>( last_seg ); + // hide the handlers in bspline and spiro modes + if ( cubic && + (*cubic)[2] != this->p[0] && !this->spiro && !this->bspline ) + { + Geom::Point p2 = (*cubic)[2]; + c0->set_position(p2); + c0->show(); + cl0->set_coords(p2, p[0]); + cl0->show(); + } else { + c0->hide(); + cl0->hide(); + } + } + + // simply redraw the spiro. because its a redrawing, we don't call the global function, + // but we call the redrawing at the ending. + this->_bsplineSpiroBuild(); +} + +void PenTool::_lastpointMove(gdouble x, gdouble y) { + if (this->npoints != 5) + return; + + y *= -_desktop->yaxisdir(); + + // green + if (!this->green_curve->is_unset()) { + this->green_curve->last_point_additive_move( Geom::Point(x,y) ); + } else { + // start anchor too + if (this->green_anchor) { + this->green_anchor->dp += Geom::Point(x, y); + } + } + + // red + + this->p[0] += Geom::Point(x, y); + this->p[1] += Geom::Point(x, y); + this->_redrawAll(); +} + +void PenTool::_lastpointMoveScreen(gdouble x, gdouble y) { + this->_lastpointMove(x / _desktop->current_zoom(), y / _desktop->current_zoom()); +} + +void PenTool::_lastpointToCurve() { + // avoid that if the "red_curve" contains only two points ( rect ), it doesn't stop here. + if (this->npoints != 5 && !this->spiro && !this->bspline) + return; + + this->p[1] = this->red_curve->last_segment()->initialPoint() + (1./3.)*(*this->red_curve->last_point() - this->red_curve->last_segment()->initialPoint()); + //modificate the last segment of the green curve so it creates the type of node we need + if (this->spiro||this->bspline) { + if (!this->green_curve->is_unset()) { + Geom::Point A(0,0); + Geom::Point B(0,0); + Geom::Point C(0,0); + Geom::Point D(0,0); + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>( this->green_curve->last_segment() ); + //We obtain the last segment 4 points in the previous curve + if ( cubic ){ + A = (*cubic)[0]; + B = (*cubic)[1]; + if (this->spiro) { + C = this->p[0] + (this->p[0] - this->p[1]); + } else { + C = *this->green_curve->last_point() + (1./3.)*(this->green_curve->last_segment()->initialPoint() - *this->green_curve->last_point()); + } + D = (*cubic)[3]; + } else { + A = this->green_curve->last_segment()->initialPoint(); + B = this->green_curve->last_segment()->initialPoint(); + if (this->spiro) { + C = this->p[0] + (this->p[0] - this->p[1]); + } else { + C = *this->green_curve->last_point() + (1./3.)*(this->green_curve->last_segment()->initialPoint() - *this->green_curve->last_point()); + } + D = *this->green_curve->last_point(); + } + auto previous = std::make_unique<SPCurve>(); + previous->moveto(A); + previous->curveto(B, C, D); + if ( this->green_curve->get_segment_count() == 1) { + this->green_curve = std::move(previous); + } else { + //we eliminate the last segment + this->green_curve->backspace(); + //and we add it again with the recreation + green_curve->append_continuous(*previous); + } + } + //if the last node is an union with another curve + if (this->green_curve->is_unset() && this->sa && !this->sa->curve->is_unset()) { + this->_bsplineSpiroStartAnchor(false); + } + } + + this->_redrawAll(); +} + + +void PenTool::_lastpointToLine() { + // avoid that if the "red_curve" contains only two points ( rect) it doesn't stop here. + if (this->npoints != 5 && !this->bspline) + return; + + // modify the last segment of the green curve so the type of node we want is created. + if(this->spiro || this->bspline){ + if(!this->green_curve->is_unset()){ + Geom::Point A(0,0); + Geom::Point B(0,0); + Geom::Point C(0,0); + Geom::Point D(0,0); + auto previous = std::make_unique<SPCurve>(); + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>( this->green_curve->last_segment() ); + if ( cubic ) { + A = this->green_curve->last_segment()->initialPoint(); + B = (*cubic)[1]; + C = *this->green_curve->last_point(); + D = C; + } else { + //We obtain the last segment 4 points in the previous curve + A = this->green_curve->last_segment()->initialPoint(); + B = A; + C = *this->green_curve->last_point(); + D = C; + } + previous->moveto(A); + previous->curveto(B, C, D); + if( this->green_curve->get_segment_count() == 1){ + this->green_curve = std::move(previous); + }else{ + //we eliminate the last segment + this->green_curve->backspace(); + //and we add it again with the recreation + green_curve->append_continuous(*previous); + } + } + // if the last node is an union with another curve + if(this->green_curve->is_unset() && this->sa && !this->sa->curve->is_unset()){ + this->_bsplineSpiroStartAnchor(true); + } + } + + this->p[1] = this->p[0]; + this->_redrawAll(); +} + + +bool PenTool::_handleKeyPress(GdkEvent *event) { + bool ret = false; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px + + // Check for undo if we have started drawing a path. User might have change shortcut. + if (this->npoints > 0) { + Gtk::AccelKey shortcut = Inkscape::Shortcuts::get_from_event((GdkEventKey*)event); + auto app = InkscapeApplication::instance(); + auto gapp = app->gtk_app(); + auto actions = gapp->get_actions_for_accel(shortcut.get_abbrev()); + if (std::find(actions.begin(), actions.end(), "win.undo") != actions.end()) { + return _undoLastPoint(); + } + } + + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Left: // move last point left + case GDK_KEY_KP_Left: + if (!MOD__CTRL(event)) { // not ctrl + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + this->_lastpointMoveScreen(-10, 0); // shift + } + else { + this->_lastpointMoveScreen(-1, 0); // no shift + } + } + else { // no alt + if (MOD__SHIFT(event)) { + this->_lastpointMove(-10*nudge, 0); // shift + } + else { + this->_lastpointMove(-nudge, 0); // no shift + } + } + ret = true; + } + break; + case GDK_KEY_Up: // move last point up + case GDK_KEY_KP_Up: + if (!MOD__CTRL(event)) { // not ctrl + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + this->_lastpointMoveScreen(0, 10); // shift + } + else { + this->_lastpointMoveScreen(0, 1); // no shift + } + } + else { // no alt + if (MOD__SHIFT(event)) { + this->_lastpointMove(0, 10*nudge); // shift + } + else { + this->_lastpointMove(0, nudge); // no shift + } + } + ret = true; + } + break; + case GDK_KEY_Right: // move last point right + case GDK_KEY_KP_Right: + if (!MOD__CTRL(event)) { // not ctrl + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + this->_lastpointMoveScreen(10, 0); // shift + } + else { + this->_lastpointMoveScreen(1, 0); // no shift + } + } + else { // no alt + if (MOD__SHIFT(event)) { + this->_lastpointMove(10*nudge, 0); // shift + } + else { + this->_lastpointMove(nudge, 0); // no shift + } + } + ret = true; + } + break; + case GDK_KEY_Down: // move last point down + case GDK_KEY_KP_Down: + if (!MOD__CTRL(event)) { // not ctrl + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + this->_lastpointMoveScreen(0, -10); // shift + } + else { + this->_lastpointMoveScreen(0, -1); // no shift + } + } + else { // no alt + if (MOD__SHIFT(event)) { + this->_lastpointMove(0, -10*nudge); // shift + } + else { + this->_lastpointMove(0, -nudge); // no shift + } + } + ret = true; + } + break; + +/*TODO: this is not yet enabled?? looks like some traces of the Geometry tool + case GDK_KEY_P: + case GDK_KEY_p: + if (MOD__SHIFT_ONLY(event)) { + sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::PARALLEL, 2); + ret = true; + } + break; + + case GDK_KEY_C: + case GDK_KEY_c: + if (MOD__SHIFT_ONLY(event)) { + sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::CIRCLE_3PTS, 3); + ret = true; + } + break; + + case GDK_KEY_B: + case GDK_KEY_b: + if (MOD__SHIFT_ONLY(event)) { + sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::PERP_BISECTOR, 2); + ret = true; + } + break; + + case GDK_KEY_A: + case GDK_KEY_a: + if (MOD__SHIFT_ONLY(event)) { + sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::ANGLE_BISECTOR, 3); + ret = true; + } + break; +*/ + + case GDK_KEY_U: + case GDK_KEY_u: + if (MOD__SHIFT_ONLY(event)) { + this->_lastpointToCurve(); + ret = true; + } + break; + case GDK_KEY_L: + case GDK_KEY_l: + if (MOD__SHIFT_ONLY(event)) { + this->_lastpointToLine(); + ret = true; + } + break; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (this->npoints != 0) { + this->ea = nullptr; // unset end anchor if set (otherwise crashes) + if(MOD__SHIFT_ONLY(event)) { + // All this is needed to stop the last control + // point dispeating and stop making an n-1 shape. + Geom::Point const p(0, 0); + if(this->red_curve->is_unset()) { + this->red_curve->moveto(p); + } + this->_finishSegment(p, 0); + this->_finish(true); + } else { + this->_finish(false); + } + ret = true; + } + break; + case GDK_KEY_Escape: + if (this->npoints != 0) { + // if drawing, cancel, otherwise pass it up for deselecting + this->_cancel (); + ret = true; + } + break; + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + _desktop->selection->toGuides(); + ret = true; + } + break; + case GDK_KEY_BackSpace: + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + ret = _undoLastPoint(); + break; + case GDK_KEY_Z: + case GDK_KEY_z: + if (event->key.state & INK_GDK_PRIMARY_MASK) { + ret = _undoLastPoint(); + } + break; + default: + break; + } + return ret; +} + +void PenTool::_resetColors() { + // Red + this->red_curve->reset(); + this->red_bpath->set_bpath(nullptr); + + // Blue + this->blue_curve->reset(); + this->blue_bpath->set_bpath(nullptr); + + // Green + for (auto path : this->green_bpaths) { + delete path; + } + this->green_bpaths.clear(); + this->green_curve->reset(); + this->green_anchor.reset(); + + this->sa = nullptr; + this->ea = nullptr; + + if (this->sa_overwrited) { + this->sa_overwrited->reset(); + } + + this->npoints = 0; + this->red_curve_is_valid = false; +} + + +void PenTool::_setInitialPoint(Geom::Point const p) { + g_assert( this->npoints == 0 ); + + this->p[0] = p; + this->p[1] = p; + this->npoints = 2; + this->red_bpath->set_bpath(nullptr); +} + +/** + * Show the status message for the current line/curve segment. + * This type of message always shows angle/distance as the last + * two parameters ("angle %3.2f°, distance %s"). + */ +void PenTool::_setAngleDistanceStatusMessage(Geom::Point const p, int pc_point_to_compare, gchar const *message) { + g_assert((pc_point_to_compare == 0) || (pc_point_to_compare == 3)); // exclude control handles + g_assert(message != nullptr); + + Geom::Point rel = p - this->p[pc_point_to_compare]; + Inkscape::Util::Quantity q = Inkscape::Util::Quantity(Geom::L2(rel), "px"); + Glib::ustring dist = q.string(_desktop->namedview->display_units); + double angle = atan2(rel[Geom::Y], rel[Geom::X]) * 180 / M_PI; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/compassangledisplay/value", false) != 0) { + angle = 90 - angle; + + if (_desktop->is_yaxisdown()) { + angle = 180 - angle; + } + + if (angle < 0) { + angle += 360; + } + } + + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, message, angle, dist.c_str()); +} + +// this function changes the colors red, green and blue making them transparent or not, depending on if spiro is being used. +void PenTool::_bsplineSpiroColor() +{ + static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (this->spiro){ + this->red_color = 0xff000000; + this->green_color = 0x00ff0000; + } else if(this->bspline) { + this->highlight_color = currentLayer()->highlight_color(); + if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){ + this->green_color = 0xff00007f; + this->red_color = 0xff00007f; + } else { + this->green_color = this->highlight_color; + this->red_color = this->highlight_color; + } + } else { + this->highlight_color = currentLayer()->highlight_color(); + this->red_color = 0xff00007f; + if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){ + this->green_color = 0x00ff007f; + } else { + this->green_color = this->highlight_color; + } + blue_bpath->hide(); + } + + //We erase all the "green_bpaths" to recreate them after with the colour + //transparency recently modified + if (!this->green_bpaths.empty()) { + // remove old piecewise green canvasitems + for (auto path : this->green_bpaths) { + delete path; + } + this->green_bpaths.clear(); + + // one canvas bpath for all of green_curve + auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), green_curve.get(), true); + canvas_shape->set_stroke(green_color); + canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO); + this->green_bpaths.push_back(canvas_shape); + } + + this->red_bpath->set_stroke(red_color); +} + + +void PenTool::_bsplineSpiro(bool shift) +{ + if(!this->spiro && !this->bspline){ + return; + } + + shift?this->_bsplineSpiroOff():this->_bsplineSpiroOn(); + this->_bsplineSpiroBuild(); +} + +void PenTool::_bsplineSpiroOn() +{ + if(!this->red_curve->is_unset()){ + using Geom::X; + using Geom::Y; + this->npoints = 5; + this->p[0] = *this->red_curve->first_point(); + this->p[3] = this->red_curve->first_segment()->finalPoint(); + this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]); + this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP); + } +} + +void PenTool::_bsplineSpiroOff() +{ + if(!this->red_curve->is_unset()){ + this->npoints = 5; + this->p[0] = *this->red_curve->first_point(); + this->p[3] = this->red_curve->first_segment()->finalPoint(); + this->p[2] = this->p[3]; + } +} + +void PenTool::_bsplineSpiroStartAnchor(bool shift) +{ + if(this->sa->curve->is_unset()){ + return; + } + + LivePathEffect::LPEBSpline *lpe_bsp = nullptr; + + if (SP_IS_LPE_ITEM(this->white_item) && SP_LPE_ITEM(this->white_item)->hasPathEffect()){ + Inkscape::LivePathEffect::Effect *thisEffect = + SP_LPE_ITEM(this->white_item)->getFirstPathEffectOfType(Inkscape::LivePathEffect::BSPLINE); + if(thisEffect){ + lpe_bsp = dynamic_cast<LivePathEffect::LPEBSpline*>(thisEffect->getLPEObj()->get_lpe()); + } + } + if(lpe_bsp){ + this->bspline = true; + }else{ + this->bspline = false; + } + LivePathEffect::LPESpiro *lpe_spi = nullptr; + + if (SP_IS_LPE_ITEM(this->white_item) && SP_LPE_ITEM(this->white_item)->hasPathEffect()){ + Inkscape::LivePathEffect::Effect *thisEffect = + SP_LPE_ITEM(this->white_item)->getFirstPathEffectOfType(Inkscape::LivePathEffect::SPIRO); + if(thisEffect){ + lpe_spi = dynamic_cast<LivePathEffect::LPESpiro*>(thisEffect->getLPEObj()->get_lpe()); + } + } + if(lpe_spi){ + this->spiro = true; + }else{ + this->spiro = false; + } + if(!this->spiro && !this->bspline){ + _bsplineSpiroColor(); + return; + } + if(shift){ + this->_bsplineSpiroStartAnchorOff(); + } else { + this->_bsplineSpiroStartAnchorOn(); + } +} + +void PenTool::_bsplineSpiroStartAnchorOn() +{ + using Geom::X; + using Geom::Y; + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->sa_overwrited ->last_segment()); + auto last_segment = std::make_unique<SPCurve>(); + Geom::Point point_a = this->sa_overwrited->last_segment()->initialPoint(); + Geom::Point point_d = *this->sa_overwrited->last_point(); + Geom::Point point_c = point_d + (1./3)*(point_a - point_d); + point_c = Geom::Point(point_c[X] + HANDLE_CUBIC_GAP, point_c[Y] + HANDLE_CUBIC_GAP); + if(cubic){ + last_segment->moveto(point_a); + last_segment->curveto((*cubic)[1],point_c,point_d); + }else{ + last_segment->moveto(point_a); + last_segment->curveto(point_a,point_c,point_d); + } + if( this->sa_overwrited->get_segment_count() == 1){ + this->sa_overwrited = std::move(last_segment); + }else{ + //we eliminate the last segment + this->sa_overwrited->backspace(); + //and we add it again with the recreation + sa_overwrited->append_continuous(*last_segment); + } +} + +void PenTool::_bsplineSpiroStartAnchorOff() +{ + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->sa_overwrited->last_segment()); + if(cubic){ + auto last_segment = std::make_unique<SPCurve>(); + last_segment->moveto((*cubic)[0]); + last_segment->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]); + if( this->sa_overwrited->get_segment_count() == 1){ + this->sa_overwrited = std::move(last_segment); + }else{ + //we eliminate the last segment + this->sa_overwrited->backspace(); + //and we add it again with the recreation + sa_overwrited->append_continuous(*last_segment); + } + } +} + +void PenTool::_bsplineSpiroMotion(guint const state){ + bool shift = state & GDK_SHIFT_MASK; + if(!this->spiro && !this->bspline){ + return; + } + using Geom::X; + using Geom::Y; + if(this->red_curve->is_unset()) return; + this->npoints = 5; + std::unique_ptr<SPCurve> tmp_curve(new SPCurve()); + this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]); + this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP); + if (this->green_curve->is_unset() && !this->sa) { + this->p[1] = this->p[0] + (1./3)*(this->p[3] - this->p[0]); + this->p[1] = Geom::Point(this->p[1][X] + HANDLE_CUBIC_GAP, this->p[1][Y] + HANDLE_CUBIC_GAP); + if(shift){ + this->p[2] = this->p[3]; + } + } else if (!this->green_curve->is_unset()){ + tmp_curve = this->green_curve->copy(); + } else { + tmp_curve = this->sa_overwrited->copy(); + } + if ((state & GDK_MOD1_MASK ) && previous != Geom::Point(0,0)) { //ALT drag + this->p[0] = this->p[0] + (this->p[3] - previous); + } + if(!tmp_curve ->is_unset()){ + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment()); + if ((state & GDK_MOD1_MASK ) && + !Geom::are_near(*tmp_curve ->last_point(), this->p[0], 0.1)) + { + auto previous_weight_power = std::make_unique<SPCurve>(); + Geom::D2< Geom::SBasis > SBasisweight_power; + previous_weight_power->moveto(tmp_curve ->last_segment()->initialPoint()); + previous_weight_power->lineto(this->p[0]); + SBasisweight_power = previous_weight_power->first_segment()->toSBasis(); + if( tmp_curve ->get_segment_count() == 1){ + Geom::Point initial = tmp_curve ->last_segment()->initialPoint(); + tmp_curve->reset(); + tmp_curve->moveto(initial); + }else{ + tmp_curve->backspace(); + } + if(this->bspline && cubic && !Geom::are_near((*cubic)[2],(*cubic)[3])){ + tmp_curve->curveto(SBasisweight_power.valueAt(0.33334), SBasisweight_power.valueAt(0.66667), this->p[0]); + } else if(this->bspline && cubic) { + tmp_curve->curveto(SBasisweight_power.valueAt(0.33334), this->p[0], this->p[0]); + } else if (cubic && !Geom::are_near((*cubic)[2],(*cubic)[3])) { + tmp_curve->curveto((*cubic)[1], (*cubic)[2] + (this->p[3] - previous), this->p[0]); + } else if (cubic){ + tmp_curve->curveto((*cubic)[1], this->p[0], this->p[0]); + } else { + tmp_curve->lineto(this->p[0]); + } + cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment()); + if (this->sa && this->green_curve->is_unset()) { + this->sa_overwrited = tmp_curve->copy(); + } + this->green_curve = tmp_curve->copy(); + } + if (cubic) { + if (this->bspline) { + auto weight_power = std::make_unique<SPCurve>(); + Geom::D2< Geom::SBasis > SBasisweight_power; + weight_power->moveto(this->red_curve->last_segment()->initialPoint()); + weight_power->lineto(*this->red_curve->last_point()); + SBasisweight_power = weight_power->first_segment()->toSBasis(); + this->p[1] = SBasisweight_power.valueAt(0.33334); + if(!Geom::are_near(this->p[1],this->p[0])){ + this->p[1] = Geom::Point(this->p[1][X] + HANDLE_CUBIC_GAP,this->p[1][Y] + HANDLE_CUBIC_GAP); + } else { + this->p[1] = this->p[0]; + } + if (shift) { + this->p[2] = this->p[3]; + } + if(Geom::are_near((*cubic)[3], (*cubic)[2])) { + this->p[1] = this->p[0]; + } + } else { + this->p[1] = (*cubic)[3] + ((*cubic)[3] - (*cubic)[2] ); + } + } else { + this->p[1] = this->p[0]; + if (shift) { + this->p[2] = this->p[3]; + } + } + previous = *this->red_curve->last_point(); + auto red = std::make_unique<SPCurve>(); + red->moveto(this->p[0]); + red->curveto(this->p[1],this->p[2],this->p[3]); + this->red_bpath->set_bpath(red.get(), true); + } + + if(this->anchor_statusbar && !this->red_curve->is_unset()){ + if(shift){ + this->_bsplineSpiroEndAnchorOff(); + }else{ + this->_bsplineSpiroEndAnchorOn(); + } + } + + // remove old piecewise green canvasitems + for (auto path: this->green_bpaths) { + delete path; + } + this->green_bpaths.clear(); + + // one canvas bpath for all of green_curve + auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), green_curve.get(), true); + canvas_shape->set_stroke(green_color); + canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO); + this->green_bpaths.push_back(canvas_shape); + + this->_bsplineSpiroBuild(); +} + +void PenTool::_bsplineSpiroEndAnchorOn() +{ + + using Geom::X; + using Geom::Y; + this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]); + this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP); + std::unique_ptr<SPCurve> tmp_curve(new SPCurve()); + std::unique_ptr<SPCurve> last_segment(new SPCurve()); + Geom::Point point_c(0,0); + if( this->green_anchor && this->green_anchor->active ){ + tmp_curve = this->green_curve->create_reverse(); + if(this->green_curve->get_segment_count()==0){ + return; + } + } else if(this->sa){ + tmp_curve = this->sa_overwrited->create_reverse(); + }else{ + return; + } + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment()); + if(this->bspline){ + point_c = *tmp_curve ->last_point() + (1./3)*(tmp_curve ->last_segment()->initialPoint() - *tmp_curve ->last_point()); + point_c = Geom::Point(point_c[X] + HANDLE_CUBIC_GAP, point_c[Y] + HANDLE_CUBIC_GAP); + }else{ + point_c = this->p[3] + this->p[3] - this->p[2]; + } + if(cubic){ + last_segment->moveto((*cubic)[0]); + last_segment->curveto((*cubic)[1],point_c,(*cubic)[3]); + }else{ + last_segment->moveto(tmp_curve ->last_segment()->initialPoint()); + last_segment->lineto(*tmp_curve ->last_point()); + } + if( tmp_curve ->get_segment_count() == 1){ + tmp_curve = std::move(last_segment); + }else{ + //we eliminate the last segment + tmp_curve ->backspace(); + //and we add it again with the recreation + tmp_curve ->append_continuous(*last_segment); + } + tmp_curve = tmp_curve->create_reverse(); + if( this->green_anchor && this->green_anchor->active ) + { + this->green_curve->reset(); + this->green_curve = std::move(tmp_curve); + }else{ + this->sa_overwrited->reset(); + this->sa_overwrited = std::move(tmp_curve); + } +} + +void PenTool::_bsplineSpiroEndAnchorOff() +{ + + std::unique_ptr<SPCurve> tmp_curve(new SPCurve()); + std::unique_ptr<SPCurve> last_segment(new SPCurve()); + this->p[2] = this->p[3]; + if( this->green_anchor && this->green_anchor->active ){ + tmp_curve = this->green_curve->create_reverse(); + if(this->green_curve->get_segment_count()==0){ + return; + } + } else if(this->sa){ + tmp_curve = this->sa_overwrited->create_reverse(); + }else{ + return; + } + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment()); + if(cubic){ + last_segment->moveto((*cubic)[0]); + last_segment->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]); + }else{ + last_segment->moveto(tmp_curve ->last_segment()->initialPoint()); + last_segment->lineto(*tmp_curve ->last_point()); + } + if( tmp_curve ->get_segment_count() == 1){ + tmp_curve = std::move(last_segment); + }else{ + //we eliminate the last segment + tmp_curve ->backspace(); + //and we add it again with the recreation + tmp_curve ->append_continuous(*last_segment); + } + tmp_curve = tmp_curve->create_reverse(); + + if( this->green_anchor && this->green_anchor->active ) + { + this->green_curve->reset(); + this->green_curve = std::move(tmp_curve); + }else{ + this->sa_overwrited->reset(); + this->sa_overwrited = std::move(tmp_curve); + } +} + +//prepares the curves for its transformation into BSpline curve. +void PenTool::_bsplineSpiroBuild() +{ + if(!this->spiro && !this->bspline){ + return; + } + + //We create the base curve + auto curve = std::make_unique<SPCurve>(); + //If we continuate the existing curve we add it at the start + if(this->sa && !this->sa->curve->is_unset()){ + curve = this->sa_overwrited->copy(); + } + + if (!this->green_curve->is_unset()){ + curve->append_continuous(*green_curve); + } + + //and the red one + if (!this->red_curve->is_unset()){ + this->red_curve->reset(); + this->red_curve->moveto(this->p[0]); + if(this->anchor_statusbar && !this->sa && !(this->green_anchor && this->green_anchor->active)){ + this->red_curve->curveto(this->p[1],this->p[3],this->p[3]); + }else{ + this->red_curve->curveto(this->p[1],this->p[2],this->p[3]); + } + red_bpath->set_bpath(red_curve.get(), true); + curve->append_continuous(*red_curve); + } + previous = *this->red_curve->last_point(); + if(!curve->is_unset()){ + // close the curve if the final points of the curve are close enough + if(Geom::are_near(curve->first_path()->initialPoint(), curve->last_path()->finalPoint())){ + curve->closepath_current(); + } + //TODO: CALL TO CLONED FUNCTION SPIRO::doEffect IN lpe-spiro.cpp + //For example + //using namespace Inkscape::LivePathEffect; + //LivePathEffectObject *lpeobj = static_cast<LivePathEffectObject*> (curve); + //Effect *spr = static_cast<Effect*> ( new LPEbspline(lpeobj) ); + //spr->doEffect(curve); + if (this->bspline) { + Geom::PathVector hp; + LivePathEffect::sp_bspline_do_effect(curve.get(), 0, hp); + } else { + LivePathEffect::sp_spiro_do_effect(curve.get()); + } + + blue_bpath->set_bpath(curve.get(), true); + blue_bpath->set_stroke(blue_color); + blue_bpath->show(); + + this->blue_curve->reset(); + //We hide the holders that doesn't contribute anything + if (this->spiro){ + c1->set_position(p[0]); + c1->show(); + } else { + c1->hide(); + } + c0->hide(); + cl0->hide(); + cl1->hide(); + } else { + //if the curve is empty + blue_bpath->hide(); + } +} + +void PenTool::_setSubsequentPoint(Geom::Point const p, bool statusbar, guint status) { + g_assert( this->npoints != 0 ); + + // todo: Check callers to see whether 2 <= npoints is guaranteed. + + this->p[2] = p; + this->p[3] = p; + this->p[4] = p; + this->npoints = 5; + this->red_curve->reset(); + bool is_curve; + this->red_curve->moveto(this->p[0]); + if (this->polylines_paraxial && !statusbar) { + // we are drawing horizontal/vertical lines and hit an anchor; + Geom::Point const origin = this->p[0]; + // if the previous point and the anchor are not aligned either horizontally or vertically... + if ((std::abs(p[Geom::X] - origin[Geom::X]) > 1e-9) && (std::abs(p[Geom::Y] - origin[Geom::Y]) > 1e-9)) { + // ...then we should draw an L-shaped path, consisting of two paraxial segments + Geom::Point intermed = p; + this->_setToNearestHorizVert(intermed, status); + this->red_curve->lineto(intermed); + } + this->red_curve->lineto(p); + is_curve = false; + } else { + // one of the 'regular' modes + if (this->p[1] != this->p[0] || this->spiro) { + this->red_curve->curveto(this->p[1], p, p); + is_curve = true; + } else { + this->red_curve->lineto(p); + is_curve = false; + } + } + + red_bpath->set_bpath(red_curve.get(), true); + + if (statusbar) { + gchar *message; + if(this->spiro || this->bspline){ + message = is_curve ? + _("<b>Curve segment</b>: angle %3.2f°; <b>Shift+Click</b> creates cusp node, <b>ALT</b> moves previous, <b>Enter</b> or <b>Shift+Enter</b> to finish" ): + _("<b>Line segment</b>: angle %3.2f°; <b>Shift+Click</b> creates cusp node, <b>ALT</b> moves previous, <b>Enter</b> or <b>Shift+Enter</b> to finish"); + this->_setAngleDistanceStatusMessage(p, 0, message); + } else { + message = is_curve ? + _("<b>Curve segment</b>: angle %3.2f°, distance %s; with <b>Ctrl</b> to snap angle, <b>Enter</b> or <b>Shift+Enter</b> to finish the path" ): + _("<b>Line segment</b>: angle %3.2f°, distance %s; with <b>Ctrl</b> to snap angle, <b>Enter</b> or <b>Shift+Enter</b> to finish the path"); + this->_setAngleDistanceStatusMessage(p, 0, message); + } + + } +} + + +void PenTool::_setCtrl(Geom::Point const q, guint const state) { // use 'q' as 'p' shadows member variable. + c1->show(); + cl1->show(); + + if ( this->npoints == 2 ) { + this->p[1] = q; + c0->hide(); + cl0->hide(); + c1->set_position(p[1]); + cl1->set_coords(p[0], p[1]); + this->_setAngleDistanceStatusMessage(q, 0, _("<b>Curve handle</b>: angle %3.2f°, length %s; with <b>Ctrl</b> to snap angle")); + } else if ( this->npoints == 5 ) { + this->p[4] = q; + c0->show(); + cl0->show(); + bool is_symm = false; + if ( ( ( this->mode == PenTool::MODE_CLICK ) && ( state & GDK_CONTROL_MASK ) ) || + ( ( this->mode == PenTool::MODE_DRAG ) && !( state & GDK_SHIFT_MASK ) ) ) { + Geom::Point delta = q - this->p[3]; + this->p[2] = this->p[3] - delta; + is_symm = true; + this->red_curve->reset(); + this->red_curve->moveto(this->p[0]); + this->red_curve->curveto(this->p[1], this->p[2], this->p[3]); + red_bpath->set_bpath(red_curve.get(), true); + } + c0->set_position(this->p[2]); + cl0->set_coords(this->p[3], this->p[2]); + c1->set_position(this->p[4]); + cl1->set_coords(this->p[3], this->p[4]); + + + + gchar *message = is_symm ? + _("<b>Curve handle, symmetric</b>: angle %3.2f°, length %s; with <b>Ctrl</b> to snap angle, with <b>Shift</b> to move this handle only") : + _("<b>Curve handle</b>: angle %3.2f°, length %s; with <b>Ctrl</b> to snap angle, with <b>Shift</b> to move this handle only"); + this->_setAngleDistanceStatusMessage(q, 3, message); + } else { + g_warning("Something bad happened - npoints is %d", this->npoints); + } +} + +void PenTool::_finishSegment(Geom::Point const q, guint const state) { // use 'q' as 'p' shadows member variable. + if (this->polylines_paraxial) { + this->nextParaxialDirection(q, this->p[0], state); + } + + ++num_clicks; + + + if (!this->red_curve->is_unset()) { + this->_bsplineSpiro(state & GDK_SHIFT_MASK); + if(!this->green_curve->is_unset() && + !Geom::are_near(*this->green_curve->last_point(),this->p[0])) + { + std::unique_ptr<SPCurve> lsegment(new SPCurve()); + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->green_curve->last_segment()); + if (cubic) { + lsegment->moveto((*cubic)[0]); + lsegment->curveto((*cubic)[1], this->p[0] - ((*cubic)[2] - (*cubic)[3]), *this->red_curve->first_point()); + this->green_curve->backspace(); + green_curve->append_continuous(*lsegment); + } + } + this->green_curve->append_continuous(*red_curve); + auto curve = this->red_curve->copy(); + + /// \todo fixme: + auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), curve.get(), true); + canvas_shape->set_stroke(green_color); + canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO); + this->green_bpaths.push_back(canvas_shape); + + this->p[0] = this->p[3]; + this->p[1] = this->p[4]; + this->npoints = 2; + + this->red_curve->reset(); + } +} + +// Partial fix for https://bugs.launchpad.net/inkscape/+bug/171990 +// TODO: implement the redo feature +bool PenTool::_undoLastPoint() { + bool ret = false; + + if ( this->green_curve->is_unset() || (this->green_curve->last_segment() == nullptr) ) { + if (!this->red_curve->is_unset()) { + this->_cancel (); + ret = true; + } else { + // do nothing; this event should be handled upstream + } + } else { + // Reset red curve + this->red_curve->reset(); + // Get last segment + if ( this->green_curve->is_unset() ) { + g_warning("pen_handle_key_press, case GDK_KP_Delete: Green curve is empty"); + return false; + } + // The code below assumes that this->green_curve has only ONE path ! + Geom::Curve const * crv = this->green_curve->last_segment(); + this->p[0] = crv->initialPoint(); + if ( Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>(crv)) { + this->p[1] = (*cubic)[1]; + + } else { + this->p[1] = this->p[0]; + } + + // assign the value in a third of the distance of the last segment. + if (this->bspline){ + this->p[1] = this->p[0] + (1./3)*(this->p[3] - this->p[0]); + } + + Geom::Point const pt( (this->npoints < 4) ? crv->finalPoint() : this->p[3] ); + + this->npoints = 2; + // delete the last segment of the green curve and green bpath + if (this->green_curve->get_segment_count() == 1) { + this->npoints = 5; + if (!this->green_bpaths.empty()) { + delete this->green_bpaths.back(); + this->green_bpaths.pop_back(); + } + this->green_curve->reset(); + } else { + this->green_curve->backspace(); + if (this->green_bpaths.size() > 1) { + delete this->green_bpaths.back(); + this->green_bpaths.pop_back(); + } else if (this->green_bpaths.size() == 1) { + green_bpaths.back()->set_bpath(green_curve.get(), true); + } + } + + // assign the value of this->p[1] to the opposite of the green line last segment + if (this->spiro){ + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(this->green_curve->last_segment()); + if ( cubic ) { + this->p[1] = (*cubic)[3] + (*cubic)[3] - (*cubic)[2]; + c1->set_position(this->p[0]); + } else { + this->p[1] = this->p[0]; + } + } + + c0->hide(); + c1->hide(); + cl0->hide(); + cl1->hide(); + this->state = PenTool::POINT; + + if(this->polylines_paraxial) { + // We compare the point we're removing with the nearest horiz/vert to + // see if the line was added with SHIFT or not. + Geom::Point compare(pt); + this->_setToNearestHorizVert(compare, 0); + if ((std::abs(compare[Geom::X] - pt[Geom::X]) > 1e-9) + || (std::abs(compare[Geom::Y] - pt[Geom::Y]) > 1e-9)) { + this->paraxial_angle = this->paraxial_angle.cw(); + } + } + this->_setSubsequentPoint(pt, true); + + //redraw + this->_bsplineSpiroBuild(); + ret = true; + } + + return ret; +} + +void PenTool::_finish(gboolean const closed) { + if (this->expecting_clicks_for_LPE > 1) { + // don't let the path be finished before we have collected the required number of mouse clicks + return; + } + + + this->num_clicks = 0; + + this->_disableEvents(); + + this->message_context->clear(); + + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Drawing finished")); + + // cancelate line without a created segment + this->red_curve->reset(); + spdc_concat_colors_and_flush(this, closed); + this->sa = nullptr; + this->ea = nullptr; + + this->npoints = 0; + this->state = PenTool::POINT; + + c0->hide(); + c1->hide(); + cl0->hide(); + cl1->hide(); + + this->green_anchor.reset(); + + this->_enableEvents(); +} + +void PenTool::_disableEvents() { + this->events_disabled = true; +} + +void PenTool::_enableEvents() { + g_return_if_fail(this->events_disabled != 0); + + this->events_disabled = false; +} + +void PenTool::waitForLPEMouseClicks(Inkscape::LivePathEffect::EffectType effect_type, unsigned int num_clicks, bool use_polylines) { + if (effect_type == Inkscape::LivePathEffect::INVALID_LPE) + return; + + this->waiting_LPE_type = effect_type; + this->expecting_clicks_for_LPE = num_clicks; + this->polylines_only = use_polylines; + this->polylines_paraxial = false; // TODO: think if this is correct for all cases +} + +void PenTool::nextParaxialDirection(Geom::Point const &pt, Geom::Point const &origin, guint state) { + // + // after the first mouse click we determine whether the mouse pointer is closest to a + // horizontal or vertical segment; for all subsequent mouse clicks, we use the direction + // orthogonal to the last one; pressing Shift toggles the direction + // + // num_clicks is not reliable because spdc_pen_finish_segment is sometimes called too early + // (on first mouse release), in which case num_clicks immediately becomes 1. + // if (this->num_clicks == 0) { + + if (this->green_curve->is_unset()) { + // first mouse click + double h = pt[Geom::X] - origin[Geom::X]; + double v = pt[Geom::Y] - origin[Geom::Y]; + this->paraxial_angle = Geom::Point(h, v).ccw(); + } + if(!(state & GDK_SHIFT_MASK)) { + this->paraxial_angle = this->paraxial_angle.ccw(); + } +} + +void PenTool::_setToNearestHorizVert(Geom::Point &pt, guint const state) const { + Geom::Point const origin = this->p[0]; + Geom::Point const target = (state & GDK_SHIFT_MASK) ? this->paraxial_angle : this->paraxial_angle.ccw(); + + // Create a horizontal or vertical constraint line + Inkscape::Snapper::SnapConstraint cl(origin, target); + + // Snap along the constraint line; if we didn't snap then still the constraint will be applied + SnapManager &m = _desktop->namedview->snap_manager; + + Inkscape::Selection *selection = _desktop->getSelection(); + // selection->singleItem() is the item that is currently being drawn. This item will not be snapped to (to avoid self-snapping) + // TODO: Allow snapping to the stationary parts of the item, and only ignore the last segment + + m.setup(_desktop, true, selection->singleItem()); + m.constrainedSnapReturnByRef(pt, Inkscape::SNAPSOURCE_NODE_HANDLE, cl); + m.unSetup(); +} + +} +} +} + +/* + 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/tools/pen-tool.h b/src/ui/tools/pen-tool.h new file mode 100644 index 0000000..bd48ce7 --- /dev/null +++ b/src/ui/tools/pen-tool.h @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * PenTool: a context for pen tool events. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_PEN_CONTEXT_H +#define SEEN_PEN_CONTEXT_H + +#include <sigc++/sigc++.h> + +#include "ui/tools/freehand-base.h" +#include "live_effects/effect.h" + +#define SP_PEN_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PenTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_PEN_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::PenTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { + +class CanvasItemCtrl; +class CanvasItemCurve; + +namespace UI { +namespace Tools { + +/** + * PenTool: a context for pen tool events. + */ +class PenTool : public FreehandBase { +public: + PenTool(SPDesktop *desktop, + std::string prefs_path = "/tools/freehand/pen", + const std::string& cursor_filename = "pen.svg"); + ~PenTool() override; + + enum Mode { + MODE_CLICK, + MODE_DRAG + }; + + enum State { + POINT, + CONTROL, + CLOSE, + STOP, + DEAD + }; + + Geom::Point p[5]; + Geom::Point previous; + /** \invar npoints in {0, 2, 5}. */ + // npoints somehow determines the type of the node (what does it mean, exactly? the number of Bezier handles?) + gint npoints = 0; + + Mode mode = MODE_CLICK; + State state = POINT; + bool polylines_only = false; + bool polylines_paraxial = false; + Geom::Point paraxial_angle; + + bool spiro = false; // Spiro mode active? + bool bspline = false; // BSpline mode active? + int num_clicks = 0;; + + unsigned int expecting_clicks_for_LPE = 0; // if positive, finish the path after this many clicks + Inkscape::LivePathEffect::Effect *waiting_LPE = nullptr; // if NULL, waiting_LPE_type in SPDrawContext is taken into account + SPLPEItem *waiting_item = nullptr; + + Inkscape::CanvasItemCtrl *c0 = nullptr; // Start point of path. + Inkscape::CanvasItemCtrl *c1 = nullptr; // End point of path. + + Inkscape::CanvasItemCurve *cl0 = nullptr; + Inkscape::CanvasItemCurve *cl1 = nullptr; + + bool events_disabled = false; + + void nextParaxialDirection(Geom::Point const &pt, Geom::Point const &origin, guint state); + void setPolylineMode(); + bool hasWaitingLPE(); + void waitForLPEMouseClicks(Inkscape::LivePathEffect::EffectType effect_type, unsigned int num_clicks, bool use_polylines = true); + +protected: + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + +private: + bool _handleButtonPress(GdkEventButton const &bevent); + bool _handleMotionNotify(GdkEventMotion const &mevent); + bool _handleButtonRelease(GdkEventButton const &revent); + bool _handle2ButtonPress(GdkEventButton const &bevent); + bool _handleKeyPress(GdkEvent *event); + //this function changes the colors red, green and blue making them transparent or not depending on if the function uses spiro + void _bsplineSpiroColor(); + //creates a node in bspline or spiro modes + void _bsplineSpiro(bool shift); + //creates a node in bspline or spiro modes + void _bsplineSpiroOn(); + //creates a CUSP node + void _bsplineSpiroOff(); + //continues the existing curve in bspline or spiro mode + void _bsplineSpiroStartAnchor(bool shift); + //continues the existing curve with the union node in bspline or spiro modes + void _bsplineSpiroStartAnchorOn(); + //continues an existing curve with the union node in CUSP mode + void _bsplineSpiroStartAnchorOff(); + //modifies the "red_curve" when it detects movement + void _bsplineSpiroMotion(guint const state); + //closes the curve with the last node in bspline or spiro mode + void _bsplineSpiroEndAnchorOn(); + //closes the curve with the last node in CUSP mode + void _bsplineSpiroEndAnchorOff(); + //apply the effect + void _bsplineSpiroBuild(); + + void _setInitialPoint(Geom::Point const p); + void _setSubsequentPoint(Geom::Point const p, bool statusbar, guint status = 0); + void _setCtrl(Geom::Point const p, guint state); + void _finishSegment(Geom::Point p, guint state); + bool _undoLastPoint(); + + void _finish(gboolean closed); + + void _resetColors(); + + void _disableEvents(); + void _enableEvents(); + + void _setToNearestHorizVert(Geom::Point &pt, guint const state) const; + + void _setAngleDistanceStatusMessage(Geom::Point const p, int pc_point_to_compare, gchar const *message); + + void _lastpointToLine(); + void _lastpointToCurve(); + void _lastpointMoveScreen(gdouble x, gdouble y); + void _lastpointMove(gdouble x, gdouble y); + void _redrawAll(); + + void _endpointSnapHandle(Geom::Point &p, guint const state); + void _endpointSnap(Geom::Point &p, guint const state); + + void _cancel(); + + sigc::connection _desktop_destroy; +}; + +} +} +} + +#endif /* !SEEN_PEN_CONTEXT_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/tools/pencil-tool.cpp b/src/ui/tools/pencil-tool.cpp new file mode 100644 index 0000000..055f581 --- /dev/null +++ b/src/ui/tools/pencil-tool.cpp @@ -0,0 +1,1189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Pencil event context implementation. + */ + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <numeric> // For std::accumulate +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/bezier-utils.h> +#include <2geom/circle.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/svg-path-parser.h> + +#include "pencil-tool.h" + +#include "context-fns.h" +#include "desktop.h" +#include "desktop-style.h" +#include "layer-manager.h" +#include "message-context.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "snap.h" + +#include "display/curve.h" +#include "display/control/canvas-item-bpath.h" +#include "display/control/snap-indicator.h" + +#include "livarot/Path.h" // Simplify paths + +#include "live_effects/lpe-powerstroke-interpolators.h" +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-simplify.h" +#include "live_effects/lpeobject.h" + +#include "object/sp-lpe-item.h" +#include "object/sp-path.h" +#include "path/path-boolop.h" +#include "style.h" + +#include "svg/svg.h" + +#include "ui/draw-anchor.h" +#include "ui/tool/event-utils.h" + +#include "xml/node.h" +#include "xml/sp-css-attr.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +static Geom::Point pencil_drag_origin_w(0, 0); +static bool pencil_within_tolerance = false; + +static bool in_svg_plane(Geom::Point const &p) { return Geom::LInfty(p) < 1e18; } +const double HANDLE_CUBIC_GAP = 0.01; + +PencilTool::PencilTool(SPDesktop *desktop) + : FreehandBase(desktop, "/tools/freehand/pencil", "pencil.svg") + , p() + , _npoints(0) + , _state(SP_PENCIL_CONTEXT_IDLE) + , _req_tangent(0, 0) + , _is_drawing(false) + , sketch_n(0) + , _pressure_curve(nullptr) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/freehand/pencil/selcue")) { + this->enableSelectionCue(); + } + this->_pressure_curve = std::make_unique<SPCurve>(); + this->_is_drawing = false; + this->anchor_statusbar = false; +} + +PencilTool::~PencilTool() { +} + +void PencilTool::_extinput(GdkEvent *event) { + if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &this->pressure)) { + this->pressure = CLAMP (this->pressure, DDC_MIN_PRESSURE, DDC_MAX_PRESSURE); + is_tablet = true; + } else { + this->pressure = DDC_DEFAULT_PRESSURE; + is_tablet = false; + } +} + +/** Snaps new node relative to the previous node. */ +void PencilTool::_endpointSnap(Geom::Point &p, guint const state) { + if ((state & GDK_CONTROL_MASK)) { //CTRL enables constrained snapping + if (this->_npoints > 0) { + spdc_endpoint_snap_rotation(this, p, this->p[0], state); + } + } else { + if (!(state & GDK_SHIFT_MASK)) { //SHIFT disables all snapping, except the angular snapping above + //After all, the user explicitly asked for angular snapping by + //pressing CTRL + std::optional<Geom::Point> origin = this->_npoints > 0 ? this->p[0] : std::optional<Geom::Point>(); + spdc_endpoint_snap_free(this, p, origin, state); + } else { + _desktop->snapindicator->remove_snaptarget(); + } + } +} + +/** + * Callback for handling all pencil context events. + */ +bool PencilTool::root_handler(GdkEvent* event) { + bool ret = false; + this->_extinput(event); + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = this->_handleButtonPress(event->button); + break; + + case GDK_MOTION_NOTIFY: + ret = this->_handleMotionNotify(event->motion); + break; + + case GDK_BUTTON_RELEASE: + ret = this->_handleButtonRelease(event->button); + break; + + case GDK_KEY_PRESS: + ret = this->_handleKeyPress(event->key); + break; + + case GDK_KEY_RELEASE: + ret = this->_handleKeyRelease(event->key); + break; + + default: + break; + } + if (!ret) { + ret = FreehandBase::root_handler(event); + } + + return ret; +} + +bool PencilTool::_handleButtonPress(GdkEventButton const &bevent) { + bool ret = false; + if ( bevent.button == 1) { + Inkscape::Selection *selection = _desktop->getSelection(); + + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return true; + } + + /* Grab mouse, so release will not pass unnoticed */ + grabCanvasEvents(); + + Geom::Point const button_w(bevent.x, bevent.y); + + /* Find desktop coordinates */ + Geom::Point p = _desktop->w2d(button_w); + + /* Test whether we hit any anchor. */ + SPDrawAnchor *anchor = spdc_test_inside(this, button_w); + if (tablet_enabled) { + anchor = nullptr; + } + pencil_drag_origin_w = Geom::Point(bevent.x,bevent.y); + pencil_within_tolerance = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tablet_enabled = prefs->getBool("/tools/freehand/pencil/pressure", false); + switch (this->_state) { + case SP_PENCIL_CONTEXT_ADDLINE: + /* Current segment will be finished with release */ + ret = true; + break; + default: + /* Set first point of sequence */ + SnapManager &m = _desktop->namedview->snap_manager; + if (bevent.state & GDK_CONTROL_MASK) { + m.setup(_desktop, true); + if (!(bevent.state & GDK_SHIFT_MASK)) { + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + } + spdc_create_single_dot(this, p, "/tools/freehand/pencil", bevent.state); + m.unSetup(); + ret = true; + break; + } + if (anchor) { + p = anchor->dp; + //Put the start overwrite curve always on the same direction + if (anchor->start) { + this->sa_overwrited = anchor->curve->create_reverse(); + } else { + this->sa_overwrited = anchor->curve->copy(); + } + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Continuing selected path")); + } else { + m.setup(_desktop, true); + if (tablet_enabled) { + // This is the first click of a new curve; deselect item so that + // this curve is not combined with it (unless it is drawn from its + // anchor, which is handled by the sibling branch above) + selection->clear(); + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path")); + } else if (!(bevent.state & GDK_SHIFT_MASK)) { + // This is the first click of a new curve; deselect item so that + // this curve is not combined with it (unless it is drawn from its + // anchor, which is handled by the sibling branch above) + selection->clear(); + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path")); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + } else if (selection->singleItem() && SP_IS_PATH(selection->singleItem())) { + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path")); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + } + m.unSetup(); + } + if (!tablet_enabled) { + this->sa = anchor; + } + this->_setStartpoint(p); + ret = true; + break; + } + + set_high_motion_precision(); + this->_is_drawing = true; + } + return ret; +} + +bool PencilTool::_handleMotionNotify(GdkEventMotion const &mevent) { + if ((mevent.state & GDK_CONTROL_MASK) && (mevent.state & GDK_BUTTON1_MASK)) { + // mouse was accidentally moved during Ctrl+click; + // ignore the motion and create a single point + this->_is_drawing = false; + return true; + } + bool ret = false; + + if ((mevent.state & GDK_BUTTON2_MASK)) { + // allow scrolling + return ret; + } + + /* Test whether we hit any anchor. */ + SPDrawAnchor *anchor = spdc_test_inside(this, pencil_drag_origin_w); + if (this->pressure == 0.0 && tablet_enabled && !anchor) { + // tablet event was accidentally fired without press; + return ret; + } + + if ( ( mevent.state & GDK_BUTTON1_MASK ) && this->_is_drawing) { + /* Grab mouse, so release will not pass unnoticed */ + grabCanvasEvents(); + } + + /* Find desktop coordinates */ + Geom::Point p = _desktop->w2d(Geom::Point(mevent.x, mevent.y)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (pencil_within_tolerance) { + gint const tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + if ( Geom::LInfty( Geom::Point(mevent.x,mevent.y) - pencil_drag_origin_w ) < tolerance ) { + return false; // 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) + pencil_within_tolerance = false; + + anchor = spdc_test_inside(this, Geom::Point(mevent.x,mevent.y)); + + switch (this->_state) { + case SP_PENCIL_CONTEXT_ADDLINE: + if (is_tablet) { + this->_state = SP_PENCIL_CONTEXT_FREEHAND; + return false; + } + /* Set red endpoint */ + if (anchor) { + p = anchor->dp; + } else { + Geom::Point ptnr(p); + this->_endpointSnap(ptnr, mevent.state); + p = ptnr; + } + this->_setEndpoint(p); + ret = true; + break; + default: + /* We may be idle or already freehand */ + if ( (mevent.state & GDK_BUTTON1_MASK) && this->_is_drawing ) { + if (this->_state == SP_PENCIL_CONTEXT_IDLE) { + this->discard_delayed_snap_event(); + } + this->_state = SP_PENCIL_CONTEXT_FREEHAND; + + if ( !this->sa && !this->green_anchor ) { + /* Create green anchor */ + this->green_anchor.reset(new SPDrawAnchor(this, this->green_curve.get(), TRUE, this->p[0])); + } + if (anchor) { + p = anchor->dp; + } + if ( this->_npoints != 0) { // buttonpress may have happened before we entered draw context! + if (this->ps.empty()) { + // Only in freehand mode we have to add the first point also to this->ps (apparently) + // - We cannot add this point in spdc_set_startpoint, because we only need it for freehand + // - We cannot do this in the button press handler because at that point we don't know yet + // whether we're going into freehand mode or not + this->ps.push_back(this->p[0]); + if (tablet_enabled) { + this->_wps.emplace_back(0, 0); + } + } + this->_addFreehandPoint(p, mevent.state, false); + ret = true; + } + if (anchor && !this->anchor_statusbar) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Release</b> here to close and finish the path.")); + this->anchor_statusbar = true; + this->ea = anchor; + } else if (!anchor && this->anchor_statusbar) { + this->message_context->clear(); + this->anchor_statusbar = false; + this->ea = nullptr; + } else if (!anchor) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("Drawing a freehand path")); + this->ea = nullptr; + } + + } else { + if (anchor && !this->anchor_statusbar) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drag</b> to continue the path from this point.")); + this->anchor_statusbar = true; + } else if (!anchor && this->anchor_statusbar) { + this->message_context->clear(); + this->anchor_statusbar = false; + } + } + + // Show the pre-snap indicator to communicate to the user where we would snap to if he/she were to + // a) press the mousebutton to start a freehand drawing, or + // b) release the mousebutton to finish a freehand drawing + if (!tablet_enabled && !this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop, true); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + } + return ret; +} + +bool PencilTool::_handleButtonRelease(GdkEventButton const &revent) { + bool ret = false; + + set_high_motion_precision(false); + + if ( revent.button == 1 && this->_is_drawing) { + this->_is_drawing = false; + + /* Find desktop coordinates */ + Geom::Point p = _desktop->w2d(Geom::Point(revent.x, revent.y)); + + /* Test whether we hit any anchor. */ + SPDrawAnchor *anchor = spdc_test_inside(this, Geom::Point(revent.x, revent.y)); + + switch (this->_state) { + case SP_PENCIL_CONTEXT_IDLE: + /* Releasing button in idle mode means single click */ + /* We have already set up start point/anchor in button_press */ + if (!(revent.state & GDK_CONTROL_MASK) && !is_tablet) { + // Ctrl+click creates a single point so only set context in ADDLINE mode when Ctrl isn't pressed + this->_state = SP_PENCIL_CONTEXT_ADDLINE; + } + /*Or select the down item if we are in tablet mode*/ + if (is_tablet) { + using namespace Inkscape::LivePathEffect; + SPItem *item = sp_event_context_find_item(_desktop, Geom::Point(revent.x, revent.y), FALSE, FALSE); + if (item && (!this->white_item || item != white_item)) { + if (SP_IS_LPE_ITEM(item)) { + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + if (lpe) { + LPEPowerStroke* ps = static_cast<LPEPowerStroke*>(lpe); + if (ps) { + _desktop->selection->clear(); + _desktop->selection->add(item); + } + } + } + } + } + break; + case SP_PENCIL_CONTEXT_ADDLINE: + /* Finish segment now */ + if (anchor) { + p = anchor->dp; + } else { + this->_endpointSnap(p, revent.state); + } + this->ea = anchor; + this->_setEndpoint(p); + this->_finishEndpoint(); + this->_state = SP_PENCIL_CONTEXT_IDLE; + this->discard_delayed_snap_event(); + break; + case SP_PENCIL_CONTEXT_FREEHAND: + if (revent.state & GDK_MOD1_MASK && !tablet_enabled) { + /* sketch mode: interpolate the sketched path and improve the current output path with the new interpolation. don't finish sketch */ + this->_sketchInterpolate(); + + this->green_anchor.reset(); + + this->_state = SP_PENCIL_CONTEXT_SKETCH; + } else { + /* Finish segment now */ + /// \todo fixme: Clean up what follows (Lauris) + if (anchor) { + p = anchor->dp; + } else { + Geom::Point p_end = p; + if (tablet_enabled) { + this->_addFreehandPoint(p_end, revent.state, true); + this->_pressure_curve->reset(); + } else { + this->_endpointSnap(p_end, revent.state); + if (p_end != p) { + // then we must have snapped! + this->_addFreehandPoint(p_end, revent.state, true); + } + } + } + + this->ea = anchor; + /* Write curves to object */ + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing freehand")); + this->_interpolate(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (tablet_enabled) { + gint shapetype = prefs->getInt("/tools/freehand/pencil/shape", 0); + gint simplify = prefs->getInt("/tools/freehand/pencil/simplify", 0); + gint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0); + prefs->setInt("/tools/freehand/pencil/shape", 0); + prefs->setInt("/tools/freehand/pencil/simplify", 0); + prefs->setInt("/tools/freehand/pencil/freehand-mode", 0); + spdc_concat_colors_and_flush(this, FALSE); + prefs->setInt("/tools/freehand/pencil/freehand-mode", mode); + prefs->setInt("/tools/freehand/pencil/simplify", simplify); + prefs->setInt("/tools/freehand/pencil/shape", shapetype); + } else { + spdc_concat_colors_and_flush(this, FALSE); + } + this->points.clear(); + this->sa = nullptr; + this->ea = nullptr; + this->ps.clear(); + this->_wps.clear(); + this->green_anchor.reset(); + this->_state = SP_PENCIL_CONTEXT_IDLE; + // reset sketch mode too + this->sketch_n = 0; + } + break; + case SP_PENCIL_CONTEXT_SKETCH: + default: + break; + } + + ungrabCanvasEvents(); + + ret = true; + } + return ret; +} + +void PencilTool::_cancel() { + ungrabCanvasEvents(); + + this->_is_drawing = false; + this->_state = SP_PENCIL_CONTEXT_IDLE; + this->discard_delayed_snap_event(); + + this->red_curve->reset(); + this->red_bpath->set_bpath(red_curve.get()); + + for (auto path : this->green_bpaths) { + delete path; + } + this->green_bpaths.clear(); + this->green_curve->reset(); + this->green_anchor.reset(); + + this->message_context->clear(); + this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled")); +} + +bool PencilTool::_handleKeyPress(GdkEventKey const &event) { + bool ret = false; + + switch (get_latin_keyval(&event)) { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // Prevent the zoom field from activation. + if (!Inkscape::UI::held_only_control(event)) { + ret = true; + } + break; + case GDK_KEY_Escape: + if (this->_npoints != 0) { + // if drawing, cancel, otherwise pass it up for deselecting + if (this->_state != SP_PENCIL_CONTEXT_IDLE) { + this->_cancel(); + ret = true; + } + } + break; + case GDK_KEY_z: + case GDK_KEY_Z: + if (Inkscape::UI::held_only_control(event) && this->_npoints != 0) { + // if drawing, cancel, otherwise pass it up for undo + if (this->_state != SP_PENCIL_CONTEXT_IDLE) { + this->_cancel(); + ret = true; + } + } + break; + case GDK_KEY_g: + case GDK_KEY_G: + if (Inkscape::UI::held_only_shift(event)) { + _desktop->selection->toGuides(); + ret = true; + } + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Meta_L: + case GDK_KEY_Meta_R: + if (this->_state == SP_PENCIL_CONTEXT_IDLE) { + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("<b>Sketch mode</b>: holding <b>Alt</b> interpolates between sketched paths. Release <b>Alt</b> to finalize.")); + } + break; + default: + break; + } + return ret; +} + +bool PencilTool::_handleKeyRelease(GdkEventKey const &event) { + bool ret = false; + + switch (get_latin_keyval(&event)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Meta_L: + case GDK_KEY_Meta_R: + if (this->_state == SP_PENCIL_CONTEXT_SKETCH) { + spdc_concat_colors_and_flush(this, FALSE); + this->sketch_n = 0; + this->sa = nullptr; + this->ea = nullptr; + this->green_anchor.reset(); + this->_state = SP_PENCIL_CONTEXT_IDLE; + this->discard_delayed_snap_event(); + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing freehand sketch")); + ret = true; + } + break; + default: + break; + } + return ret; +} + +/** + * Reset points and set new starting point. + */ +void PencilTool::_setStartpoint(Geom::Point const &p) { + this->_npoints = 0; + this->red_curve_is_valid = false; + if (in_svg_plane(p)) { + this->p[this->_npoints++] = p; + } +} + +/** + * Change moving endpoint position. + * <ul> + * <li>Ctrl constrains to moving to H/V direction, snapping in given direction. + * <li>Otherwise we snap freely to whatever attractors are available. + * </ul> + * + * Number of points is (re)set to 2 always, 2nd point is modified. + * We change RED curve. + */ +void PencilTool::_setEndpoint(Geom::Point const &p) { + if (this->_npoints == 0) { + return; + /* May occur if first point wasn't in SVG plane (e.g. weird w2d transform, perhaps from bad + * zoom setting). + */ + } + g_return_if_fail( this->_npoints > 0 ); + + this->red_curve->reset(); + if ( ( p == this->p[0] ) + || !in_svg_plane(p) ) + { + this->_npoints = 1; + } else { + this->p[1] = p; + this->_npoints = 2; + + this->red_curve->moveto(this->p[0]); + this->red_curve->lineto(this->p[1]); + this->red_curve_is_valid = true; + if (!tablet_enabled) { + red_bpath->set_bpath(red_curve.get()); + } + } +} + +/** + * Finalize addline. + * + * \todo + * fixme: I'd like remove red reset from concat colors (lauris). + * Still not sure, how it will make most sense. + */ +void PencilTool::_finishEndpoint() { + if (this->red_curve->is_unset() || + this->red_curve->first_point() == this->red_curve->second_point()) + { + this->red_curve->reset(); + if (!tablet_enabled) { + red_bpath->set_bpath(nullptr); + } + } else { + /* Write curves to object. */ + spdc_concat_colors_and_flush(this, FALSE); + this->sa = nullptr; + this->ea = nullptr; + } +} + +static inline double square(double const x) { return x * x; } + + + +void PencilTool::addPowerStrokePencil() +{ + { + SPDocument *document = _desktop->doc(); + if (!document) { + return; + } + using namespace Inkscape::LivePathEffect; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double tol = prefs->getDoubleLimited("/tools/freehand/pencil/base-simplify", 25.0, 0.0, 100.0) * 0.4; + double tolerance_sq = 0.02 * square(_desktop->w2d().descrim() * tol) * exp(0.2 * tol - 2); + int n_points = this->ps.size(); + // worst case gives us a segment per point + int max_segs = 4 * n_points; + std::vector<Geom::Point> b(max_segs); + auto curvepressure = std::make_unique<SPCurve>(); + int const n_segs = Geom::bezier_fit_cubic_r(b.data(), this->ps.data(), n_points, tolerance_sq, max_segs); + if (n_segs > 0) { + /* Fit and draw and reset state */ + curvepressure->moveto(b[0]); + for (int c = 0; c < n_segs; c++) { + curvepressure->curveto(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]); + } + } + Geom::Affine transform_coordinate = currentLayer()->i2dt_affine().inverse(); + curvepressure->transform(transform_coordinate); + Geom::Path path = curvepressure->get_pathvector()[0]; + + if (!path.empty()) { + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *pp = nullptr; + pp = xml_doc->createElement("svg:path"); + pp->setAttribute("d", sp_svg_write_path(path)); + pp->setAttribute("id", "power_stroke_preview"); + Inkscape::GC::release(pp); + + SPShape *powerpreview = SP_SHAPE(currentLayer()->appendChildRepr(pp)); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(powerpreview); + if (!lpeitem) { + return; + } + DocumentUndo::ScopedInsensitive tmp(document); + tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 0.0, 100.0) + 30; + if (tol > 30) { + tol = tol / (130.0 * (132.0 - tol)); + Inkscape::SVGOStringStream threshold; + threshold << tol; + Effect::createAndApply(SIMPLIFY, document, lpeitem); + Effect *lpe = lpeitem->getCurrentLPE(); + Inkscape::LivePathEffect::LPESimplify *simplify = + static_cast<Inkscape::LivePathEffect::LPESimplify *>(lpe); + if (simplify) { + sp_lpe_item_enable_path_effects(lpeitem, false); + Glib::ustring pref_path = "/live_effects/simplify/smooth_angles"; + bool valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + lpe->getRepr()->setAttribute("smooth_angles", "0"); + } + pref_path = "/live_effects/simplify/helper_size"; + valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + lpe->getRepr()->setAttribute("helper_size", "0"); + } + pref_path = "/live_effects/simplify/step"; + valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + lpe->getRepr()->setAttribute("step", "1"); + } + lpe->getRepr()->setAttribute("threshold", threshold.str()); + lpe->getRepr()->setAttribute("simplify_individual_paths", "false"); + lpe->getRepr()->setAttribute("simplify_just_coalesce", "false"); + sp_lpe_item_enable_path_effects(lpeitem, true); + } + sp_lpe_item_update_patheffect(lpeitem, false, true); + SPCurve const *curvepressure = powerpreview->curve(); + if (curvepressure->is_empty()) { + return; + } + path = curvepressure->get_pathvector()[0]; + } + powerStrokeInterpolate(path); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring pref_path_pp = "/live_effects/powerstroke/powerpencil"; + prefs->setBool(pref_path_pp, true); + Effect::createAndApply(POWERSTROKE, document, lpeitem); + Effect *lpe = lpeitem->getCurrentLPE(); + Inkscape::LivePathEffect::LPEPowerStroke *pspreview = static_cast<LPEPowerStroke *>(lpe); + if (pspreview) { + sp_lpe_item_enable_path_effects(lpeitem, false); + Glib::ustring pref_path = "/live_effects/powerstroke/interpolator_type"; + bool valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + pspreview->getRepr()->setAttribute("interpolator_type", "CentripetalCatmullRom"); + } + pref_path = "/live_effects/powerstroke/linejoin_type"; + valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + pspreview->getRepr()->setAttribute("linejoin_type", "spiro"); + } + pref_path = "/live_effects/powerstroke/interpolator_beta"; + valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + pspreview->getRepr()->setAttribute("interpolator_beta", "0.75"); + } + gint cap = prefs->getInt("/live_effects/powerstroke/powerpencilcap", 2); + pspreview->getRepr()->setAttribute("start_linecap_type", LineCapTypeConverter.get_key(cap)); + pspreview->getRepr()->setAttribute("end_linecap_type", LineCapTypeConverter.get_key(cap)); + pspreview->getRepr()->setAttribute("sort_points", "true"); + pspreview->getRepr()->setAttribute("not_jump", "true"); + pspreview->offset_points.param_set_and_write_new_value(this->points); + sp_lpe_item_enable_path_effects(lpeitem, true); + sp_lpe_item_update_patheffect(lpeitem, false, true); + pp->setAttribute("style", "fill:#888888;opacity:1;fill-rule:nonzero;stroke:none;"); + } + prefs->setBool(pref_path_pp, false); + } + } +} + +/** + * Add a virtual point to the future pencil path. + * + * @param p the point to add. + * @param state event state + * @param last the point is the last of the user stroke. + */ +void PencilTool::_addFreehandPoint(Geom::Point const &p, guint /*state*/, bool last) +{ + g_assert( this->_npoints > 0 ); + g_return_if_fail(unsigned(this->_npoints) < G_N_ELEMENTS(this->p)); + + double distance = 0; + if ( ( p != this->p[ this->_npoints - 1 ] ) + && in_svg_plane(p) ) + { + this->p[this->_npoints++] = p; + this->_fitAndSplit(); + if (tablet_enabled) { + distance = Geom::distance(p, this->ps.back()) + this->_wps.back()[Geom::X]; + } + this->ps.push_back(p); + } + if (tablet_enabled && in_svg_plane(p)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double min = prefs->getIntLimited("/tools/freehand/pencil/minpressure", 0, 0, 100) / 100.0; + double max = prefs->getIntLimited("/tools/freehand/pencil/maxpressure", 30, 0, 100) / 100.0; + if (min > max) { + min = max; + } + double dezoomify_factor = 0.05 * 1000 / _desktop->current_zoom(); + double const pressure_shrunk = pressure * (max - min) + min; // C++20 -> use std::lerp() + double pressure_computed = std::abs(pressure_shrunk * dezoomify_factor); + double pressure_computed_scaled = std::abs(pressure_computed * _desktop->getDocument()->getDocumentScale().inverse()[Geom::X]); + if (p != this->p[this->_npoints - 1]) { + this->_wps.emplace_back(distance, pressure_computed_scaled); + } + if (pressure_computed) { + Geom::Circle pressure_dot(p, pressure_computed); + Geom::Piecewise<Geom::D2<Geom::SBasis>> pressure_piecewise; + pressure_piecewise.push_cut(0); + pressure_piecewise.push(pressure_dot.toSBasis(), 1); + Geom::PathVector pressure_path = Geom::path_from_piecewise(pressure_piecewise, 0.1); + Geom::PathVector previous_presure = this->_pressure_curve->get_pathvector(); + if (!pressure_path.empty() && !previous_presure.empty()) { + pressure_path = sp_pathvector_boolop(pressure_path, previous_presure, bool_op_union, fill_nonZero, fill_nonZero); + } + this->_pressure_curve->set_pathvector(pressure_path); + red_bpath->set_bpath(_pressure_curve.get()); + } + if (last) { + this->addPowerStrokePencil(); + } + } +} + +void PencilTool::powerStrokeInterpolate(Geom::Path const path) +{ + size_t ps_size = this->ps.size(); + if ( ps_size <= 1 ) { + return; + } + + using Geom::X; + using Geom::Y; + gint path_size = path.size(); + std::vector<Geom::Point> tmp_points; + Geom::Point previous = Geom::Point(Geom::infinity(), 0); + bool increase = false; + size_t i = 0; + double dezoomify_factor = 0.05 * 1000 / _desktop->current_zoom(); + double limit = 6 * dezoomify_factor; + double max = + std::max(this->_wps.back()[Geom::X] - (this->_wps.back()[Geom::X] / 10), this->_wps.back()[Geom::X] - limit); + double min = std::min(this->_wps.back()[Geom::X] / 10, limit); + double original_lenght = this->_wps.back()[Geom::X]; + double max10 = 0; + double min10 = 0; + for (auto wps : this->_wps) { + i++; + Geom::Coord pressure = wps[Geom::Y]; + max10 = max10 > pressure ? max10 : pressure; + min10 = min10 <= pressure ? min10 : pressure; + if (!original_lenght || wps[Geom::X] > max) { + break; + } + if (wps[Geom::Y] == 0 || wps[Geom::X] < min) { + continue; + } + if (previous[Geom::Y] < (max10 + min10) / 2.0) { + if (increase && tmp_points.size() > 1) { + tmp_points.pop_back(); + } + wps[Geom::Y] = max10; + tmp_points.push_back(wps); + increase = true; + } else { + if (!increase && tmp_points.size() > 1) { + tmp_points.pop_back(); + } + wps[Geom::Y] = min10; + tmp_points.push_back(wps); + increase = false; + } + previous = wps; + max10 = 0; + min10 = 999999999; + } + this->points.clear(); + double prev_pressure = 0; + for (auto point : tmp_points) { + point[Geom::X] /= (double)original_lenght; + point[Geom::X] *= path_size; + if (std::abs(point[Geom::Y] - prev_pressure) > point[Geom::Y] / 10.0) { + this->points.push_back(point); + prev_pressure = point[Geom::Y]; + } + } + if (points.empty() && !_wps.empty()) { + // Synthesize a pressure data point based on the average pressure + double average_pressure = std::accumulate(_wps.begin(), _wps.end(), 0.0, + [](double const &sum_so_far, Geom::Point const &point) -> double { + return sum_so_far + point[Geom::Y]; + }) / (double)_wps.size(); + points.emplace_back(0.5 * path.size(), /* place halfway along the path */ + 2.0 * average_pressure /* 2.0 - for correct average thickness of a kite */); + } +} + +void PencilTool::_interpolate() { + size_t ps_size = this->ps.size(); + if ( ps_size <= 1 ) { + return; + } + using Geom::X; + using Geom::Y; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 0.0, 100.0) * 0.4; + bool simplify = prefs->getInt("/tools/freehand/pencil/simplify", 0); + if(simplify){ + double tol2 = prefs->getDoubleLimited("/tools/freehand/pencil/base-simplify", 25.0, 0.0, 100.0) * 0.4; + tol = std::min(tol,tol2); + } + this->green_curve->reset(); + this->red_curve->reset(); + this->red_curve_is_valid = false; + + double tolerance_sq = 0.02 * square(_desktop->w2d().descrim() * tol) * exp(0.2 * tol - 2); + + g_assert(is_zero(this->_req_tangent) || is_unit_vector(this->_req_tangent)); + + int n_points = this->ps.size(); + + // worst case gives us a segment per point + int max_segs = 4 * n_points; + + std::vector<Geom::Point> b(max_segs); + int const n_segs = Geom::bezier_fit_cubic_r(b.data(), this->ps.data(), n_points, tolerance_sq, max_segs); + if (n_segs > 0) { + /* Fit and draw and reset state */ + this->green_curve->moveto(b[0]); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0); + for (int c = 0; c < n_segs; c++) { + // if we are in BSpline we modify the trace to create adhoc nodes + if (mode == 2) { + Geom::Point point_at1 = b[4 * c + 0] + (1./3) * (b[4 * c + 3] - b[4 * c + 0]); + point_at1 = Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP); + Geom::Point point_at2 = b[4 * c + 3] + (1./3) * (b[4 * c + 0] - b[4 * c + 3]); + point_at2 = Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP); + this->green_curve->curveto(point_at1,point_at2,b[4*c+3]); + } else { + if (!tablet_enabled || c != n_segs - 1) { + this->green_curve->curveto(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]); + } else { + std::optional<Geom::Point> finalp = this->green_curve->last_point(); + if (this->green_curve->nodes_in_path() > 4 && Geom::are_near(*finalp, b[4 * c + 3], 10.0)) { + this->green_curve->backspace(); + this->green_curve->curveto(*finalp, b[4 * c + 3], b[4 * c + 3]); + } else { + this->green_curve->curveto(b[4 * c + 1], b[4 * c + 3], b[4 * c + 3]); + } + } + } + } + if (!tablet_enabled) { + red_bpath->set_bpath(green_curve.get()); + } + + /* Fit and draw and copy last point */ + g_assert(!this->green_curve->is_empty()); + + /* Set up direction of next curve. */ + { + Geom::Curve const * last_seg = this->green_curve->last_segment(); + g_assert( last_seg ); // Relevance: validity of (*last_seg) + this->p[0] = last_seg->finalPoint(); + this->_npoints = 1; + Geom::Curve *last_seg_reverse = last_seg->reverse(); + Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) ); + delete last_seg_reverse; + this->_req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) ) + ? Geom::Point(0, 0) + : Geom::unit_vector(req_vec) ); + } + } +} + + +/* interpolates the sketched curve and tweaks the current sketch interpolation*/ +void PencilTool::_sketchInterpolate() { + if ( this->ps.size() <= 1 ) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0) * 0.4; + bool simplify = prefs->getInt("/tools/freehand/pencil/simplify", 0); + if(simplify){ + double tol2 = prefs->getDoubleLimited("/tools/freehand/pencil/base-simplify", 25.0, 1.0, 100.0) * 0.4; + tol = std::min(tol,tol2); + } + double tolerance_sq = 0.02 * square(_desktop->w2d().descrim() * tol) * exp(0.2 * tol - 2); + + bool average_all_sketches = prefs->getBool("/tools/freehand/pencil/average_all_sketches", true); + + g_assert(is_zero(this->_req_tangent) || is_unit_vector(this->_req_tangent)); + + this->red_curve->reset(); + this->red_curve_is_valid = false; + + int n_points = this->ps.size(); + + // worst case gives us a segment per point + int max_segs = 4 * n_points; + + std::vector<Geom::Point> b(max_segs); + + int const n_segs = Geom::bezier_fit_cubic_r(b.data(), this->ps.data(), n_points, tolerance_sq, max_segs); + + if (n_segs > 0) { + Geom::Path fit(b[0]); + + for (int c = 0; c < n_segs; c++) { + fit.appendNew<Geom::CubicBezier>(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]); + } + + Geom::Piecewise<Geom::D2<Geom::SBasis> > fit_pwd2 = fit.toPwSb(); + + if (this->sketch_n > 0) { + double t; + + if (average_all_sketches) { + // Average = (sum of all) / n + // = (sum of all + new one) / n+1 + // = ((old average)*n + new one) / n+1 + t = this->sketch_n / (this->sketch_n + 1.); + } else { + t = 0.5; + } + + this->sketch_interpolation = Geom::lerp(t, fit_pwd2, this->sketch_interpolation); + + // simplify path, to eliminate small segments + Path path; + path.LoadPathVector(Geom::path_from_piecewise(this->sketch_interpolation, 0.01)); + path.Simplify(0.5); + + Geom::PathVector pathv = path.MakePathVector(); + this->sketch_interpolation = pathv[0].toPwSb(); + } else { + this->sketch_interpolation = fit_pwd2; + } + + this->sketch_n++; + + this->green_curve->reset(); + this->green_curve->set_pathvector(Geom::path_from_piecewise(this->sketch_interpolation, 0.01)); + if (!tablet_enabled) { + red_bpath->set_bpath(green_curve.get()); + } + /* Fit and draw and copy last point */ + g_assert(!this->green_curve->is_empty()); + + /* Set up direction of next curve. */ + { + Geom::Curve const * last_seg = this->green_curve->last_segment(); + g_assert( last_seg ); // Relevance: validity of (*last_seg) + this->p[0] = last_seg->finalPoint(); + this->_npoints = 1; + Geom::Curve *last_seg_reverse = last_seg->reverse(); + Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) ); + delete last_seg_reverse; + this->_req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) ) + ? Geom::Point(0, 0) + : Geom::unit_vector(req_vec) ); + } + } + + this->ps.clear(); + this->points.clear(); + this->_wps.clear(); +} + +void PencilTool::_fitAndSplit() { + g_assert( this->_npoints > 1 ); + + double const tolerance_sq = 0; + + Geom::Point b[4]; + g_assert(is_zero(this->_req_tangent) + || is_unit_vector(this->_req_tangent)); + Geom::Point const tHatEnd(0, 0); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const n_segs = Geom::bezier_fit_cubic_full(b, nullptr, this->p, this->_npoints, + this->_req_tangent, tHatEnd, + tolerance_sq, 1); + if ( n_segs > 0 + && unsigned(this->_npoints) < G_N_ELEMENTS(this->p) ) + { + /* Fit and draw and reset state */ + + this->red_curve->reset(); + this->red_curve->moveto(b[0]); + using Geom::X; + using Geom::Y; + // if we are in BSpline we modify the trace to create adhoc nodes + guint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0); + if(mode == 2){ + Geom::Point point_at1 = b[0] + (1./3)*(b[3] - b[0]); + point_at1 = Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP); + Geom::Point point_at2 = b[3] + (1./3)*(b[0] - b[3]); + point_at2 = Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP); + this->red_curve->curveto(point_at1,point_at2,b[3]); + }else{ + this->red_curve->curveto(b[1], b[2], b[3]); + } + if (!tablet_enabled) { + red_bpath->set_bpath(red_curve.get()); + } + this->red_curve_is_valid = true; + } else { + /* Fit and draw and copy last point */ + + g_assert(!this->red_curve->is_empty()); + + /* Set up direction of next curve. */ + { + Geom::Curve const * last_seg = this->red_curve->last_segment(); + g_assert( last_seg ); // Relevance: validity of (*last_seg) + this->p[0] = last_seg->finalPoint(); + this->_npoints = 1; + Geom::Curve *last_seg_reverse = last_seg->reverse(); + Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) ); + delete last_seg_reverse; + this->_req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) ) + ? Geom::Point(0, 0) + : Geom::unit_vector(req_vec) ); + } + + green_curve->append_continuous(*red_curve); + auto curve = this->red_curve->copy(); + + /// \todo fixme: + + auto layer = _desktop->layerManager().currentLayer(); + this->highlight_color = layer->highlight_color(); + if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){ + this->green_color = 0x00ff007f; + } else { + this->green_color = this->highlight_color; + } + + auto cshape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), curve.get(), true); + cshape->set_stroke(green_color); + cshape->set_fill(0x0, SP_WIND_RULE_NONZERO); + + this->green_bpaths.push_back(cshape); + + this->red_curve_is_valid = false; + } +} + +} +} +} + +/* + 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/tools/pencil-tool.h b/src/ui/tools/pencil-tool.h new file mode 100644 index 0000000..e9443ea --- /dev/null +++ b/src/ui/tools/pencil-tool.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * PencilTool: a context for pencil tool events + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_PENCIL_CONTEXT_H +#define SEEN_PENCIL_CONTEXT_H + + +#include "ui/tools/freehand-base.h" + +#include <2geom/piecewise.h> +#include <2geom/d2.h> +#include <2geom/sbasis.h> +#include <2geom/pathvector.h> +// #include <future> + +#include <memory> + +class SPShape; + +#define DDC_MIN_PRESSURE 0.0 +#define DDC_MAX_PRESSURE 1.0 +#define DDC_DEFAULT_PRESSURE 1.0 +#define SP_PENCIL_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PencilTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_PENCIL_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::PencilTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +enum PencilState { + SP_PENCIL_CONTEXT_IDLE, + SP_PENCIL_CONTEXT_ADDLINE, + SP_PENCIL_CONTEXT_FREEHAND, + SP_PENCIL_CONTEXT_SKETCH +}; + +/** + * PencilTool: a context for pencil tool events + */ +class PencilTool : public FreehandBase { +public: + PencilTool(SPDesktop *desktop); + ~PencilTool() override; + + Geom::Point p[16]; + std::vector<Geom::Point> ps; + std::vector<Geom::Point> points; + void addPowerStrokePencil(); + void powerStrokeInterpolate(Geom::Path const path); + Geom::Piecewise<Geom::D2<Geom::SBasis> > sketch_interpolation; // the current proposal from the sketched paths + unsigned sketch_n; // number of sketches done + +protected: + bool root_handler(GdkEvent* event) override; +private: + bool _handleButtonPress(GdkEventButton const &bevent); + bool _handleMotionNotify(GdkEventMotion const &mevent); + bool _handleButtonRelease(GdkEventButton const &revent); + bool _handleKeyPress(GdkEventKey const &event); + bool _handleKeyRelease(GdkEventKey const &event); + void _setStartpoint(Geom::Point const &p); + void _setEndpoint(Geom::Point const &p); + void _finishEndpoint(); + void _addFreehandPoint(Geom::Point const &p, guint state, bool last); + void _fitAndSplit(); + void _interpolate(); + void _sketchInterpolate(); + void _extinput(GdkEvent *event); + void _cancel(); + void _endpointSnap(Geom::Point &p, guint const state); + std::vector<Geom::Point> _wps; + std::unique_ptr<SPCurve> _pressure_curve; + Geom::Point _req_tangent; + bool _is_drawing; + PencilState _state; + gint _npoints; + // std::future<bool> future; +}; + +} +} +} + +#endif /* !SEEN_PENCIL_CONTEXT_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/tools/rect-tool.cpp b/src/ui/tools/rect-tool.cpp new file mode 100644 index 0000000..cbbbbab --- /dev/null +++ b/src/ui/tools/rect-tool.cpp @@ -0,0 +1,465 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Rectangle drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2000-2005 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "include/macros.h" +#include "message-context.h" +#include "selection-chemistry.h" +#include "selection.h" + +#include "object/sp-rect.h" +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/shape-editor.h" +#include "ui/tools/rect-tool.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +RectTool::RectTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/shapes/rect", "rect.svg") + , rect(nullptr) + , rx(0) + , ry(0) +{ + this->shape_editor = new ShapeEditor(desktop); + + SPItem *item = desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = desktop->getSelection()->connectChanged( + sigc::mem_fun(this, &RectTool::selection_changed) + ); + + sp_event_context_read(this, "rx"); + sp_event_context_read(this, "ry"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +RectTool::~RectTool() { + ungrabCanvasEvents(); + + this->finishItem(); + this->enableGrDrag(false); + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->rect) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + */ +void RectTool::selection_changed(Inkscape::Selection* selection) { + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); +} + +void RectTool::set(const Inkscape::Preferences::Entry& val) { + /* fixme: Proper error handling for non-numeric data. Use a locale-independent function like + * g_ascii_strtod (or a thin wrapper that does the right thing for invalid values inf/nan). */ + Glib::ustring name = val.getEntryName(); + + if ( name == "rx" ) { + this->rx = val.getDoubleLimited(); // prevents NaN and +/-Inf from messing up + } else if ( name == "ry" ) { + this->ry = val.getDoubleLimited(); + } +} + +bool RectTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if ( event->button.button == 1) { + this->setup_for_drag_start(event); + } + break; + // motion and release are always on root (why?) + default: + break; + } + + ret = ToolBase::item_handler(item, event); + + return ret; +} + +bool RectTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = _desktop->getSelection(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + Geom::Point const button_w(event->button.x, event->button.y); + + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + // remember clicked item, disregarding groups, honoring Alt + this->item_to_select = sp_event_context_find_item (_desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + + dragging = true; + + /* Position center */ + Geom::Point button_dt(_desktop->w2d(button_w)); + this->center = button_dt; + + /* Snap center */ + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + this->center = button_dt; + + grabCanvasEvents(); + ret = TRUE; + } + break; + case GDK_MOTION_NOTIFY: + if ( dragging + && (event->motion.state & GDK_BUTTON1_MASK)) + { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->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 draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + + this->drag(motion_dt, event->motion.state); // this will also handle the snapping + gobble_motion_events(GDK_BUTTON1_MASK); + ret = TRUE; + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + if (event->button.button == 1) { + dragging = false; + this->discard_delayed_snap_event(); + + if (!this->within_tolerance) { + // we've been dragging, finish the rect + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = nullptr; + ret = TRUE; + ungrabCanvasEvents(); + } + break; + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + if (!dragging){ + sp_event_show_modifier_tip (this->defaultMessageContext(), event, + _("<b>Ctrl</b>: make square or integer-ratio rect, lock a rounded corner circular"), + _("<b>Shift</b>: draw around the starting point"), + nullptr); + } + break; + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("rect-width"); + ret = TRUE; + } + break; + + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + _desktop->selection->toGuides(); + ret = true; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + this->discard_delayed_snap_event(); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_space: + if (dragging) { + ungrabCanvasEvents(); + dragging = false; + this->discard_delayed_snap_event(); + + if (!this->within_tolerance) { + // we've been dragging, finish the rect + this->finishItem(); + } + // do not return true, so that space would work switching to selector + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + case GDK_KEY_RELEASE: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + default: + break; + } + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void RectTool::drag(Geom::Point const pt, guint state) { + if (!this->rect) { + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:rect"); + + // Set style + sp_desktop_apply_style_tool(_desktop, repr, "/tools/shapes/rect", false); + + this->rect = SP_RECT(currentLayer()->appendChildRepr(repr)); + Inkscape::GC::release(repr); + + this->rect->transform = currentLayer()->i2doc_affine().inverse(); + this->rect->updateRepr(); + } + + Geom::Rect const r = Inkscape::snap_rectangular_box(_desktop, this->rect, pt, this->center, state); + + this->rect->setPosition(r.min()[Geom::X], r.min()[Geom::Y], r.dimensions()[Geom::X], r.dimensions()[Geom::Y]); + + if (this->rx != 0.0) { + this->rect->setRx(true, this->rx); + } + + if (this->ry != 0.0) { + if (this->rx == 0.0) + this->rect->setRy(true, CLAMP(this->ry, 0, MIN(r.dimensions()[Geom::X], r.dimensions()[Geom::Y])/2)); + else + this->rect->setRy(true, CLAMP(this->ry, 0, r.dimensions()[Geom::Y])); + } + + // status text + double rdimx = r.dimensions()[Geom::X]; + double rdimy = r.dimensions()[Geom::Y]; + + Inkscape::Util::Quantity rdimx_q = Inkscape::Util::Quantity(rdimx, "px"); + Inkscape::Util::Quantity rdimy_q = Inkscape::Util::Quantity(rdimy, "px"); + Glib::ustring xs = rdimx_q.string(_desktop->namedview->display_units); + Glib::ustring ys = rdimy_q.string(_desktop->namedview->display_units); + + if (state & GDK_CONTROL_MASK) { + int ratio_x, ratio_y; + bool is_golden_ratio = false; + + if (fabs (rdimx) > fabs (rdimy)) { + if (fabs(rdimx / rdimy - goldenratio) < 1e-6) { + is_golden_ratio = true; + } + + ratio_x = (int) rint (rdimx / rdimy); + ratio_y = 1; + } else { + if (fabs(rdimy / rdimx - goldenratio) < 1e-6) { + is_golden_ratio = true; + } + + ratio_x = 1; + ratio_y = (int) rint (rdimy / rdimx); + } + + if (!is_golden_ratio) { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s (constrained to ratio %d:%d); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str(), ratio_x, ratio_y); + } else { + if (ratio_y == 1) { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s (constrained to golden ratio 1.618 : 1); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s (constrained to golden ratio 1 : 1.618); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } + } + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s; with <b>Ctrl</b> to make square, integer-ratio, or golden-ratio rectangle; with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } +} + +void RectTool::finishItem() { + this->message_context->clear(); + + if (this->rect != nullptr) { + if (this->rect->width.computed == 0 || this->rect->height.computed == 0) { + this->cancel(); // Don't allow the creating of zero sized rectangle, for example when the start and and point snap to the snap grid point + return; + } + + this->rect->updateRepr(); + this->rect->doWriteTransform(this->rect->transform, nullptr, true); + + _desktop->getSelection()->set(this->rect); + + DocumentUndo::done(_desktop->getDocument(), _("Create rectangle"), INKSCAPE_ICON("draw-rectangle")); + + this->rect = nullptr; + } +} + +void RectTool::cancel(){ + _desktop->getSelection()->clear(); + ungrabCanvasEvents(); + + if (this->rect != nullptr) { + this->rect->deleteObject(); + this->rect = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + DocumentUndo::cancel(_desktop->getDocument()); +} + +} +} +} + +/* + 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/tools/rect-tool.h b/src/ui/tools/rect-tool.h new file mode 100644 index 0000000..79d1a8a --- /dev/null +++ b/src/ui/tools/rect-tool.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_RECT_CONTEXT_H__ +#define __SP_RECT_CONTEXT_H__ + +/* + * Rectangle drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +class SPRect; + +namespace Inkscape { + +class Selection; + +namespace UI { +namespace Tools { + +class RectTool : public ToolBase { +public: + RectTool(SPDesktop *desktop); + ~RectTool() override; + + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; +private: + SPRect *rect; + Geom::Point center; + + gdouble rx; /* roundness radius (x direction) */ + gdouble ry; /* roundness radius (y direction) */ + + sigc::connection sel_changed_connection; + + void drag(Geom::Point const pt, guint state); + void finishItem(); + void cancel(); + void selection_changed(Inkscape::Selection* selection); +}; + +} +} +} + +#endif diff --git a/src/ui/tools/select-tool.cpp b/src/ui/tools/select-tool.cpp new file mode 100644 index 0000000..0e80c3d --- /dev/null +++ b/src/ui/tools/select-tool.cpp @@ -0,0 +1,1146 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Selection and transformation context + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 authors + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 1999-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cstring> +#include <string> + +#include <gtkmm/widget.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "include/macros.h" +#include "layer-manager.h" +#include "message-stack.h" +#include "rubberband.h" +#include "selection-chemistry.h" +#include "selection-describer.h" +#include "selection.h" +#include "seltrans.h" + +#include "actions/actions-tools.h" // set_active_tool() + +#include "display/drawing-item.h" +#include "display/control/canvas-item-catchall.h" +#include "display/control/canvas-item-drawing.h" +#include "display/control/snap-indicator.h" + +#include "object/box3d.h" +#include "style.h" + +#include "ui/cursor-utils.h" +#include "ui/modifiers.h" + +#include "ui/tools/select-tool.h" + +#include "ui/widget/canvas.h" + + +using Inkscape::DocumentUndo; +using Inkscape::Modifiers::Modifier; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static gint rb_escaped = 0; // if non-zero, rubberband was canceled by esc, so the next button release should not deselect +static gint drag_escaped = 0; // if non-zero, drag was canceled by esc +static bool is_cycling = false; + +SelectTool::SelectTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/select", "select.svg") + , dragging(false) + , _force_dragging(false) + , _alt_on(false) + , moved(false) + , button_press_state(0) + , cycling_wrap(true) + , item(nullptr) + , _seltrans(nullptr) + , _describer(nullptr) +{ + auto select_click = Modifier::get(Modifiers::Type::SELECT_ADD_TO)->get_label(); + auto select_scroll = Modifier::get(Modifiers::Type::SELECT_CYCLE)->get_label(); + + // cursors in select context + _default_cursor = "select.svg"; + + no_selection_msg = g_strdup_printf( + _("No objects selected. Click, %s+click, %s+scroll mouse on top of objects, or drag around objects to select."), + select_click.c_str(), select_scroll.c_str()); + + this->_describer = new Inkscape::SelectionDescriber( + desktop->selection, + desktop->messageStack(), + _("Click selection again to toggle scale/rotation handles"), + no_selection_msg); + + this->_seltrans = new Inkscape::SelTrans(desktop); + + sp_event_context_read(this, "show"); + sp_event_context_read(this, "transform"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/select/gradientdrag")) { + this->enableGrDrag(); + } +} + +SelectTool::~SelectTool() +{ + this->enableGrDrag(false); + + if (grabbed) { + grabbed->ungrab(); + grabbed = nullptr; + } + + delete this->_seltrans; + this->_seltrans = nullptr; + + delete this->_describer; + this->_describer = nullptr; + g_free(no_selection_msg); + + if (item) { + sp_object_unref(item); + item = nullptr; + } +} + +void SelectTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "show") { + if (val.getString() == "outline") { + this->_seltrans->setShow(Inkscape::SelTrans::SHOW_OUTLINE); + } else { + this->_seltrans->setShow(Inkscape::SelTrans::SHOW_CONTENT); + } + } +} + +bool SelectTool::sp_select_context_abort() { + Inkscape::SelTrans *seltrans = this->_seltrans; + + if (this->dragging) { + if (this->moved) { // cancel dragging an object + seltrans->ungrab(); + this->moved = FALSE; + this->dragging = FALSE; + this->discard_delayed_snap_event(); + drag_escaped = 1; + + if (this->item) { + // only undo if the item is still valid + if (this->item->document) { + DocumentUndo::undo(_desktop->getDocument()); + } + + sp_object_unref( this->item, nullptr); + } + this->item = nullptr; + + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Move canceled.")); + return true; + } + } else { + if (Inkscape::Rubberband::get(_desktop)->is_started()) { + Inkscape::Rubberband::get(_desktop)->stop(); + rb_escaped = 1; + defaultMessageContext()->clear(); + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Selection canceled.")); + return true; + } + } + return false; +} + +static bool +key_is_a_modifier (guint key) { + return (key == GDK_KEY_Alt_L || + key == GDK_KEY_Alt_R || + key == GDK_KEY_Control_L || + key == GDK_KEY_Control_R || + key == GDK_KEY_Shift_L || + key == GDK_KEY_Shift_R || + key == GDK_KEY_Meta_L || // Meta is when you press Shift+Alt (at least on my machine) + key == GDK_KEY_Meta_R); +} + +static void +sp_select_context_up_one_layer(SPDesktop *desktop) +{ + /* Click in empty place, go up one level -- but don't leave a layer to root. + * + * (Rationale: we don't usually allow users to go to the root, since that + * detracts from the layer metaphor: objects at the root level can in front + * of or behind layers. Whereas it's fine to go to the root if editing + * a document that has no layers (e.g. a non-Inkscape document).) + * + * Once we support editing SVG "islands" (e.g. <svg> embedded in an xhtml + * document), we might consider further restricting the below to disallow + * leaving a layer to go to a non-layer. + */ + if (SPObject *const current_layer = desktop->layerManager().currentLayer()) { + SPObject *const parent = current_layer->parent; + SPGroup *current_group = dynamic_cast<SPGroup *>(current_layer); + if ( parent + && ( parent->parent + || !( current_group + && ( SPGroup::LAYER == current_group->layerMode() ) ) ) ) + { + desktop->layerManager().setCurrentLayer(parent); + if (current_group && (SPGroup::LAYER != current_group->layerMode())) { + desktop->getSelection()->set(current_layer); + } + } + } +} + +bool SelectTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + // make sure we still have valid objects to move around + if (this->item && this->item->document == nullptr) { + this->sp_select_context_abort(); + } + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + /* Left mousebutton */ + + // save drag origin + xp = (gint) event->button.x; + yp = (gint) event->button.y; + within_tolerance = true; + + // remember what modifiers were on before button press + this->button_press_state = event->button.state; + bool first_hit = Modifier::get(Modifiers::Type::SELECT_FIRST_HIT)->active(this->button_press_state); + bool force_drag = Modifier::get(Modifiers::Type::SELECT_FORCE_DRAG)->active(this->button_press_state); + bool always_box = Modifier::get(Modifiers::Type::SELECT_ALWAYS_BOX)->active(this->button_press_state); + bool touch_path = Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->active(this->button_press_state); + + // if shift or ctrl was pressed, do not move objects; + // pass the event to root handler which will perform rubberband, shift-click, ctrl-click, ctrl-drag + if (!(always_box || first_hit || touch_path)) { + + this->dragging = TRUE; + this->moved = FALSE; + + this->set_cursor("select-dragging.svg"); + + // remember the clicked item in this->item: + if (this->item) { + sp_object_unref(this->item, nullptr); + this->item = nullptr; + } + + this->item = sp_event_context_find_item (_desktop, Geom::Point(event->button.x, event->button.y), force_drag, FALSE); + sp_object_ref(this->item, nullptr); + + rb_escaped = drag_escaped = 0; + + if (grabbed) { + grabbed->ungrab(); + grabbed = nullptr; + } + + grabbed = _desktop->getCanvasDrawing(); + grabbed->grab(Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK ); + + ret = TRUE; + } + } else if (event->button.button == 3 && !this->dragging) { + // right click; do not eat it so that right-click menu can appear, but cancel dragging & rubberband + this->sp_select_context_abort(); + } + break; + + + case GDK_ENTER_NOTIFY: { + if (!dragging && !_alt_on && !_desktop->isWaitingCursor()) { + this->set_cursor("select-mouseover.svg"); + } + break; + } + case GDK_LEAVE_NOTIFY: + if (!dragging && !_force_dragging && !_desktop->isWaitingCursor()) { + this->set_cursor("select.svg"); + } + break; + + case GDK_KEY_PRESS: + if (get_latin_keyval (&event->key) == GDK_KEY_space) { + if (this->dragging && this->grabbed) { + /* stamping mode: show content mode moving */ + _seltrans->stamp(); + ret = TRUE; + } + } else if (get_latin_keyval (&event->key) == GDK_KEY_Tab) { + if (this->dragging && this->grabbed) { + _seltrans->getNextClosestPoint(false); + } else { + sp_selection_item_next(_desktop); + } + ret = TRUE; + } else if (get_latin_keyval (&event->key) == GDK_KEY_ISO_Left_Tab) { + if (this->dragging && this->grabbed) { + _seltrans->getNextClosestPoint(true); + } else { + sp_selection_item_prev(_desktop); + } + ret = TRUE; + } + break; + + case GDK_BUTTON_RELEASE: + case GDK_KEY_RELEASE: + if (_alt_on) { + _default_cursor = "select-mouseover.svg"; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::item_handler(item, event); + } + + return ret; +} + +void SelectTool::sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event) { + if ( this->cycling_items.empty() ) + return; + + Inkscape::DrawingItem *arenaitem; + + if(cycling_cur_item) { + arenaitem = cycling_cur_item->get_arenaitem(_desktop->dkey); + arenaitem->setOpacity(0.3); + } + + // Find next item and activate it + + + std::vector<SPItem *>::iterator next = cycling_items.end(); + + if ((scroll_event->direction == GDK_SCROLL_UP) || + (scroll_event->direction == GDK_SCROLL_SMOOTH && scroll_event->delta_y < 0)) { + if (! cycling_cur_item) { + next = cycling_items.begin(); + } else { + next = std::find( cycling_items.begin(), cycling_items.end(), cycling_cur_item ); + g_assert (next != cycling_items.end()); + ++next; + if (next == cycling_items.end()) { + if ( cycling_wrap ) { + next = cycling_items.begin(); + } else { + --next; + } + } + } + } else { + if (! cycling_cur_item) { + next = cycling_items.end(); + --next; + } else { + next = std::find( cycling_items.begin(), cycling_items.end(), cycling_cur_item ); + g_assert (next != cycling_items.end()); + if (next == cycling_items.begin()){ + if ( cycling_wrap ) { + next = cycling_items.end(); + --next; + } + } else { + --next; + } + } + } + + this->cycling_cur_item = *next; + g_assert(next != cycling_items.end()); + g_assert(cycling_cur_item != nullptr); + + arenaitem = cycling_cur_item->get_arenaitem(_desktop->dkey); + arenaitem->setOpacity(1.0); + + if (Modifier::get(Modifiers::Type::SELECT_ADD_TO)->active(scroll_event->state)) { + selection->add(cycling_cur_item); + } else { + selection->set(cycling_cur_item); + } +} + +void SelectTool::sp_select_context_reset_opacities() { + for (auto item : this->cycling_items_cmp) { + if (item) { + Inkscape::DrawingItem *arenaitem = item->get_arenaitem(_desktop->dkey); + arenaitem->setOpacity(SP_SCALE24_TO_FLOAT(item->style->opacity.value)); + } else { + g_assert_not_reached(); + } + } + + this->cycling_items_cmp.clear(); + this->cycling_cur_item = nullptr; +} + +bool SelectTool::root_handler(GdkEvent* event) { + SPItem *item = nullptr; + SPItem *item_at_point = nullptr, *group_at_point = nullptr, *item_in_group = nullptr; + gint ret = FALSE; + + Inkscape::Selection *selection = _desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // make sure we still have valid objects to move around + if (this->item && this->item->document == nullptr) { + this->sp_select_context_abort(); + } + + switch (event->type) { + case GDK_2BUTTON_PRESS: + if (event->button.button == 1) { + if (!selection->isEmpty()) { + SPItem *clicked_item = selection->items().front(); + + if (dynamic_cast<SPGroup *>(clicked_item) && !dynamic_cast<SPBox3D *>(clicked_item)) { // enter group if it's not a 3D box + _desktop->layerManager().setCurrentLayer(clicked_item); + _desktop->getSelection()->clear(); + this->dragging = false; + this->discard_delayed_snap_event(); + + } else { // switch tool + Geom::Point const button_pt(event->button.x, event->button.y); + Geom::Point const p(_desktop->w2d(button_pt)); + set_active_tool(_desktop, clicked_item, p); + } + } else { + sp_select_context_up_one_layer(_desktop); + } + + ret = TRUE; + } + break; + + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + // save drag origin + xp = (gint) event->button.x; + yp = (gint) event->button.y; + within_tolerance = true; + + Geom::Point const button_pt(event->button.x, event->button.y); + Geom::Point const p(_desktop->w2d(button_pt)); + + if(Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->active(event->button.state)) { + Inkscape::Rubberband::get(_desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH); + } else { + Inkscape::Rubberband::get(_desktop)->defaultMode(); + } + + Inkscape::Rubberband::get(_desktop)->start(_desktop, p); + + if (this->grabbed) { + grabbed->ungrab(); + this->grabbed = nullptr; + } + + grabbed = _desktop->getCanvasCatchall(); + grabbed->grab(Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK ); + + // remember what modifiers were on before button press + this->button_press_state = event->button.state; + + this->moved = FALSE; + + rb_escaped = drag_escaped = 0; + + ret = TRUE; + } else if (event->button.button == 3) { + // right click; do not eat it so that right-click menu can appear, but cancel dragging & rubberband + this->sp_select_context_abort(); + } + break; + + case GDK_MOTION_NOTIFY: + { + if (this->grabbed && event->button.state & (GDK_SHIFT_MASK | GDK_MOD1_MASK)) { + _desktop->snapindicator->remove_snaptarget(); + } + + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + bool first_hit = Modifier::get(Modifiers::Type::SELECT_FIRST_HIT)->active(this->button_press_state); + bool force_drag = Modifier::get(Modifiers::Type::SELECT_FORCE_DRAG)->active(this->button_press_state); + bool always_box = Modifier::get(Modifiers::Type::SELECT_ALWAYS_BOX)->active(this->button_press_state); + + if ((event->motion.state & GDK_BUTTON1_MASK)) { + Geom::Point const motion_pt(event->motion.x, event->motion.y); + Geom::Point const p(_desktop->w2d(motion_pt)); + if ( within_tolerance + && ( abs( (gint) event->motion.x - xp ) < tolerance ) + && ( abs( (gint) event->motion.y - yp ) < tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + within_tolerance = false; + + if (first_hit || (force_drag && !always_box && !selection->isEmpty())) { + // if it's not click and ctrl or alt was pressed (the latter with some selection + // but not with shift) we want to drag rather than rubberband + this->dragging = TRUE; + this->set_cursor("select-dragging.svg"); + } + + if (this->dragging) { + /* User has dragged fast, so we get events on root (lauris)*/ + // not only that; we will end up here when ctrl-dragging as well + // and also when we started within tolerance, but trespassed tolerance outside of item + if (Inkscape::Rubberband::get(_desktop)->is_started()) { + Inkscape::Rubberband::get(_desktop)->stop(); + } + this->defaultMessageContext()->clear(); + + // Look for an item where the mouse was reported to be by mouse press (not mouse move). + item_at_point = _desktop->getItemAtPoint(Geom::Point(xp, yp), FALSE); + + if (item_at_point || this->moved || force_drag) { + // drag only if starting from an item, or if something is already grabbed, or if alt-dragging + if (!this->moved) { + item_in_group = _desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE); + group_at_point = _desktop->getGroupAtPoint(Geom::Point(event->button.x, event->button.y)); + + { + SPGroup *selGroup = dynamic_cast<SPGroup *>(selection->single()); + if (selGroup && (selGroup->layerMode() == SPGroup::LAYER)) { + group_at_point = selGroup; + } + } + + // group-at-point is meant to be topmost item if it's a group, + // not topmost group of all items at point + if (group_at_point != item_in_group && + !(group_at_point && item_at_point && + group_at_point->isAncestorOf(item_at_point))) { + group_at_point = nullptr; + } + + // if neither a group nor an item (possibly in a group) at point are selected, set selection to the item at point + if ((!item_in_group || !selection->includes(item_in_group)) && + (!group_at_point || !selection->includes(group_at_point)) && !force_drag) { + // select what is under cursor + if (!_seltrans->isEmpty()) { + _seltrans->resetState(); + } + + // when simply ctrl-dragging, we don't want to go into groups + if (item_at_point && !selection->includes(item_at_point)) { + selection->set(item_at_point); + } + } // otherwise, do not change selection so that dragging selected-within-group items, as well as alt-dragging, is possible + + _seltrans->grab(p, -1, -1, FALSE, TRUE); + this->moved = TRUE; + } + + if (!_seltrans->isEmpty()) { + // this->discard_delayed_snap_event(); + _seltrans->moveTo(p, event->button.state); + } + + _desktop->scroll_to_point(p); + gobble_motion_events(GDK_BUTTON1_MASK); + ret = TRUE; + } else { + this->dragging = FALSE; + this->discard_delayed_snap_event(); + } + + } else { + if (Inkscape::Rubberband::get(_desktop)->is_started()) { + Inkscape::Rubberband::get(_desktop)->move(p); + + auto touch_path = Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->get_label(); + auto mode = Inkscape::Rubberband::get(_desktop)->getMode(); + if (mode == RUBBERBAND_MODE_TOUCHPATH) { + this->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE, + _("<b>Draw over</b> objects to select them; release <b>%s</b> to switch to rubberband selection"), touch_path.c_str()); + } else if (mode == RUBBERBAND_MODE_TOUCHRECT) { + this->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE, + _("<b>Drag near</b> objects to select them; press <b>%s</b> to switch to touch selection"), touch_path.c_str()); + } else { + this->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE, + _("<b>Drag around</b> objects to select them; press <b>%s</b> to switch to touch selection"), touch_path.c_str()); + } + + gobble_motion_events(GDK_BUTTON1_MASK); + } + } + } + break; + } + case GDK_BUTTON_RELEASE: + xp = yp = 0; + + if ((event->button.button == 1) && (this->grabbed)) { + if (this->dragging) { + + if (this->moved) { + // item has been moved + _seltrans->ungrab(); + this->moved = FALSE; + } else if (this->item && !drag_escaped) { + // item has not been moved -> simply a click, do selecting + if (!selection->isEmpty()) { + if(Modifier::get(Modifiers::Type::SELECT_ADD_TO)->active(event->button.state)) { + // with shift, toggle selection + _seltrans->resetState(); + selection->toggle(this->item); + } else { + SPObject* single = selection->single(); + SPGroup *singleGroup = dynamic_cast<SPGroup *>(single); + // without shift, increase state (i.e. toggle scale/rotation handles) + if (selection->includes(this->item)) { + _seltrans->increaseState(); + } else if (singleGroup && (singleGroup->layerMode() == SPGroup::LAYER) && single->isAncestorOf(this->item)) { + _seltrans->increaseState(); + } else { + _seltrans->resetState(); + selection->set(this->item); + } + } + } else { // simple or shift click, no previous selection + _seltrans->resetState(); + selection->set(this->item); + } + } + + this->dragging = FALSE; + + auto window = _desktop->getCanvas()->get_window(); + + if (!_alt_on) { + if (_force_dragging) { + this->set_cursor(_default_cursor); + _force_dragging = false; + } else { + this->set_cursor("select-mouseover.svg"); + } + } + + this->discard_delayed_snap_event(); + + if (this->item) { + sp_object_unref( this->item, nullptr); + } + + this->item = nullptr; + } else { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop); + + if (r->is_started() && !within_tolerance) { + // this was a rubberband drag + std::vector<SPItem*> items; + + if (r->getMode() == RUBBERBAND_MODE_RECT) { + Geom::OptRect const b = r->getRectangle(); + items = _desktop->getDocument()->getItemsInBox(_desktop->dkey, (*b) * _desktop->dt2doc()); + } else if (r->getMode() == RUBBERBAND_MODE_TOUCHRECT) { + Geom::OptRect const b = r->getRectangle(); + items = _desktop->getDocument()->getItemsPartiallyInBox(_desktop->dkey, (*b) * _desktop->dt2doc()); + } else if (r->getMode() == RUBBERBAND_MODE_TOUCHPATH) { + bool topmost_items_only = prefs->getBool("/options/selection/touchsel_topmost_only"); + items = _desktop->getDocument()->getItemsAtPoints(_desktop->dkey, r->getPoints(), true, topmost_items_only); + } + + _seltrans->resetState(); + r->stop(); + this->defaultMessageContext()->clear(); + + if(Modifier::get(Modifiers::Type::SELECT_ADD_TO)->active(event->button.state)) { + // with shift, add to selection + selection->addList (items); + } else { + // without shift, simply select anew + selection->setList (items); + } + + } else { // it was just a click, or a too small rubberband + r->stop(); + + bool add_to = Modifier::get(Modifiers::Type::SELECT_ADD_TO)->active(event->button.state); + bool in_groups = Modifier::get(Modifiers::Type::SELECT_IN_GROUPS)->active(event->button.state); + bool force_drag = Modifier::get(Modifiers::Type::SELECT_FORCE_DRAG)->active(event->button.state); + + if (add_to && !rb_escaped && !drag_escaped) { + // this was a shift+click or alt+shift+click, select what was clicked upon + + if (in_groups) { + // go into groups, honoring force_drag (Alt) + item = sp_event_context_find_item (_desktop, + Geom::Point(event->button.x, event->button.y), force_drag, TRUE); + } else { + // don't go into groups, honoring Alt + item = sp_event_context_find_item (_desktop, + Geom::Point(event->button.x, event->button.y), force_drag, FALSE); + } + + if (item) { + selection->toggle(item); + item = nullptr; + } + + } else if ((in_groups || force_drag) && !rb_escaped && !drag_escaped) { // ctrl+click, alt+click + item = sp_event_context_find_item (_desktop, + Geom::Point(event->button.x, event->button.y), force_drag, in_groups); + + if (item) { + if (selection->includes(item)) { + _seltrans->increaseState(); + } else { + _seltrans->resetState(); + selection->set(item); + } + + item = nullptr; + } + } else { // click without shift, simply deselect, unless with Alt or something was cancelled + if (!selection->isEmpty()) { + if (!(rb_escaped) && !(drag_escaped) && !force_drag) { + selection->clear(); + } + + rb_escaped = 0; + } + } + } + + ret = TRUE; + } + if (grabbed) { + grabbed->ungrab(); + grabbed = nullptr; + } + // Think is not necessary now + // _desktop->updateNow(); + } + + if (event->button.button == 1) { + Inkscape::Rubberband::get(_desktop)->stop(); // might have been started in another tool! + } + + this->button_press_state = 0; + break; + + case GDK_SCROLL: { + + GdkEventScroll *scroll_event = (GdkEventScroll*) event; + + // do nothing specific if alt was not pressed + if ( ! Modifier::get(Modifiers::Type::SELECT_CYCLE)->active(scroll_event->state)) + break; + + is_cycling = true; + + /* Rebuild list of items underneath the mouse pointer */ + Geom::Point p = _desktop->d2w(_desktop->point()); + SPItem *item = _desktop->getItemAtPoint(p, true, nullptr); + this->cycling_items.clear(); + + SPItem *tmp = nullptr; + while(item != nullptr) { + this->cycling_items.push_back(item); + item = _desktop->getItemAtPoint(p, true, item); + if (selection->includes(item)) tmp = item; + } + + /* Compare current item list with item list during previous scroll ... */ + bool item_lists_differ = this->cycling_items != this->cycling_items_cmp; + + if(item_lists_differ) { + this->sp_select_context_reset_opacities(); + for (auto l : this->cycling_items_cmp) + selection->remove(l); // deselects the previous content of the cycling loop + this->cycling_items_cmp = (this->cycling_items); + + // set opacities in new stack + for(auto item : this->cycling_items) { + if (item) { + Inkscape::DrawingItem *arenaitem = item->get_arenaitem(_desktop->dkey); + arenaitem->setOpacity(0.3); + } + } + } + if(!cycling_cur_item) cycling_cur_item = tmp; + + this->cycling_wrap = prefs->getBool("/options/selection/cycleWrap", true); + + // Cycle through the items underneath the mouse pointer, one-by-one + this->sp_select_context_cycle_through_items(selection, scroll_event); + + ret = TRUE; + + GtkWindow *w = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_desktop->getCanvas()->gobj()))); + if (w) { + gtk_window_present(w); + _desktop->getCanvas()->grab_focus(); + } + break; + } + + case GDK_KEY_PRESS: // keybindings for select context + { + { + guint keyval = get_latin_keyval(&event->key); + + bool alt = ( MOD__ALT(event) + || (keyval == GDK_KEY_Alt_L) + || (keyval == GDK_KEY_Alt_R) + || (keyval == GDK_KEY_Meta_L) + || (keyval == GDK_KEY_Meta_R)); + + if (alt) { + _alt_on = true; + } + + if (!key_is_a_modifier (keyval)) { + this->defaultMessageContext()->clear(); + } else if (this->grabbed || _seltrans->isGrabbed()) { + if (Inkscape::Rubberband::get(_desktop)->is_started()) { + // if Alt then change cursor to moving cursor: + if (Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->active(event->key.state | keyval)) { + Inkscape::Rubberband::get(_desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH); + } + } else { + // do not change the statusbar text when mousekey is down to move or transform the object, + // because the statusbar text is already updated somewhere else. + break; + } + } else { + Modifiers::responsive_tooltip(this->defaultMessageContext(), event, 6, + Modifiers::Type::SELECT_IN_GROUPS, Modifiers::Type::MOVE_CONFINE, + Modifiers::Type::SELECT_ADD_TO, Modifiers::Type::SELECT_TOUCH_PATH, + Modifiers::Type::SELECT_CYCLE, Modifiers::Type::SELECT_FORCE_DRAG); + + // if Alt and nonempty selection, show moving cursor ("move selected"): + if (alt && !selection->isEmpty() && !_desktop->isWaitingCursor()) { + this->set_cursor("select-dragging.svg"); + _force_dragging = true; + _default_cursor = "select.svg"; + } + //*/ + break; + } + } + + gdouble const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + auto const y_dir = _desktop->yaxisdir(); + + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Left: // move selection left + case GDK_KEY_KP_Left: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events( get_latin_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + _desktop->getSelection()->moveScreen(mul*-10, 0); // shift + } else { + _desktop->getSelection()->moveScreen(mul*-1, 0); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + _desktop->getSelection()->move(mul*-10*nudge, 0); // shift + } else { + _desktop->getSelection()->move(mul*-nudge, 0); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Up: // move selection up + case GDK_KEY_KP_Up: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + mul *= -y_dir; + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + _desktop->getSelection()->moveScreen(0, mul*10); // shift + } else { + _desktop->getSelection()->moveScreen(0, mul*1); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + _desktop->getSelection()->move(0, mul*10*nudge); // shift + } else { + _desktop->getSelection()->move(0, mul*nudge); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Right: // move selection right + case GDK_KEY_KP_Right: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + _desktop->getSelection()->moveScreen(mul*10, 0); // shift + } else { + _desktop->getSelection()->moveScreen(mul*1, 0); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + _desktop->getSelection()->move(mul*10*nudge, 0); // shift + } else { + _desktop->getSelection()->move(mul*nudge, 0); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Down: // move selection down + case GDK_KEY_KP_Down: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + mul *= -y_dir; + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + _desktop->getSelection()->moveScreen(0, mul*-10); // shift + } else { + _desktop->getSelection()->moveScreen(0, mul*-1); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + _desktop->getSelection()->move(0, mul*-10*nudge); // shift + } else { + _desktop->getSelection()->move(0, mul*-nudge); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (!this->sp_select_context_abort()) { + selection->clear(); + } + + ret = TRUE; + break; + + case GDK_KEY_a: + case GDK_KEY_A: + if (MOD__CTRL_ONLY(event)) { + sp_edit_select_all(_desktop); + ret = TRUE; + } + break; + + case GDK_KEY_space: + /* stamping mode: show outline mode moving */ + /* FIXME: Is next condition ok? (lauris) */ + if (this->dragging && this->grabbed) { + _seltrans->stamp(); + ret = TRUE; + } + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("select-x"); + ret = TRUE; + } + break; + + case GDK_KEY_bracketleft: + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + selection->rotateScreen(-mul * y_dir); + } else if (MOD__CTRL(event)) { + selection->rotate(-90 * y_dir); + } else if (snaps) { + selection->rotate(-180.0/snaps * y_dir); + } + + ret = TRUE; + break; + + case GDK_KEY_bracketright: + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + selection->rotateScreen(mul * y_dir); + } else if (MOD__CTRL(event)) { + selection->rotate(90 * y_dir); + } else if (snaps) { + selection->rotate(180.0/snaps * y_dir); + } + + ret = TRUE; + break; + + case GDK_KEY_Return: + if (MOD__CTRL_ONLY(event)) { + if (selection->singleItem()) { + SPItem *clicked_item = selection->singleItem(); + SPGroup *clickedGroup = dynamic_cast<SPGroup *>(clicked_item); + if ( (clickedGroup && (clickedGroup->layerMode() != SPGroup::LAYER)) || dynamic_cast<SPBox3D *>(clicked_item)) { // enter group or a 3D box + _desktop->layerManager().setCurrentLayer(clicked_item); + _desktop->getSelection()->clear(); + } else { + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Selected object is not a group. Cannot enter.")); + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_BackSpace: + if (MOD__CTRL_ONLY(event)) { + sp_select_context_up_one_layer(_desktop); + ret = TRUE; + } + break; + + case GDK_KEY_s: + case GDK_KEY_S: + if (MOD__SHIFT_ONLY(event)) { + if (!selection->isEmpty()) { + _seltrans->increaseState(); + } + + ret = TRUE; + } + break; + + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + _desktop->selection->toGuides(); + ret = true; + } + break; + + default: + break; + } + break; + } + case GDK_KEY_RELEASE: { + guint keyval = get_latin_keyval(&event->key); + if (key_is_a_modifier (keyval)) { + this->defaultMessageContext()->clear(); + } + + bool alt = ( MOD__ALT(event) + || (keyval == GDK_KEY_Alt_L) + || (keyval == GDK_KEY_Alt_R) + || (keyval == GDK_KEY_Meta_L) + || (keyval == GDK_KEY_Meta_R)); + + if (alt) { + _alt_on = false; + } + + if (Inkscape::Rubberband::get(_desktop)->is_started()) { + // if Alt then change cursor to moving cursor: + if (alt) { + Inkscape::Rubberband::get(_desktop)->defaultMode(); + } + } else { + if (alt) { + // quit cycle-selection and reset opacities + if (is_cycling) { + this->sp_select_context_reset_opacities(); + is_cycling = false; + } + } + } + + // set cursor to default. + if (alt && !(this->grabbed || _seltrans->isGrabbed()) && !selection->isEmpty() && !_desktop->isWaitingCursor()) { + this->set_cursor(_default_cursor); + _force_dragging = false; + } + break; + } + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + +/* + 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/tools/select-tool.h b/src/ui/tools/select-tool.h new file mode 100644 index 0000000..51ed41a --- /dev/null +++ b/src/ui/tools/select-tool.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_SELECT_CONTEXT_H__ +#define __SP_SELECT_CONTEXT_H__ + +/* + * Select tool + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/tool-base.h" + +#define SP_SELECT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::SelectTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_SELECT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::SelectTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { + class SelTrans; + class SelectionDescriber; +} + +namespace Inkscape { +namespace UI { +namespace Tools { + +class SelectTool : public ToolBase { +public: + SelectTool(SPDesktop *desktop); + ~SelectTool() override; + + bool dragging; + bool moved; + guint button_press_state; + + std::vector<SPItem *> cycling_items; + std::vector<SPItem *> cycling_items_cmp; + SPItem *cycling_cur_item; + bool cycling_wrap; + + SPItem *item; + Inkscape::CanvasItem *grabbed = nullptr; + Inkscape::SelTrans *_seltrans; + Inkscape::SelectionDescriber *_describer; + gchar *no_selection_msg = nullptr; + + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; +private: + bool sp_select_context_abort(); + void sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event); + void sp_select_context_reset_opacities(); + + bool _alt_on; + bool _force_dragging; + + std::string _default_cursor; +}; + +} // namespace Tools +} // 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 : diff --git a/src/ui/tools/spiral-tool.cpp b/src/ui/tools/spiral-tool.cpp new file mode 100644 index 0000000..f79eab7 --- /dev/null +++ b/src/ui/tools/spiral-tool.cpp @@ -0,0 +1,411 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Spiral drawing context + * + * Authors: + * Mitsuru Oka + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2001 Lauris Kaplinski + * Copyright (C) 2001-2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "spiral-tool.h" + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "selection.h" + +#include "include/macros.h" + +#include "object/sp-namedview.h" +#include "object/sp-spiral.h" + +#include "ui/icon-names.h" +#include "ui/shape-editor.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +SpiralTool::SpiralTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/shapes/spiral", "spiral.svg") + , spiral(nullptr) + , revo(3) + , exp(1) + , t0(0) +{ + sp_event_context_read(this, "expansion"); + sp_event_context_read(this, "revolution"); + sp_event_context_read(this, "t0"); + + this->shape_editor = new ShapeEditor(desktop); + + SPItem *item = desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + Inkscape::Selection *selection = desktop->getSelection(); + this->sel_changed_connection.disconnect(); + + this->sel_changed_connection = selection->connectChanged(sigc::mem_fun(this, &SpiralTool::selection_changed)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +SpiralTool::~SpiralTool() { + ungrabCanvasEvents(); + + this->finishItem(); + this->sel_changed_connection.disconnect(); + + this->enableGrDrag(false); + + delete this->shape_editor; + this->shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->spiral) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + */ +void SpiralTool::selection_changed(Inkscape::Selection *selection) { + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); +} + + +void SpiralTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring name = val.getEntryName(); + + if (name == "expansion") { + this->exp = CLAMP(val.getDouble(), 0.0, 1000.0); + } else if (name == "revolution") { + this->revo = CLAMP(val.getDouble(3.0), 0.05, 40.0); + } else if (name == "t0") { + this->t0 = CLAMP(val.getDouble(), 0.0, 0.999); + } +} + +bool SpiralTool::root_handler(GdkEvent* event) { + static gboolean dragging; + + Inkscape::Selection *selection = _desktop->getSelection(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + dragging = TRUE; + + this->center = this->setup_for_drag_start(event); + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + grabCanvasEvents(); + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && (event->motion.state & GDK_BUTTON1_MASK)) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->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 draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop, true, this->spiral); + m.freeSnapReturnByRef(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + this->drag(motion_dt, event->motion.state); + + gobble_motion_events(GDK_BUTTON1_MASK); + + ret = TRUE; + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + if (event->button.button == 1) { + dragging = FALSE; + this->discard_delayed_snap_event(); + + if (!this->within_tolerance) { + // we've been dragging, finish the spiral + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = nullptr; + ret = TRUE; + ungrabCanvasEvents(); + } + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + sp_event_show_modifier_tip(this->defaultMessageContext(), event, + _("<b>Ctrl</b>: snap angle"), + nullptr, + _("<b>Alt</b>: lock spiral radius")); + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("spiral-revolutions"); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + this->discard_delayed_snap_event(); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_space: + if (dragging) { + ungrabCanvasEvents(); + dragging = false; + this->discard_delayed_snap_event(); + + if (!this->within_tolerance) { + // we've been dragging, finish the spiral + finishItem(); + } + // do not return true, so that space would work switching to selector + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void SpiralTool::drag(Geom::Point const &p, guint state) { + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + if (!this->spiral) { + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "spiral"); + + // Set style + sp_desktop_apply_style_tool(_desktop, repr, "/tools/shapes/spiral", false); + + this->spiral = SP_SPIRAL(currentLayer()->appendChildRepr(repr)); + Inkscape::GC::release(repr); + this->spiral->transform = currentLayer()->i2doc_affine().inverse(); + this->spiral->updateRepr(); + } + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop, true, this->spiral); + Geom::Point pt2g = p; + m.freeSnapReturnByRef(pt2g, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + Geom::Point const p0 = _desktop->dt2doc(this->center); + Geom::Point const p1 = _desktop->dt2doc(pt2g); + + Geom::Point const delta = p1 - p0; + gdouble const rad = Geom::L2(delta); + + // Start angle calculated from end angle and number of revolutions. + gdouble arg = Geom::atan2(delta) - 2.0*M_PI * spiral->revo; + + if (state & GDK_CONTROL_MASK) { + /* Snap start angle */ + double snaps_radian = M_PI/snaps; + arg = std::round(arg/snaps_radian) * snaps_radian; + } + + /* Fixme: these parameters should be got from dialog box */ + this->spiral->setPosition(p0[Geom::X], p0[Geom::Y], + /*expansion*/ this->exp, + /*revolution*/ this->revo, + rad, arg, + /*t0*/ this->t0); + + /* status text */ + Inkscape::Util::Quantity q = Inkscape::Util::Quantity(rad, "px"); + Glib::ustring rads = q.string(_desktop->namedview->display_units); + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Spiral</b>: radius %s, angle %.2f°; with <b>Ctrl</b> to snap angle"), + rads.c_str(), arg * 180/M_PI + 360*spiral->revo); +} + +void SpiralTool::finishItem() { + this->message_context->clear(); + + if (this->spiral != nullptr) { + if (this->spiral->rad == 0) { + this->cancel(); // Don't allow the creating of zero sized spiral, for example when the start and and point snap to the snap grid point + return; + } + + spiral->set_shape(); + spiral->updateRepr(SP_OBJECT_WRITE_EXT); + // compensate stroke scaling couldn't be done in doWriteTransform + double const expansion = spiral->transform.descrim(); + spiral->doWriteTransform(spiral->transform, nullptr, true); + spiral->adjust_stroke_width_recursive(expansion); + + _desktop->getSelection()->set(this->spiral); + DocumentUndo::done(_desktop->getDocument(), _("Create spiral"), INKSCAPE_ICON("draw-spiral")); + + this->spiral = nullptr; + } +} + +void SpiralTool::cancel() { + _desktop->getSelection()->clear(); + ungrabCanvasEvents(); + + if (this->spiral != nullptr) { + this->spiral->deleteObject(); + this->spiral = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + DocumentUndo::cancel(_desktop->getDocument()); +} + +} +} +} + +/* + 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/tools/spiral-tool.h b/src/ui/tools/spiral-tool.h new file mode 100644 index 0000000..203617c --- /dev/null +++ b/src/ui/tools/spiral-tool.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_SPIRAL_CONTEXT_H__ +#define __SP_SPIRAL_CONTEXT_H__ + +/** \file + * Spiral drawing context + */ +/* + * Authors: + * Mitsuru Oka + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2001 Lauris Kaplinski + * Copyright (C) 2001-2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/connection.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +#define SP_SPIRAL_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::SpiralTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_SPIRAL_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::SpiralTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +class SPSpiral; + +namespace Inkscape { + +class Selection; + +namespace UI { +namespace Tools { + +class SpiralTool : public ToolBase { +public: + SpiralTool(SPDesktop *desktop); + ~SpiralTool() override; + + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; +private: + SPSpiral * spiral; + Geom::Point center; + gdouble revo; + gdouble exp; + gdouble t0; + + sigc::connection sel_changed_connection; + + void drag(Geom::Point const &p, guint state); + void finishItem(); + void cancel(); + void selection_changed(Inkscape::Selection *selection); +}; + +} +} +} + +#endif diff --git a/src/ui/tools/spray-tool.cpp b/src/ui/tools/spray-tool.cpp new file mode 100644 index 0000000..705b129 --- /dev/null +++ b/src/ui/tools/spray-tool.cpp @@ -0,0 +1,1538 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Spray Tool + * + * Authors: + * Pierre-Antoine MARC + * Pierre CACLIN + * Aurel-Aimé MARMION + * Julien LERAY + * Benoît LAVORATA + * Vincent MONTAGNE + * Pierre BARBRY-BLOT + * Steren GIANNINI (steren.giannini@gmail.com) + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * Adrian Boguszewski + * + * Copyright (C) 2009 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <numeric> +#include <vector> +#include <tuple> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/circle.h> + + +#include "context-fns.h" +#include "desktop-events.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "include/macros.h" +#include "message-context.h" +#include "path-chemistry.h" +#include "selection.h" + +#include "display/cairo-utils.h" +#include "display/curve.h" +#include "display/drawing-context.h" +#include "display/drawing.h" +#include "display/control/canvas-item-bpath.h" +#include "display/control/canvas-item-drawing.h" + +#include "object/box3d.h" +#include "object/sp-use.h" +#include "object/sp-item-transform.h" + +#include "svg/svg.h" +#include "svg/svg-color.h" + +#include "ui/icon-names.h" +#include "ui/toolbar/spray-toolbar.h" +#include "ui/tools/spray-tool.h" + + +using Inkscape::DocumentUndo; + +#define DDC_RED_RGBA 0xff0000ff +#define DYNA_MIN_WIDTH 1.0e-6 + +// Disabled in 0.91 because of Bug #1274831 (crash, spraying an object +// with the mode: spray object in single path) +// Please enable again when working on 1.0 +#define ENABLE_SPRAY_MODE_SINGLE_PATH + +namespace Inkscape { +namespace UI { +namespace Tools { + +enum { + PICK_COLOR, + PICK_OPACITY, + PICK_R, + PICK_G, + PICK_B, + PICK_H, + PICK_S, + PICK_L +}; + +/** + * This function returns pseudo-random numbers from a normal distribution + * @param mu : mean + * @param sigma : standard deviation ( > 0 ) + */ +inline double NormalDistribution(double mu, double sigma) +{ + // use Box Muller's algorithm + return mu + sigma * sqrt( -2.0 * log(g_random_double_range(0, 1)) ) * cos( 2.0*M_PI*g_random_double_range(0, 1) ); +} + +/* Method to rotate items */ +static void sp_spray_rotate_rel(Geom::Point c, SPDesktop */*desktop*/, SPItem *item, Geom::Rotate const &rotation) +{ + Geom::Translate const s(c); + Geom::Affine affine = s.inverse() * rotation * s; + // Rotate item. + item->set_i2d_affine(item->i2dt_affine() * affine); + // Use each item's own transform writer, consistent with sp_selection_apply_affine() + item->doWriteTransform(item->transform); + // Restore the center position (it's changed because the bbox center changed) + if (item->isCenterSet()) { + item->setCenter(c); + item->updateRepr(); + } +} + +/* Method to scale items */ +static void sp_spray_scale_rel(Geom::Point c, SPDesktop */*desktop*/, SPItem *item, Geom::Scale const &scale) +{ + Geom::Translate const s(c); + item->set_i2d_affine(item->i2dt_affine() * s.inverse() * scale * s); + item->doWriteTransform(item->transform); +} + +SprayTool::SprayTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/spray", "spray.svg", false) + , pressure(TC_DEFAULT_PRESSURE) + , dragging(false) + , usepressurewidth(false) + , usepressurepopulation(false) + , usepressurescale(false) + , usetilt(false) + , usetext(false) + , width(0.2) + , ratio(0) + , tilt(0) + , rotation_variation(0) + , population(0) + , scale_variation(1) + , scale(1) + , mean(0.2) + , standard_deviation(0.2) + , distrib(1) + , mode(0) + , is_drawing(false) + , is_dilating(false) + , has_dilated(false) + , dilate_area(nullptr) + , no_overlap(false) + , picker(false) + , pick_center(true) + , pick_inverse_value(false) + , pick_fill(false) + , pick_stroke(false) + , pick_no_overlap(false) + , over_transparent(true) + , over_no_transparent(true) + , offset(0) + , pick(0) + , do_trace(false) + , pick_to_size(false) + , pick_to_presence(false) + , pick_to_color(false) + , pick_to_opacity(false) + , invert_picked(false) + , gamma_picked(0) + , rand_picked(0) +{ + dilate_area = new Inkscape::CanvasItemBpath(desktop->getCanvasControls()); + dilate_area->set_stroke(0xff9900ff); + dilate_area->set_fill(0x0, SP_WIND_RULE_EVENODD); + dilate_area->hide(); + + this->is_drawing = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/clonetiler/dotrace", false); + if (prefs->getBool("/tools/spray/selcue")) { + this->enableSelectionCue(); + } + if (prefs->getBool("/tools/spray/gradientdrag")) { + this->enableGrDrag(); + } + desktop->getSelection()->setBackup(); + sp_event_context_read(this, "distrib"); + sp_event_context_read(this, "width"); + sp_event_context_read(this, "ratio"); + sp_event_context_read(this, "tilt"); + sp_event_context_read(this, "rotation_variation"); + sp_event_context_read(this, "scale_variation"); + sp_event_context_read(this, "mode"); + sp_event_context_read(this, "population"); + sp_event_context_read(this, "mean"); + sp_event_context_read(this, "standard_deviation"); + sp_event_context_read(this, "usepressurewidth"); + sp_event_context_read(this, "usepressurepopulation"); + sp_event_context_read(this, "usepressurescale"); + sp_event_context_read(this, "Scale"); + sp_event_context_read(this, "offset"); + sp_event_context_read(this, "picker"); + sp_event_context_read(this, "pick_center"); + sp_event_context_read(this, "pick_inverse_value"); + sp_event_context_read(this, "pick_fill"); + sp_event_context_read(this, "pick_stroke"); + sp_event_context_read(this, "pick_no_overlap"); + sp_event_context_read(this, "over_no_transparent"); + sp_event_context_read(this, "over_transparent"); + sp_event_context_read(this, "no_overlap"); +} + +SprayTool::~SprayTool() { + if (!object_set.isEmpty()) { + object_set.clear(); + } + _desktop->getSelection()->restoreBackup(); + this->enableGrDrag(false); + this->style_set_connection.disconnect(); + + if (this->dilate_area) { + delete this->dilate_area; + this->dilate_area = nullptr; + } +} + +void SprayTool::update_cursor(bool /*with_shift*/) { + guint num = 0; + gchar *sel_message = nullptr; + + if (!_desktop->selection->isEmpty()) { + num = (guint)boost::distance(_desktop->selection->items()); + sel_message = g_strdup_printf(ngettext("<b>%i</b> object selected","<b>%i</b> objects selected",num), num); + } else { + sel_message = g_strdup_printf("%s", _("<b>Nothing</b> selected")); + } + + switch (this->mode) { + case SPRAY_MODE_COPY: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray <b>copies</b> of the initial selection."), sel_message); + break; + case SPRAY_MODE_CLONE: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray <b>clones</b> of the initial selection."), sel_message); + break; + case SPRAY_MODE_SINGLE_PATH: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray in a <b>single path</b> of the initial selection."), sel_message); + break; + default: + break; + } + g_free(sel_message); +} + + +void SprayTool::setCloneTilerPrefs() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->do_trace = prefs->getBool("/dialogs/clonetiler/dotrace", false); + this->pick = prefs->getInt("/dialogs/clonetiler/pick"); + this->pick_to_size = prefs->getBool("/dialogs/clonetiler/pick_to_size", false); + this->pick_to_presence = prefs->getBool("/dialogs/clonetiler/pick_to_presence", false); + this->pick_to_color = prefs->getBool("/dialogs/clonetiler/pick_to_color", false); + this->pick_to_opacity = prefs->getBool("/dialogs/clonetiler/pick_to_opacity", false); + this->rand_picked = 0.01 * prefs->getDoubleLimited("/dialogs/clonetiler/rand_picked", 0, 0, 100); + this->invert_picked = prefs->getBool("/dialogs/clonetiler/invert_picked", false); + this->gamma_picked = prefs->getDoubleLimited("/dialogs/clonetiler/gamma_picked", 0, -10, 10); +} + +void SprayTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "mode") { + this->mode = val.getInt(); + this->update_cursor(false); + } else if (path == "width") { + this->width = 0.01 * CLAMP(val.getInt(10), 1, 100); + } else if (path == "usepressurewidth") { + this->usepressurewidth = val.getBool(); + } else if (path == "usepressurepopulation") { + this->usepressurepopulation = val.getBool(); + } else if (path == "usepressurescale") { + this->usepressurescale = val.getBool(); + } else if (path == "population") { + this->population = 0.01 * CLAMP(val.getInt(10), 1, 100); + } else if (path == "rotation_variation") { + this->rotation_variation = CLAMP(val.getDouble(0.0), 0, 100.0); + } else if (path == "scale_variation") { + this->scale_variation = CLAMP(val.getDouble(1.0), 0, 100.0); + } else if (path == "standard_deviation") { + this->standard_deviation = 0.01 * CLAMP(val.getInt(10), 1, 100); + } else if (path == "mean") { + this->mean = 0.01 * CLAMP(val.getInt(10), 1, 100); +// Not implemented in the toolbar and preferences yet + } else if (path == "distribution") { + this->distrib = val.getInt(1); + } else if (path == "tilt") { + this->tilt = CLAMP(val.getDouble(0.1), 0, 1000.0); + } else if (path == "ratio") { + this->ratio = CLAMP(val.getDouble(), 0.0, 0.9); + } else if (path == "offset") { + this->offset = val.getDoubleLimited(100.0, 0, 1000.0); + } else if (path == "pick_center") { + this->pick_center = val.getBool(true); + } else if (path == "pick_inverse_value") { + this->pick_inverse_value = val.getBool(false); + } else if (path == "pick_fill") { + this->pick_fill = val.getBool(false); + } else if (path == "pick_stroke") { + this->pick_stroke = val.getBool(false); + } else if (path == "pick_no_overlap") { + this->pick_no_overlap = val.getBool(false); + } else if (path == "over_no_transparent") { + this->over_no_transparent = val.getBool(true); + } else if (path == "over_transparent") { + this->over_transparent = val.getBool(true); + } else if (path == "no_overlap") { + this->no_overlap = val.getBool(false); + } else if (path == "picker") { + this->picker = val.getBool(false); + } +} + +static void sp_spray_extinput(SprayTool *tc, GdkEvent *event) +{ + if (gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &tc->pressure)) { + tc->pressure = CLAMP(tc->pressure, TC_MIN_PRESSURE, TC_MAX_PRESSURE); + } else { + tc->pressure = TC_DEFAULT_PRESSURE; + } +} + +static double get_width(SprayTool *tc) +{ + double pressure = (tc->usepressurewidth? tc->pressure / TC_DEFAULT_PRESSURE : 1); + return pressure * tc->width; +} + +static double get_dilate_radius(SprayTool *tc) +{ + return 250 * get_width(tc)/tc->getDesktop()->current_zoom(); +} + +static double get_path_mean(SprayTool *tc) +{ + return tc->mean; +} + +static double get_path_standard_deviation(SprayTool *tc) +{ + return tc->standard_deviation; +} + +static double get_population(SprayTool *tc) +{ + double pressure = (tc->usepressurepopulation? tc->pressure / TC_DEFAULT_PRESSURE : 1); + return pressure * tc->population; +} + +static double get_pressure(SprayTool *tc) +{ + double pressure = tc->pressure / TC_DEFAULT_PRESSURE; + return pressure; +} + +static double get_move_mean(SprayTool *tc) +{ + return tc->mean; +} + +static double get_move_standard_deviation(SprayTool *tc) +{ + return tc->standard_deviation; +} + +/** + * Method to handle the distribution of the items + * @param[out] radius : radius of the position of the sprayed object + * @param[out] angle : angle of the position of the sprayed object + * @param[in] a : mean + * @param[in] s : standard deviation + * @param[in] choice : + + */ +static void random_position(double &radius, double &angle, double &a, double &s, int /*choice*/) +{ + // angle is taken from an uniform distribution + angle = g_random_double_range(0, M_PI*2.0); + + // radius is taken from a Normal Distribution + double radius_temp =-1; + while(!((radius_temp >= 0) && (radius_temp <=1 ))) + { + radius_temp = NormalDistribution(a, s); + } + // Because we are in polar coordinates, a special treatment has to be done to the radius. + // Otherwise, positions taken from an uniform repartition on radius and angle will not seam to + // be uniformily distributed on the disk (more at the center and less at the boundary). + // We counter this effect with a 0.5 exponent. This is empiric. + radius = pow(radius_temp, 0.5); + +} + +static void sp_spray_transform_path(SPItem * item, Geom::Path &path, Geom::Affine affine, Geom::Point center){ + path *= i2anc_affine(static_cast<SPItem *>(item->parent), nullptr).inverse(); + path *= item->transform.inverse(); + Geom::Affine dt2p; + if (item->parent) { + dt2p = static_cast<SPItem *>(item->parent)->i2dt_affine().inverse(); + } else { + dt2p = item->document->dt2doc(); + } + Geom::Affine i2dt = item->i2dt_affine() * Geom::Translate(center).inverse() * affine * Geom::Translate(center); + path *= i2dt * dt2p; + path *= i2anc_affine(static_cast<SPItem *>(item->parent), nullptr); +} + +/** +Randomizes \a val by \a rand, with 0 < val < 1 and all values (including 0, 1) having the same +probability of being displaced. + */ +double randomize01(double val, double rand) +{ + double base = MIN (val - rand, 1 - 2*rand); + if (base < 0) { + base = 0; + } + val = base + g_random_double_range (0, MIN (2 * rand, 1 - base)); + return CLAMP(val, 0, 1); // this should be unnecessary with the above provisions, but just in case... +} + +static guint32 getPickerData(Geom::IntRect area, SPDesktop *desktop) +{ + Inkscape::CanvasItemDrawing *canvas_item_drawing = desktop->getCanvasDrawing(); + Inkscape::Drawing *drawing = canvas_item_drawing->get_drawing(); + + // Ensure drawing up-to-date. (Is this really necessary?) + drawing->update(); + + // Get average color. + double R, G, B, A; + drawing->average_color(area, R, G, B, A); + + //this can fix the bug #1511998 if confirmed + if ( A < 1e-6) { + R = 1.0; + G = 1.0; + B = 1.0; + } + + return SP_RGBA32_F_COMPOSE(R, G, B, A); +} + +static void showHidden(std::vector<SPItem *> items_down){ + for (auto item_hidden : items_down) { + item_hidden->setHidden(false); + item_hidden->updateRepr(); + } +} +//todo: maybe move same parameter to preferences +static bool fit_item(SPDesktop *desktop, + SPItem *item, + Geom::OptRect bbox, + Geom::Point &move, + Geom::Point center, + gint mode, + double angle, + double &_scale, + double scale, + bool picker, + bool pick_center, + bool pick_inverse_value, + bool pick_fill, + bool pick_stroke, + bool pick_no_overlap, + bool over_no_transparent, + bool over_transparent, + bool no_overlap, + double offset, + SPCSSAttr *css, + bool trace_scale, + int pick, + bool do_trace, + bool pick_to_size, + bool pick_to_presence, + bool pick_to_color, + bool pick_to_opacity, + bool invert_picked, + double gamma_picked , + double rand_picked) +{ + SPDocument *doc = item->document; + double width = bbox->width(); + double height = bbox->height(); + double offset_width = (offset * width)/100.0 - (width); + if(offset_width < 0 ){ + offset_width = 0; + } + double offset_height = (offset * height)/100.0 - (height); + if(offset_height < 0 ){ + offset_height = 0; + } + if(picker && pick_to_size && !trace_scale && do_trace){ + _scale = 0.1; + } + Geom::OptRect bbox_procesed = Geom::Rect(Geom::Point(bbox->left() - offset_width, bbox->top() - offset_height),Geom::Point(bbox->right() + offset_width, bbox->bottom() + offset_height)); + Geom::Path path; + path.start(Geom::Point(bbox_procesed->left(), bbox_procesed->top())); + path.appendNew<Geom::LineSegment>(Geom::Point(bbox_procesed->right(), bbox_procesed->top())); + path.appendNew<Geom::LineSegment>(Geom::Point(bbox_procesed->right(), bbox_procesed->bottom())); + path.appendNew<Geom::LineSegment>(Geom::Point(bbox_procesed->left(), bbox_procesed->bottom())); + path.close(true); + sp_spray_transform_path(item, path, Geom::Scale(_scale), center); + sp_spray_transform_path(item, path, Geom::Scale(scale), center); + sp_spray_transform_path(item, path, Geom::Rotate(angle), center); + path *= Geom::Translate(move); + path *= desktop->doc2dt(); + bbox_procesed = path.boundsFast(); + double bbox_left_main = bbox_procesed->left(); + double bbox_right_main = bbox_procesed->right(); + double bbox_top_main = bbox_procesed->top(); + double bbox_bottom_main = bbox_procesed->bottom(); + double width_transformed = bbox_procesed->width(); + double height_transformed = bbox_procesed->height(); + Geom::Point mid_point = desktop->d2w(bbox_procesed->midpoint()); + Geom::IntRect area = Geom::IntRect::from_xywh(floor(mid_point[Geom::X]), floor(mid_point[Geom::Y]), 1, 1); + guint32 rgba = getPickerData(area, desktop); + guint32 rgba2 = 0xffffff00; + Geom::Rect rect_sprayed(desktop->d2w(Geom::Point(bbox_left_main,bbox_top_main)), desktop->d2w(Geom::Point(bbox_right_main,bbox_bottom_main))); + if (!rect_sprayed.hasZeroArea()) { + rgba2 = getPickerData(rect_sprayed.roundOutwards(), desktop); + } + if(pick_no_overlap) { + if(rgba != rgba2) { + if(mode != SPRAY_MODE_ERASER) { + return false; + } + } + } + if(!pick_center) { + rgba = rgba2; + } + if(!over_transparent && (SP_RGBA32_A_F(rgba) == 0 || SP_RGBA32_A_F(rgba) < 1e-6)) { + if(mode != SPRAY_MODE_ERASER) { + return false; + } + } + if(!over_no_transparent && SP_RGBA32_A_F(rgba) > 0) { + if(mode != SPRAY_MODE_ERASER) { + return false; + } + } + if(offset < 100 ) { + offset_width = ((99.0 - offset) * width_transformed)/100.0 - width_transformed; + offset_height = ((99.0 - offset) * height_transformed)/100.0 - height_transformed; + } else { + offset_width = 0; + offset_height = 0; + } + std::vector<SPItem*> items_down = desktop->getDocument()->getItemsPartiallyInBox(desktop->dkey, *bbox_procesed); + Inkscape::Selection *selection = desktop->getSelection(); + if (selection->isEmpty()) { + return false; + } + std::vector<SPItem*> const items_selected(selection->items().begin(), selection->items().end()); + std::vector<SPItem*> items_down_erased; + for (std::vector<SPItem*>::const_iterator i=items_down.begin(); i!=items_down.end(); ++i) { + SPItem *item_down = *i; + Geom::OptRect bbox_down = item_down->documentVisualBounds(); + double bbox_left = bbox_down->left(); + double bbox_top = bbox_down->top(); + gchar const * item_down_sharp = g_strdup_printf("#%s", item_down->getId()); + items_down_erased.push_back(item_down); + for (auto item_selected : items_selected) { + gchar const * spray_origin; + if(!item_selected->getAttribute("inkscape:spray-origin")){ + spray_origin = g_strdup_printf("#%s", item_selected->getId()); + } else { + spray_origin = item_selected->getAttribute("inkscape:spray-origin"); + } + if(strcmp(item_down_sharp, spray_origin) == 0 || + (item_down->getAttribute("inkscape:spray-origin") && + strcmp(item_down->getAttribute("inkscape:spray-origin"),spray_origin) == 0 )) + { + if(mode == SPRAY_MODE_ERASER) { + if(strcmp(item_down_sharp, spray_origin) != 0 && !selection->includes(item_down) ){ + item_down->deleteObject(); + items_down_erased.pop_back(); + break; + } + } else if(no_overlap) { + if(!(offset_width < 0 && offset_height < 0 && std::abs(bbox_left - bbox_left_main) > std::abs(offset_width) && + std::abs(bbox_top - bbox_top_main) > std::abs(offset_height))){ + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + } else if(picker || over_transparent || over_no_transparent) { + item_down->setHidden(true); + item_down->updateRepr(); + } + } + } + } + if(mode == SPRAY_MODE_ERASER){ + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down_erased); + } + return false; + } + if(picker || over_transparent || over_no_transparent){ + if(!no_overlap){ + doc->ensureUpToDate(); + rgba = getPickerData(area, desktop); + if (!rect_sprayed.hasZeroArea()) { + rgba2 = getPickerData(rect_sprayed.roundOutwards(), desktop); + } + } + if(pick_no_overlap){ + if(rgba != rgba2){ + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + } + if(!pick_center){ + rgba = rgba2; + } + double opacity = 1.0; + gchar color_string[32]; *color_string = 0; + float r = SP_RGBA32_R_F(rgba); + float g = SP_RGBA32_G_F(rgba); + float b = SP_RGBA32_B_F(rgba); + float a = SP_RGBA32_A_F(rgba); + if(!over_transparent && (a == 0 || a < 1e-6)){ + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + if(!over_no_transparent && a > 0){ + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + + if(picker && do_trace){ + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, r, g, b); + + gdouble val = 0; + switch (pick) { + case PICK_COLOR: + val = 1 - hsl[2]; // inverse lightness; to match other picks where black = max + break; + case PICK_OPACITY: + val = a; + break; + case PICK_R: + val = r; + break; + case PICK_G: + val = g; + break; + case PICK_B: + val = b; + break; + case PICK_H: + val = hsl[0]; + break; + case PICK_S: + val = hsl[1]; + break; + case PICK_L: + val = 1 - hsl[2]; + break; + default: + break; + } + + if (rand_picked > 0) { + val = randomize01 (val, rand_picked); + r = randomize01 (r, rand_picked); + g = randomize01 (g, rand_picked); + b = randomize01 (b, rand_picked); + } + + if (gamma_picked != 0) { + double power; + if (gamma_picked > 0) + power = 1/(1 + fabs(gamma_picked)); + else + power = 1 + fabs(gamma_picked); + + val = pow (val, power); + r = pow ((double)r, (double)power); + g = pow ((double)g, (double)power); + b = pow ((double)b, (double)power); + } + + if (invert_picked) { + val = 1 - val; + r = 1 - r; + g = 1 - g; + b = 1 - b; + } + + val = CLAMP (val, 0, 1); + r = CLAMP (r, 0, 1); + g = CLAMP (g, 0, 1); + b = CLAMP (b, 0, 1); + + // recompose tweaked color + rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); + if (pick_to_size) { + if(!trace_scale){ + if(pick_inverse_value) { + _scale = 1.0 - val; + } else { + _scale = val; + } + if(_scale == 0.0) { + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + if(!fit_item(desktop + , item + , bbox + , move + , center + , mode + , angle + , _scale + , scale + , picker + , pick_center + , pick_inverse_value + , pick_fill + , pick_stroke + , pick_no_overlap + , over_no_transparent + , over_transparent + , no_overlap + , offset + , css + , true + , pick + , do_trace + , pick_to_size + , pick_to_presence + , pick_to_color + , pick_to_opacity + , invert_picked + , gamma_picked + , rand_picked) + ) + { + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + } + } + + if (pick_to_opacity) { + if(pick_inverse_value) { + opacity *= 1.0 - val; + } else { + opacity *= val; + } + std::stringstream opacity_str; + opacity_str.imbue(std::locale::classic()); + opacity_str << opacity; + sp_repr_css_set_property(css, "opacity", opacity_str.str().c_str()); + } + if (pick_to_presence) { + if (g_random_double_range (0, 1) > val) { + //Hiding the element is a way to retain original + //behaviour of tiled clones for presence option. + sp_repr_css_set_property(css, "opacity", "0"); + } + } + if (pick_to_color) { + sp_svg_write_color(color_string, sizeof(color_string), rgba); + if(pick_fill){ + sp_repr_css_set_property(css, "fill", color_string); + } + if(pick_stroke){ + sp_repr_css_set_property(css, "stroke", color_string); + } + } + if (opacity < 1e-6) { // invisibly transparent, skip + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + } + if(!do_trace){ + if(!pick_center){ + rgba = rgba2; + } + if (pick_inverse_value) { + r = 1 - SP_RGBA32_R_F(rgba); + g = 1 - SP_RGBA32_G_F(rgba); + b = 1 - SP_RGBA32_B_F(rgba); + } else { + r = SP_RGBA32_R_F(rgba); + g = SP_RGBA32_G_F(rgba); + b = SP_RGBA32_B_F(rgba); + } + rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); + sp_svg_write_color(color_string, sizeof(color_string), rgba); + if(pick_fill){ + sp_repr_css_set_property(css, "fill", color_string); + } + if(pick_stroke){ + sp_repr_css_set_property(css, "stroke", color_string); + } + } + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + } + return true; +} + +static bool sp_spray_recursive(SPDesktop *desktop, + Inkscape::ObjectSet *set, + SPItem *item, + SPItem *&single_path_output, + Geom::Point p, + Geom::Point /*vector*/, + gint mode, + double radius, + double population, + double &scale, + double scale_variation, + bool /*reverse*/, + double mean, + double standard_deviation, + double ratio, + double tilt, + double rotation_variation, + gint _distrib, + bool no_overlap, + bool picker, + bool pick_center, + bool pick_inverse_value, + bool pick_fill, + bool pick_stroke, + bool pick_no_overlap, + bool over_no_transparent, + bool over_transparent, + double offset, + bool usepressurescale, + double pressure, + int pick, + bool do_trace, + bool pick_to_size, + bool pick_to_presence, + bool pick_to_color, + bool pick_to_opacity, + bool invert_picked, + double gamma_picked , + double rand_picked) +{ + bool did = false; + + { + // convert 3D boxes to ordinary groups before spraying their shapes + // TODO: ideally the original object is preserved. + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + desktop->getSelection()->remove(dynamic_cast<SPObject *>(item)); + set->remove(item); + item = box->convert_to_group(); + set->add(item); + desktop->getSelection()->add(dynamic_cast<SPObject *>(item)); + } + } + + double _fid = g_random_double_range(0, 1); + double angle = g_random_double_range( - rotation_variation / 100.0 * M_PI , rotation_variation / 100.0 * M_PI ); + double _scale = g_random_double_range( 1.0 - scale_variation / 100.0, 1.0 + scale_variation / 100.0 ); + if(usepressurescale){ + _scale = pressure; + } + double dr; double dp; + random_position( dr, dp, mean, standard_deviation, _distrib ); + dr=dr*radius; + + if (mode == SPRAY_MODE_COPY || mode == SPRAY_MODE_ERASER) { + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + if(_fid <= population) + { + SPDocument *doc = item->document; + gchar const * spray_origin; + if(!item->getAttribute("inkscape:spray-origin")){ + spray_origin = g_strdup_printf("#%s", item->getId()); + } else { + spray_origin = item->getAttribute("inkscape:spray-origin"); + } + Geom::Point center = item->getCenter(); + Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint()); + SPCSSAttr *css = sp_repr_css_attr_new(); + if (mode == SPRAY_MODE_ERASER || + pick_no_overlap || no_overlap || picker || + !over_transparent || !over_no_transparent) { + if(!fit_item(desktop + , item + , a + , move + , center + , mode + , angle + , _scale + , scale + , picker + , pick_center + , pick_inverse_value + , pick_fill + , pick_stroke + , pick_no_overlap + , over_no_transparent + , over_transparent + , no_overlap + , offset + , css + , false + , pick + , do_trace + , pick_to_size + , pick_to_presence + , pick_to_color + , pick_to_opacity + , invert_picked + , gamma_picked + , rand_picked)){ + return false; + } + } + SPItem *item_copied; + // Duplicate + Inkscape::XML::Document* xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *old_repr = item->getRepr(); + Inkscape::XML::Node *parent = old_repr->parent(); + Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc); + if(!copy->attribute("inkscape:spray-origin")){ + copy->setAttribute("inkscape:spray-origin", spray_origin); + } + parent->appendChild(copy); + SPObject *new_obj = doc->getObjectByRepr(copy); + item_copied = dynamic_cast<SPItem *>(new_obj); // Conversion object->item + sp_spray_scale_rel(center,desktop, item_copied, Geom::Scale(_scale)); + sp_spray_scale_rel(center,desktop, item_copied, Geom::Scale(scale)); + sp_spray_rotate_rel(center,desktop,item_copied, Geom::Rotate(angle)); + // Move the cursor p + item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation())); + Inkscape::GC::release(copy); + if(picker){ + sp_desktop_apply_css_recursive(item_copied, css, true); + } + did = true; + } + } +#ifdef ENABLE_SPRAY_MODE_SINGLE_PATH + } else if (mode == SPRAY_MODE_SINGLE_PATH) { + if (item) { + SPDocument *doc = item->document; + Inkscape::XML::Document* xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *old_repr = item->getRepr(); + Inkscape::XML::Node *parent = old_repr->parent(); + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + if (_fid <= population) { // Rules the population of objects sprayed + // Duplicates the parent item + Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc); + gchar const * spray_origin; + if(!copy->attribute("inkscape:spray-origin")){ + spray_origin = g_strdup_printf("#%s", old_repr->attribute("id")); + } else { + spray_origin = copy->attribute("inkscape:spray-origin"); + } + parent->appendChild(copy); + SPObject *new_obj = doc->getObjectByRepr(copy); + SPItem *item_copied = dynamic_cast<SPItem *>(new_obj); + + // Move around the cursor + Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint()); + + Geom::Point center = item->getCenter(); + sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(_scale, _scale)); + sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(scale, scale)); + sp_spray_rotate_rel(center, desktop, item_copied, Geom::Rotate(angle)); + item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation())); + + // Union + // only works if no groups in selection + ObjectSet object_set_tmp = *desktop->getSelection(); + object_set_tmp.clear(); + object_set_tmp.add(item_copied); + object_set_tmp.removeLPESRecursive(true); + if (dynamic_cast<SPUse*>(object_set_tmp.objects().front())) { + object_set_tmp.unlinkRecursive(true); + } + if (single_path_output) { // Previous result + object_set_tmp.add(single_path_output); + } + object_set_tmp.pathUnion(true); + single_path_output = object_set_tmp.items().front(); + for (auto item : object_set_tmp.items()) { + auto repr = item->getRepr(); + repr->setAttribute("inkscape:spray-origin", spray_origin); + } + object_set_tmp.clear(); + Inkscape::GC::release(copy); + did = true; + } + } + } +#endif + } else if (mode == SPRAY_MODE_CLONE) { + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + if(_fid <= population) { + SPDocument *doc = item->document; + gchar const * spray_origin; + if(!item->getAttribute("inkscape:spray-origin")){ + spray_origin = g_strdup_printf("#%s", item->getId()); + } else { + spray_origin = item->getAttribute("inkscape:spray-origin"); + } + Geom::Point center=item->getCenter(); + Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint()); + SPCSSAttr *css = sp_repr_css_attr_new(); + if (mode == SPRAY_MODE_ERASER || + pick_no_overlap || no_overlap || picker || + !over_transparent || !over_no_transparent) { + if(!fit_item(desktop + , item + , a + , move + , center + , mode + , angle + , _scale + , scale + , picker + , pick_center + , pick_inverse_value + , pick_fill + , pick_stroke + , pick_no_overlap + , over_no_transparent + , over_transparent + , no_overlap + , offset + , css + , true + , pick + , do_trace + , pick_to_size + , pick_to_presence + , pick_to_color + , pick_to_opacity + , invert_picked + , gamma_picked + , rand_picked)) + { + return false; + } + } + SPItem *item_copied; + Inkscape::XML::Document* xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *old_repr = item->getRepr(); + Inkscape::XML::Node *parent = old_repr->parent(); + + // Creation of the clone + Inkscape::XML::Node *clone = xml_doc->createElement("svg:use"); + // Ad the clone to the list of the parent's children + parent->appendChild(clone); + // Generates the link between parent and child attributes + if(!clone->attribute("inkscape:spray-origin")){ + clone->setAttribute("inkscape:spray-origin", spray_origin); + } + gchar *href_str = g_strdup_printf("#%s", old_repr->attribute("id")); + clone->setAttribute("xlink:href", href_str); + g_free(href_str); + + SPObject *clone_object = doc->getObjectByRepr(clone); + // Conversion object->item + item_copied = dynamic_cast<SPItem *>(clone_object); + sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(_scale, _scale)); + sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(scale, scale)); + sp_spray_rotate_rel(center, desktop, item_copied, Geom::Rotate(angle)); + item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation())); + if(picker){ + sp_desktop_apply_css_recursive(item_copied, css, true); + } + Inkscape::GC::release(clone); + did = true; + } + } + } + + return did; +} + +static bool sp_spray_dilate(SprayTool *tc, Geom::Point /*event_p*/, Geom::Point p, Geom::Point vector, bool reverse) +{ + SPDesktop *desktop = tc->getDesktop(); + Inkscape::ObjectSet *set = tc->objectSet(); + if (set->isEmpty()) { + return false; + } + + bool did = false; + double radius = get_dilate_radius(tc); + double population = get_population(tc); + if (radius == 0 || population == 0) { + return false; + } + double path_mean = get_path_mean(tc); + if (radius == 0 || path_mean == 0) { + return false; + } + double path_standard_deviation = get_path_standard_deviation(tc); + if (radius == 0 || path_standard_deviation == 0) { + return false; + } + double move_mean = get_move_mean(tc); + double move_standard_deviation = get_move_standard_deviation(tc); + + { + std::vector<SPItem*> const items(set->items().begin(), set->items().end()); + + for(auto item : items){ + g_assert(item != nullptr); + sp_object_ref(item); + } + + for(auto item : items){ + g_assert(item != nullptr); + if (sp_spray_recursive(desktop + , set + , item + , tc->single_path_output + , p, vector + , tc->mode + , radius + , population + , tc->scale + , tc->scale_variation + , reverse + , move_mean + , move_standard_deviation + , tc->ratio + , tc->tilt + , tc->rotation_variation + , tc->distrib + , tc->no_overlap + , tc->picker + , tc->pick_center + , tc->pick_inverse_value + , tc->pick_fill + , tc->pick_stroke + , tc->pick_no_overlap + , tc->over_no_transparent + , tc->over_transparent + , tc->offset + , tc->usepressurescale + , get_pressure(tc) + , tc->pick + , tc->do_trace + , tc->pick_to_size + , tc->pick_to_presence + , tc->pick_to_color + , tc->pick_to_opacity + , tc->invert_picked + , tc->gamma_picked + , tc->rand_picked)) { + did = true; + } + } + + for(auto item : items){ + g_assert(item != nullptr); + sp_object_unref(item); + } + } + + return did; +} + +static void sp_spray_update_area(SprayTool *tc) +{ + double radius = get_dilate_radius(tc); + Geom::Affine const sm ( Geom::Scale(radius/(1-tc->ratio), radius/(1+tc->ratio)) * + Geom::Rotate(tc->tilt) * + Geom::Translate(tc->getDesktop()->point())); + + Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); // Unit circle centered at origin. + path *= sm; + tc->dilate_area->set_bpath(path); + tc->dilate_area->show(); +} + +static void sp_spray_switch_mode(SprayTool *tc, gint mode, bool with_shift) +{ + // Select the button mode + auto tb = dynamic_cast<UI::Toolbar::SprayToolbar*>(tc->getDesktop()->get_toolbar_by_name("SprayToolbar")); + + if(tb) { + tb->set_mode(mode); + } else { + std::cerr << "Could not access Spray toolbar" << std::endl; + } + + // Need to set explicitly, because the prefs may not have changed by the previous + tc->mode = mode; + tc->update_cursor(with_shift); +} + +bool SprayTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_ENTER_NOTIFY: + dilate_area->show(); + break; + case GDK_LEAVE_NOTIFY: + dilate_area->hide(); + break; + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + _desktop->getSelection()->restoreBackup(); + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return TRUE; + } + this->setCloneTilerPrefs(); + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + this->last_push = _desktop->dt2doc(motion_dt); + + sp_spray_extinput(this, event); + + set_high_motion_precision(); + this->is_drawing = true; + this->is_dilating = true; + this->has_dilated = false; + + object_set = *_desktop->getSelection(); + if (mode == SPRAY_MODE_SINGLE_PATH) { + this->single_path_output = nullptr; + } + + sp_spray_dilate(this, motion_w, this->last_push, Geom::Point(0,0), MOD__SHIFT(event)); + + this->has_dilated = true; + ret = TRUE; + } + break; + case GDK_MOTION_NOTIFY: { + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + Geom::Point motion_doc(_desktop->dt2doc(motion_dt)); + sp_spray_extinput(this, event); + + // Draw the dilating cursor + double radius = get_dilate_radius(this); + Geom::Affine const sm (Geom::Scale(radius/(1-this->ratio), radius/(1+this->ratio)) * + Geom::Rotate(this->tilt) * + Geom::Translate(_desktop->w2d(motion_w))); + + Geom::PathVector path = Geom::Path(Geom::Circle(0, 0, 1)); // Unit circle centered at origin. + path *= sm; + this->dilate_area->set_bpath(path); + this->dilate_area->show(); + + guint num = 0; + if (!_desktop->selection->isEmpty()) { + num = (guint)boost::distance(_desktop->selection->items()); + } + if (num == 0) { + this->message_context->flash(Inkscape::ERROR_MESSAGE, _("<b>Nothing selected!</b> Select objects to spray.")); + } + + // Dilating: + if (this->is_drawing && ( event->motion.state & GDK_BUTTON1_MASK )) { + sp_spray_dilate(this, motion_w, motion_doc, motion_doc - this->last_push, event->button.state & GDK_SHIFT_MASK? true : false); + //this->last_push = motion_doc; + this->has_dilated = true; + + // It's slow, so prevent clogging up with events + gobble_motion_events(GDK_BUTTON1_MASK); + return TRUE; + } + } + break; + /* Spray with the scroll */ + case GDK_SCROLL: { + if (event->scroll.state & GDK_BUTTON1_MASK) { + double temp ; + temp = this->population; + this->population = 1.0; + _desktop->setToolboxAdjustmentValue("population", this->population * 100); + Geom::Point const scroll_w(event->button.x, event->button.y); + Geom::Point const scroll_dt = _desktop->point();; + + switch (event->scroll.direction) { + case GDK_SCROLL_DOWN: + case GDK_SCROLL_UP: + case GDK_SCROLL_SMOOTH: { + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return TRUE; + } + this->last_push = _desktop->dt2doc(scroll_dt); + sp_spray_extinput(this, event); + this->is_drawing = true; + this->is_dilating = true; + this->has_dilated = false; + if(this->is_dilating) { + sp_spray_dilate(this, scroll_w, _desktop->dt2doc(scroll_dt), Geom::Point(0, 0), false); + } + this->has_dilated = true; + + this->population = temp; + _desktop->setToolboxAdjustmentValue("population", this->population * 100); + + ret = TRUE; + } + break; + case GDK_SCROLL_RIGHT: + {} break; + case GDK_SCROLL_LEFT: + {} break; + } + } + break; + } + + case GDK_BUTTON_RELEASE: { + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + + set_high_motion_precision(false); + this->is_drawing = false; + + if (this->is_dilating && event->button.button == 1) { + if (!this->has_dilated) { + // If we did not rub, do a light tap + this->pressure = 0.03; + sp_spray_dilate(this, motion_w, _desktop->dt2doc(motion_dt), Geom::Point(0,0), MOD__SHIFT(event)); + } + this->is_dilating = false; + this->has_dilated = false; + switch (this->mode) { + case SPRAY_MODE_COPY: + DocumentUndo::done(_desktop->getDocument(), _("Spray with copies"), INKSCAPE_ICON("tool-spray")); + break; + case SPRAY_MODE_CLONE: + DocumentUndo::done(_desktop->getDocument(), _("Spray with clones"), INKSCAPE_ICON("tool-spray")); + break; + case SPRAY_MODE_SINGLE_PATH: + DocumentUndo::done(_desktop->getDocument(), _("Spray in single path"), INKSCAPE_ICON("tool-spray")); + break; + } + } + _desktop->getSelection()->clear(); + object_set.clear(); + break; + } + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_j: + case GDK_KEY_J: + if (MOD__SHIFT_ONLY(event)) { + sp_spray_switch_mode(this, SPRAY_MODE_COPY, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_k: + case GDK_KEY_K: + if (MOD__SHIFT_ONLY(event)) { + sp_spray_switch_mode(this, SPRAY_MODE_CLONE, MOD__SHIFT(event)); + ret = TRUE; + } + break; +#ifdef ENABLE_SPRAY_MODE_SINGLE_PATH + case GDK_KEY_l: + case GDK_KEY_L: + if (MOD__SHIFT_ONLY(event)) { + sp_spray_switch_mode(this, SPRAY_MODE_SINGLE_PATH, MOD__SHIFT(event)); + ret = TRUE; + } + break; +#endif + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + if (!MOD__CTRL_ONLY(event)) { + this->population += 0.01; + if (this->population > 1.0) { + this->population = 1.0; + } + _desktop->setToolboxAdjustmentValue("spray-population", this->population * 100); + ret = TRUE; + } + break; + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + if (!MOD__CTRL_ONLY(event)) { + this->population -= 0.01; + if (this->population < 0.0) { + this->population = 0.0; + } + _desktop->setToolboxAdjustmentValue("spray-population", this->population * 100); + ret = TRUE; + } + break; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (!MOD__CTRL_ONLY(event)) { + this->width += 0.01; + if (this->width > 1.0) { + this->width = 1.0; + } + // The same spinbutton is for alt+x + _desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); + sp_spray_update_area(this); + ret = TRUE; + } + break; + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (!MOD__CTRL_ONLY(event)) { + this->width -= 0.01; + if (this->width < 0.01) { + this->width = 0.01; + } + _desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); + sp_spray_update_area(this); + ret = TRUE; + } + break; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + this->width = 0.01; + _desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); + sp_spray_update_area(this); + ret = TRUE; + break; + case GDK_KEY_End: + case GDK_KEY_KP_End: + this->width = 1.0; + _desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); + sp_spray_update_area(this); + ret = TRUE; + break; + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("spray-width"); + ret = TRUE; + } + break; + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->update_cursor(true); + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + break; + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + case GDK_KEY_RELEASE: { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->update_cursor(false); + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + sp_spray_switch_mode (this, prefs->getInt("/tools/spray/mode"), MOD__SHIFT(event)); + this->message_context->clear(); + break; + default: + sp_spray_switch_mode (this, prefs->getInt("/tools/spray/mode"), MOD__SHIFT(event)); + break; + } + } + + default: + break; + } + + if (!ret) { +// if ((SP_EVENT_CONTEXT_CLASS(sp_spray_context_parent_class))->root_handler) { +// ret = (SP_EVENT_CONTEXT_CLASS(sp_spray_context_parent_class))->root_handler(event_context, event); +// } + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + +/* + 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/tools/spray-tool.h b/src/ui/tools/spray-tool.h new file mode 100644 index 0000000..8216e2d --- /dev/null +++ b/src/ui/tools/spray-tool.h @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_SPRAY_CONTEXT_H__ +#define __SP_SPRAY_CONTEXT_H__ + +/* + * Spray Tool + * + * Authors: + * Pierre-Antoine MARC + * Pierre CACLIN + * Aurel-Aimé MARMION + * Julien LERAY + * Benoît LAVORATA + * Vincent MONTAGNE + * Pierre BARBRY-BLOT + * Jabiertxo ARRAIZA + * Adrian Boguszewski + * + * Copyright (C) 2009 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include "ui/tools/tool-base.h" +#include "object/object-set.h" + +#define SP_SPRAY_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::SprayTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_SPRAY_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::SprayTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { + class CanvasItemBpath; + namespace UI { + namespace Dialog { + class Dialog; + } + } +} + + +#define SAMPLING_SIZE 8 /* fixme: ?? */ + +#define TC_MIN_PRESSURE 0.0 +#define TC_MAX_PRESSURE 1.0 +#define TC_DEFAULT_PRESSURE 0.35 + +namespace Inkscape { +namespace UI { +namespace Tools { + +enum { + SPRAY_MODE_COPY, + SPRAY_MODE_CLONE, + SPRAY_MODE_SINGLE_PATH, + SPRAY_MODE_ERASER, + SPRAY_OPTION, +}; + +class SprayTool : public ToolBase { +public: + SprayTool(SPDesktop *desktop); + ~SprayTool() override; + + //ToolBase event_context; + /* extended input data */ + gdouble pressure; + + /* attributes */ + bool dragging; /* mouse state: mouse is dragging */ + bool usepressurewidth; + bool usepressurepopulation; + bool usepressurescale; + bool usetilt; + bool usetext; + + double width; + double ratio; + double tilt; + double rotation_variation; + double population; + double scale_variation; + double scale; + double mean; + double standard_deviation; + + gint distrib; + + gint mode; + + bool is_drawing; + + bool is_dilating; + bool has_dilated; + Geom::Point last_push; + Inkscape::CanvasItemBpath *dilate_area; + bool no_overlap; + bool picker; + bool pick_center; + bool pick_inverse_value; + bool pick_fill; + bool pick_stroke; + bool pick_no_overlap; + bool over_transparent; + bool over_no_transparent; + double offset; + int pick; + bool do_trace; + bool pick_to_size; + bool pick_to_presence; + bool pick_to_color; + bool pick_to_opacity; + bool invert_picked; + double gamma_picked; + double rand_picked; + sigc::connection style_set_connection; + + void set(const Inkscape::Preferences::Entry& val) override; + virtual void setCloneTilerPrefs(); + bool root_handler(GdkEvent* event) override; + void update_cursor(bool /*with_shift*/); + + ObjectSet* objectSet() { + return &object_set; + } + SPItem* single_path_output = nullptr; + +private: + ObjectSet object_set; +}; + +} +} +} + +#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/tools/star-tool.cpp b/src/ui/tools/star-tool.cpp new file mode 100644 index 0000000..bbdd9cb --- /dev/null +++ b/src/ui/tools/star-tool.cpp @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Star drawing context + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2001-2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "star-tool.h" + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "selection.h" + +#include "include/macros.h" + +#include "object/sp-namedview.h" +#include "object/sp-star.h" + +#include "ui/icon-names.h" +#include "ui/shape-editor.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +StarTool::StarTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/shapes/star", "star.svg") + , star(nullptr) + , magnitude(5) + , proportion(0.5) + , isflatsided(false) + , rounded(0) + , randomized(0) +{ + sp_event_context_read(this, "isflatsided"); + sp_event_context_read(this, "magnitude"); + sp_event_context_read(this, "proportion"); + sp_event_context_read(this, "rounded"); + sp_event_context_read(this, "randomized"); + + this->shape_editor = new ShapeEditor(desktop); + + SPItem *item = desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + Inkscape::Selection *selection = desktop->getSelection(); + + this->sel_changed_connection.disconnect(); + + this->sel_changed_connection = selection->connectChanged(sigc::mem_fun(this, &StarTool::selection_changed)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +StarTool::~StarTool() { + ungrabCanvasEvents(); + + this->finishItem(); + this->sel_changed_connection.disconnect(); + + this->enableGrDrag(false); + + delete this->shape_editor; + this->shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->star) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + * + * @param selection Should not be NULL. + */ +void StarTool::selection_changed(Inkscape::Selection* selection) { + g_assert (selection != nullptr); + + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); +} + + +void StarTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "magnitude") { + this->magnitude = CLAMP(val.getInt(5), this->isflatsided ? 3 : 2, 1024); + } else if (path == "proportion") { + this->proportion = CLAMP(val.getDouble(0.5), 0.01, 2.0); + } else if (path == "isflatsided") { + this->isflatsided = val.getBool(); + } else if (path == "rounded") { + this->rounded = val.getDouble(); + } else if (path == "randomized") { + this->randomized = val.getDouble(); + } +} + +bool StarTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = _desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + dragging = true; + + this->center = this->setup_for_drag_start(event); + + /* Snap center */ + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop, true); + m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + grabCanvasEvents(); + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && (event->motion.state & GDK_BUTTON1_MASK)) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->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 draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + + this->drag(motion_dt, event->motion.state); + + gobble_motion_events(GDK_BUTTON1_MASK); + + ret = TRUE; + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + + if (event->button.button == 1) { + dragging = false; + + this->discard_delayed_snap_event(); + + if (!this->within_tolerance) { + // we've been dragging, finish the star + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = nullptr; + ret = TRUE; + ungrabCanvasEvents(); + } + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + sp_event_show_modifier_tip(this->defaultMessageContext(), event, + _("<b>Ctrl</b>: snap angle; keep rays radial"), + nullptr, + nullptr); + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("altx-star"); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + this->discard_delayed_snap_event(); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_space: + if (dragging) { + ungrabCanvasEvents(); + + dragging = false; + + this->discard_delayed_snap_event(); + + if (!this->within_tolerance) { + // we've been dragging, finish the star + this->finishItem(); + } + // do not return true, so that space would work switching to selector + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void StarTool::drag(Geom::Point p, guint state) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + if (!this->star) { + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "star"); + + // Set style + sp_desktop_apply_style_tool(_desktop, repr, "/tools/shapes/star", false); + + this->star = SP_STAR(currentLayer()->appendChildRepr(repr)); + + Inkscape::GC::release(repr); + this->star->transform = currentLayer()->i2doc_affine().inverse(); + this->star->updateRepr(); + } + + /* Snap corner point with no constraints */ + SnapManager &m = _desktop->namedview->snap_manager; + + m.setup(_desktop, true, this->star); + Geom::Point pt2g = p; + m.freeSnapReturnByRef(pt2g, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + Geom::Point const p0 = _desktop->dt2doc(this->center); + Geom::Point const p1 = _desktop->dt2doc(pt2g); + + double const sides = (gdouble) this->magnitude; + Geom::Point const d = p1 - p0; + Geom::Coord const r1 = Geom::L2(d); + double arg1 = atan2(d); + + if (state & GDK_CONTROL_MASK) { + /* Snap angle */ + double snaps_radian = M_PI/snaps; + arg1 = std::round(arg1/snaps_radian) * snaps_radian; + } + + sp_star_position_set(this->star, this->magnitude, p0, r1, r1 * this->proportion, + arg1, arg1 + M_PI / sides, this->isflatsided, this->rounded, this->randomized); + + /* status text */ + Inkscape::Util::Quantity q = Inkscape::Util::Quantity(r1, "px"); + Glib::ustring rads = q.string(_desktop->namedview->display_units); + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + ( this->isflatsided? + _("<b>Polygon</b>: radius %s, angle %.2f°; with <b>Ctrl</b> to snap angle") : + _("<b>Star</b>: radius %s, angle %.2f°; with <b>Ctrl</b> to snap angle") ), + rads.c_str(), arg1 * 180 / M_PI); +} + +void StarTool::finishItem() { + this->message_context->clear(); + + if (this->star != nullptr) { + if (this->star->r[1] == 0) { + // Don't allow the creating of zero sized arc, for example + // when the start and and point snap to the snap grid point + this->cancel(); + return; + } + + // Set transform center, so that odd stars rotate correctly + // LP #462157 + this->star->setCenter(this->center); + this->star->set_shape(); + this->star->updateRepr(SP_OBJECT_WRITE_EXT); + // compensate stroke scaling couldn't be done in doWriteTransform + double const expansion = this->star->transform.descrim(); + this->star->doWriteTransform(this->star->transform, nullptr, true); + this->star->adjust_stroke_width_recursive(expansion); + + _desktop->getSelection()->set(this->star); + DocumentUndo::done(_desktop->getDocument(), _("Create star"), INKSCAPE_ICON("draw-polygon-star")); + + this->star = nullptr; + } +} + +void StarTool::cancel() { + _desktop->getSelection()->clear(); + ungrabCanvasEvents(); + + if (this->star != nullptr) { + this->star->deleteObject(); + this->star = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + DocumentUndo::cancel(_desktop->getDocument()); +} + +} +} +} + +/* + 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/tools/star-tool.h b/src/ui/tools/star-tool.h new file mode 100644 index 0000000..4a06a42 --- /dev/null +++ b/src/ui/tools/star-tool.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_STAR_CONTEXT_H__ +#define __SP_STAR_CONTEXT_H__ + +/* + * Star drawing context + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2001-2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +class SPStar; + +namespace Inkscape { + +class Selection; + +namespace UI { +namespace Tools { + +class StarTool : public ToolBase { +public: + StarTool(SPDesktop *desktop); + ~StarTool() override; + + void set(const Inkscape::Preferences::Entry &val) override; + bool root_handler(GdkEvent *event) override; + +private: + SPStar *star; + + Geom::Point center; + + /* Number of corners */ + gint magnitude; + + /* Outer/inner radius ratio */ + gdouble proportion; + + /* flat sides or not? */ + bool isflatsided; + + /* rounded corners ratio */ + gdouble rounded; + + // randomization + gdouble randomized; + + sigc::connection sel_changed_connection; + + void drag(Geom::Point p, guint state); + void finishItem(); + void cancel(); + void selection_changed(Inkscape::Selection* selection); +}; + +} +} +} + +#endif diff --git a/src/ui/tools/text-tool.cpp b/src/ui/tools/text-tool.cpp new file mode 100644 index 0000000..12f143e --- /dev/null +++ b/src/ui/tools/text-tool.cpp @@ -0,0 +1,1933 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * TextTool + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include <gdk/gdkkeysyms.h> +#include <gtkmm/clipboard.h> +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include "text-tool.h" + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "include/macros.h" +#include "inkscape.h" +#include "message-context.h" +#include "message-stack.h" +#include "rubberband.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "style.h" +#include "text-editing.h" + +#include "display/control/canvas-item-curve.h" +#include "display/control/canvas-item-quad.h" +#include "display/control/canvas-item-rect.h" +#include "display/control/canvas-item-bpath.h" +#include "display/curve.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "object/sp-flowtext.h" +#include "object/sp-namedview.h" +#include "object/sp-text.h" +#include "object/sp-textpath.h" +#include "object/sp-rect.h" +#include "object/sp-shape.h" +#include "object/sp-ellipse.h" + +#include "ui/knot/knot-holder.h" +#include "ui/icon-names.h" +#include "ui/shape-editor.h" +#include "ui/widget/canvas.h" +#include "ui/event-debug.h" + +#include "xml/attribute-record.h" +#include "xml/node-event-vector.h" +#include "xml/sp-css-attr.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void sp_text_context_validate_cursor_iterators(TextTool *tc); +static void sp_text_context_update_cursor(TextTool *tc, bool scroll_to_see = true); +static void sp_text_context_update_text_selection(TextTool *tc); +static gint sp_text_context_timeout(TextTool *tc); +static void sp_text_context_forget_text(TextTool *tc); + +static gint sptc_focus_in(GtkWidget *widget, GdkEventFocus *event, TextTool *tc); +static gint sptc_focus_out(GtkWidget *widget, GdkEventFocus *event, TextTool *tc); +static void sptc_commit(GtkIMContext *imc, gchar *string, TextTool *tc); + +TextTool::TextTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/text", "text.svg") +{ + GtkSettings* settings = gtk_settings_get_default(); + gint timeout = 0; + g_object_get( settings, "gtk-cursor-blink-time", &timeout, nullptr ); + + if (timeout < 0) { + timeout = 200; + } else { + timeout /= 2; + } + + cursor = new Inkscape::CanvasItemCurve(desktop->getCanvasControls()); + cursor->set_stroke(0x000000ff); + cursor->hide(); + + // The rectangle box tightly wrapping text object when selected or under cursor. + indicator = new Inkscape::CanvasItemRect(desktop->getCanvasControls()); + indicator->set_stroke(0x0000ff7f); + indicator->set_shadow(0xffffff7f, 1); + indicator->hide(); + + // The shape that the text is flowing into + frame = new Inkscape::CanvasItemBpath(desktop->getCanvasControls()); + frame->set_fill(0x00 /* zero alpha */, SP_WIND_RULE_NONZERO); + frame->set_stroke(0x0000ff7f); + frame->hide(); + + // A second frame for showing the padding of the above frame + padding_frame = new Inkscape::CanvasItemBpath(desktop->getCanvasControls()); + padding_frame->set_fill(0x00 /* zero alpha */, SP_WIND_RULE_NONZERO); + padding_frame->set_stroke(0xccccccdf); + padding_frame->hide(); + + this->timeout = g_timeout_add(timeout, (GSourceFunc) sp_text_context_timeout, this); + + this->imc = gtk_im_multicontext_new(); + if (this->imc) { + GtkWidget *canvas = GTK_WIDGET(desktop->getCanvas()->gobj()); + + /* im preedit handling is very broken in inkscape for + * multi-byte characters. See bug 1086769. + * We need to let the IM handle the preediting, and + * just take in the characters when they're finished being + * entered. + */ + gtk_im_context_set_use_preedit(this->imc, FALSE); + gtk_im_context_set_client_window(this->imc, + gtk_widget_get_window (canvas)); + + g_signal_connect(G_OBJECT(canvas), "focus_in_event", G_CALLBACK(sptc_focus_in), this); + g_signal_connect(G_OBJECT(canvas), "focus_out_event", G_CALLBACK(sptc_focus_out), this); + g_signal_connect(G_OBJECT(this->imc), "commit", G_CALLBACK(sptc_commit), this); + + if (gtk_widget_has_focus(canvas)) { + sptc_focus_in(canvas, nullptr, this); + } + } + + this->shape_editor = new ShapeEditor(desktop); + + SPItem *item = desktop->getSelection()->singleItem(); + if (item && (SP_IS_FLOWTEXT(item) || SP_IS_TEXT(item))) { + this->shape_editor->set_item(item); + } + + this->sel_changed_connection = _desktop->getSelection()->connectChangedFirst( + sigc::mem_fun(*this, &TextTool::_selectionChanged) + ); + this->sel_modified_connection = _desktop->getSelection()->connectModifiedFirst( + sigc::mem_fun(*this, &TextTool::_selectionModified) + ); + this->style_set_connection = _desktop->connectSetStyle( + sigc::mem_fun(*this, &TextTool::_styleSet) + ); + this->style_query_connection = _desktop->connectQueryStyle( + sigc::mem_fun(*this, &TextTool::_styleQueried) + ); + + _selectionChanged(desktop->getSelection()); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/text/selcue")) { + this->enableSelectionCue(); + } + if (prefs->getBool("/tools/text/gradientdrag")) { + this->enableGrDrag(); + } +} + +TextTool::~TextTool() +{ + if (_desktop) { + sp_signal_disconnect_by_data(_desktop->getCanvas()->gobj(), this); + } + + this->enableGrDrag(false); + + this->style_set_connection.disconnect(); + this->style_query_connection.disconnect(); + this->sel_changed_connection.disconnect(); + this->sel_modified_connection.disconnect(); + + sp_text_context_forget_text(SP_TEXT_CONTEXT(this)); + + if (this->imc) { + g_object_unref(G_OBJECT(this->imc)); + this->imc = nullptr; + } + + if (this->timeout) { + g_source_remove(this->timeout); + this->timeout = 0; + } + + if (cursor) { + delete cursor; + cursor = nullptr; + } + + if (this->indicator) { + delete indicator; + this->indicator = nullptr; + } + + if (this->frame) { + delete frame; + this->frame = nullptr; + } + + if (this->padding_frame) { + delete padding_frame; + this->padding_frame = nullptr; + } + + for (auto & text_selection_quad : text_selection_quads) { + text_selection_quad->hide(); + delete text_selection_quad; + } + text_selection_quads.clear(); + + delete this->shape_editor; + this->shape_editor = nullptr; + + ungrabCanvasEvents(); + + Inkscape::Rubberband::get(_desktop)->stop(); +} + +void TextTool::deleteSelected() +{ + Inkscape::UI::Tools::sp_text_delete_selection(_desktop->event_context); + DocumentUndo::done(_desktop->getDocument(), _("Delete text"), INKSCAPE_ICON("draw-text")); +} + +bool TextTool::item_handler(SPItem* item, GdkEvent* event) { + SPItem *item_ungrouped; + + gint ret = FALSE; + sp_text_context_validate_cursor_iterators(this); + Inkscape::Text::Layout::iterator old_start = this->text_sel_start; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + // this var allow too much lees subbselection queries + // reducing it to cursor iteracion, mouseup and down + // find out clicked item, disregarding groups + item_ungrouped = _desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE); + if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) { + _desktop->getSelection()->set(item_ungrouped); + if (this->text) { + // find out click point in document coordinates + Geom::Point p = _desktop->w2d(Geom::Point(event->button.x, event->button.y)); + // set the cursor closest to that point + if (event->button.state & GDK_SHIFT_MASK) { + this->text_sel_start = old_start; + this->text_sel_end = sp_te_get_position_by_coords(this->text, p); + } else { + this->text_sel_start = this->text_sel_end = sp_te_get_position_by_coords(this->text, p); + } + // update display + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + this->dragging = 1; + } + ret = TRUE; + } + } + break; + case GDK_2BUTTON_PRESS: + if (event->button.button == 1 && this->text && this->dragging) { + Inkscape::Text::Layout const *layout = te_get_layout(this->text); + if (layout) { + if (!layout->isStartOfWord(this->text_sel_start)) + this->text_sel_start.prevStartOfWord(); + if (!layout->isEndOfWord(this->text_sel_end)) + this->text_sel_end.nextEndOfWord(); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + this->dragging = 2; + ret = TRUE; + } + } + break; + case GDK_3BUTTON_PRESS: + if (event->button.button == 1 && this->text && this->dragging) { + this->text_sel_start.thisStartOfLine(); + this->text_sel_end.thisEndOfLine(); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + this->dragging = 3; + ret = TRUE; + } + break; + case GDK_BUTTON_RELEASE: + if (event->button.button == 1 && this->dragging) { + this->dragging = 0; + this->discard_delayed_snap_event(); + ret = TRUE; + _desktop->emit_text_cursor_moved(this, this); + } + break; + case GDK_MOTION_NOTIFY: + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::item_handler(item, event); + } + + return ret; +} + +static void sp_text_context_setup_text(TextTool *tc) +{ + SPDesktop *desktop = tc->getDesktop(); + + /* Create <text> */ + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text"); + rtext->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create + + /* Set style */ + sp_desktop_apply_style_tool(desktop, rtext, "/tools/text", true); + + rtext->setAttributeSvgDouble("x", tc->pdoc[Geom::X]); + rtext->setAttributeSvgDouble("y", tc->pdoc[Geom::Y]); + + /* Create <tspan> */ + Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan"); + rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan? + rtext->addChild(rtspan, nullptr); + Inkscape::GC::release(rtspan); + + /* Create TEXT */ + Inkscape::XML::Node *rstring = xml_doc->createTextNode(""); + rtspan->addChild(rstring, nullptr); + Inkscape::GC::release(rstring); + SPItem *text_item = SP_ITEM(tc->currentLayer()->appendChildRepr(rtext)); + /* fixme: Is selection::changed really immediate? */ + /* yes, it's immediate .. why does it matter? */ + desktop->getSelection()->set(text_item); + Inkscape::GC::release(rtext); + text_item->transform = tc->currentLayer()->i2doc_affine().inverse(); + + text_item->updateRepr(); + text_item->doWriteTransform(text_item->transform, nullptr, true); + DocumentUndo::done(desktop->getDocument(), _("Create text"), INKSCAPE_ICON("draw-text")); +} + +/** + * Insert the character indicated by tc.uni to replace the current selection, + * and reset tc.uni/tc.unipos to empty string. + * + * \pre tc.uni/tc.unipos non-empty. + */ +static void insert_uni_char(TextTool *const tc) +{ + g_return_if_fail(tc->unipos + && tc->unipos < sizeof(tc->uni) + && tc->uni[tc->unipos] == '\0'); + unsigned int uv; + std::stringstream ss; + ss << std::hex << tc->uni; + ss >> uv; + tc->unipos = 0; + tc->uni[tc->unipos] = '\0'; + + if ( !g_unichar_isprint(static_cast<gunichar>(uv)) + && !(g_unichar_validate(static_cast<gunichar>(uv)) && (g_unichar_type(static_cast<gunichar>(uv)) == G_UNICODE_PRIVATE_USE) ) ) { + // This may be due to bad input, so it goes to statusbar. + tc->getDesktop()->messageStack()->flash(Inkscape::ERROR_MESSAGE, + _("Non-printable character")); + } else { + if (!tc->text) { // printable key; create text if none (i.e. if nascent_object) + sp_text_context_setup_text(tc); + tc->nascent_object = false; // we don't need it anymore, having created a real <text> + } + + gchar u[10]; + guint const len = g_unichar_to_utf8(uv, u); + u[len] = '\0'; + + tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, u); + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); + DocumentUndo::done(tc->getDesktop()->getDocument(), _("Insert Unicode character"), INKSCAPE_ICON("draw-text")); + } +} + +static void hex_to_printable_utf8_buf(char const *const ehex, char *utf8) +{ + unsigned int uv; + std::stringstream ss; + ss << std::hex << ehex; + ss >> uv; + if (!g_unichar_isprint((gunichar) uv)) { + uv = 0xfffd; + } + guint const len = g_unichar_to_utf8(uv, utf8); + utf8[len] = '\0'; +} + +static void show_curr_uni_char(TextTool *const tc) +{ + g_return_if_fail(tc->unipos < sizeof(tc->uni) + && tc->uni[tc->unipos] == '\0'); + if (tc->unipos) { + char utf8[10]; + hex_to_printable_utf8_buf(tc->uni, utf8); + + /* Status bar messages are in pango markup, so we need xml escaping. */ + if (utf8[1] == '\0') { + switch(utf8[0]) { + case '<': strcpy(utf8, "<"); break; + case '>': strcpy(utf8, ">"); break; + case '&': strcpy(utf8, "&"); break; + default: break; + } + } + tc->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE, + _("Unicode (<b>Enter</b> to finish): %s: %s"), tc->uni, utf8); + } else { + tc->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): ")); + } +} + +bool TextTool::root_handler(GdkEvent* event) { + +#if EVENT_DEBUG + ui_dump_event(reinterpret_cast<GdkEvent *>(event), "TextTool::root_handler"); +#endif + + indicator->hide(); + + sp_text_context_validate_cursor_iterators(this); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + if (Inkscape::have_viable_layer(_desktop, _desktop->getMessageStack()) == false) { + return TRUE; + } + + // save drag origin + this->xp = (gint) event->button.x; + this->yp = (gint) event->button.y; + this->within_tolerance = true; + + Geom::Point const button_pt(event->button.x, event->button.y); + Geom::Point button_dt(_desktop->w2d(button_pt)); + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + this->p0 = button_dt; + Inkscape::Rubberband::get(_desktop)->start(_desktop, this->p0); + + grabCanvasEvents(); + + this->creating = true; + + /* Processed */ + return TRUE; + } + break; + case GDK_MOTION_NOTIFY: { + if (this->creating && (event->motion.state & GDK_BUTTON1_MASK)) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->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 draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_pt(event->motion.x, event->motion.y); + Geom::Point p = _desktop->w2d(motion_pt); + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + Inkscape::Rubberband::get(_desktop)->move(p); + gobble_motion_events(GDK_BUTTON1_MASK); + + // status text + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(fabs((p - this->p0)[Geom::X]), "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(fabs((p - this->p0)[Geom::Y]), "px"); + Glib::ustring xs = x_q.string(_desktop->namedview->display_units); + Glib::ustring ys = y_q.string(_desktop->namedview->display_units); + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Flowed text frame</b>: %s × %s"), xs.c_str(), ys.c_str()); + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + if ((event->motion.state & GDK_BUTTON1_MASK) && this->dragging) { + Inkscape::Text::Layout const *layout = te_get_layout(this->text); + if (!layout) + break; + // find out click point in document coordinates + Geom::Point p = _desktop->w2d(Geom::Point(event->button.x, event->button.y)); + // set the cursor closest to that point + Inkscape::Text::Layout::iterator new_end = sp_te_get_position_by_coords(this->text, p); + if (this->dragging == 2) { + // double-click dragging: go by word + if (new_end < this->text_sel_start) { + if (!layout->isStartOfWord(new_end)) + new_end.prevStartOfWord(); + } else if (!layout->isEndOfWord(new_end)) + new_end.nextEndOfWord(); + } else if (this->dragging == 3) { + // triple-click dragging: go by line + if (new_end < this->text_sel_start) + new_end.thisStartOfLine(); + else + new_end.thisEndOfLine(); + } + // update display + if (this->text_sel_end != new_end) { + this->text_sel_end = new_end; + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + } + gobble_motion_events(GDK_BUTTON1_MASK); + break; + } + // find out item under mouse, disregarding groups + SPItem *item_ungrouped = + _desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE, nullptr); + if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) { + Inkscape::Text::Layout const *layout = te_get_layout(item_ungrouped); + if (layout->inputTruncated()) { + indicator->set_stroke(0xff0000ff); + } else { + indicator->set_stroke(0x0000ff7f); + } + Geom::OptRect ibbox = item_ungrouped->desktopVisualBounds(); + if (ibbox) { + indicator->set_rect(*ibbox); + } + indicator->show(); + + this->set_cursor("text-insert.svg"); + sp_text_context_update_text_selection(this); + if (SP_IS_TEXT(item_ungrouped)) { + _desktop->event_context->defaultMessageContext()->set( + Inkscape::NORMAL_MESSAGE, + _("<b>Click</b> to edit the text, <b>drag</b> to select part of the text.")); + } else { + _desktop->event_context->defaultMessageContext()->set( + Inkscape::NORMAL_MESSAGE, + _("<b>Click</b> to edit the flowed text, <b>drag</b> to select part of the text.")); + } + this->over_text = true; + } else { + // update cursor and statusbar: we are not over a text object now + this->set_cursor("text.svg"); + _desktop->event_context->defaultMessageContext()->clear(); + this->over_text = false; + } + } break; + + case GDK_BUTTON_RELEASE: + if (event->button.button == 1) { + this->discard_delayed_snap_event(); + + Geom::Point p1 = _desktop->w2d(Geom::Point(event->button.x, event->button.y)); + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(p1, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + ungrabCanvasEvents(); + + Inkscape::Rubberband::get(_desktop)->stop(); + + if (this->creating && this->within_tolerance) { + /* Button 1, set X & Y & new item */ + _desktop->getSelection()->clear(); + this->pdoc = _desktop->dt2doc(p1); + this->show = TRUE; + this->phase = true; + this->nascent_object = true; // new object was just created + + /* Cursor */ + cursor->show(); + // Cursor height is defined by the new text object's font size; it needs to be set + // artificially here, for the text object does not exist yet: + double cursor_height = sp_desktop_get_font_size_tool(_desktop); + auto const y_dir = _desktop->yaxisdir(); + Geom::Point const cursor_size(0, y_dir * cursor_height); + cursor->set_coords(p1, p1 - cursor_size); + if (this->imc) { + GdkRectangle im_cursor; + Geom::Point const top_left = _desktop->get_display_area().corner(0); + Geom::Point const im_d0 = _desktop->d2w(p1 - top_left); + Geom::Point const im_d1 = _desktop->d2w(p1 - cursor_size - top_left); + Geom::Rect const im_rect(im_d0, im_d1); + im_cursor.x = (int) floor(im_rect.left()); + im_cursor.y = (int) floor(im_rect.top()); + im_cursor.width = (int) floor(im_rect.width()); + im_cursor.height = (int) floor(im_rect.height()); + gtk_im_context_set_cursor_location(this->imc, &im_cursor); + } + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("Type text; <b>Enter</b> to start new line.")); // FIXME:: this is a copy of a string from _update_cursor below, do not desync + + this->within_tolerance = false; + } else if (this->creating) { + double cursor_height = sp_desktop_get_font_size_tool(_desktop); + if (fabs(p1[Geom::Y] - this->p0[Geom::Y]) > cursor_height) { + // otherwise even one line won't fit; most probably a slip of hand (even if bigger than tolerance) + + if (prefs->getBool("/tools/text/use_svg2", true)) { + // SVG 2 text + + SPItem *text = create_text_with_rectangle (_desktop, this->p0, p1); + + _desktop->getSelection()->set(text); + + } else { + // SVG 1.2 text + + SPItem *ft = create_flowtext_with_internal_frame (_desktop, this->p0, p1); + + _desktop->getSelection()->set(ft); + } + + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Flowed text is created.")); + DocumentUndo::done(_desktop->getDocument(), _("Create flowed text"), INKSCAPE_ICON("draw-text")); + + } else { + _desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The frame is <b>too small</b> for the current font size. Flowed text not created.")); + } + } + this->creating = false; + _desktop->emit_text_cursor_moved(this, this); + return TRUE; + } + break; + case GDK_KEY_PRESS: { + guint const group0_keyval = get_latin_keyval(&event->key); + + if (group0_keyval == GDK_KEY_KP_Add || + group0_keyval == GDK_KEY_KP_Subtract) { + if (!(event->key.state & GDK_MOD2_MASK)) // mod2 is NumLock; if on, type +/- keys + break; // otherwise pass on keypad +/- so they can zoom + } + + if ((this->text) || (this->nascent_object)) { + // there is an active text object in this context, or a new object was just created + + // Input methods often use Ctrl+Shift+U for preediting (unimode). + // Override it so we can use our unimode. + bool preedit_activation = (MOD__CTRL(event) && MOD__SHIFT(event) && !MOD__ALT(event)) + && (group0_keyval == GDK_KEY_U || group0_keyval == GDK_KEY_u); + + if (this->unimode || !this->imc || preedit_activation + || !gtk_im_context_filter_keypress(this->imc, (GdkEventKey*) event)) { + // IM did not consume the key, or we're in unimode + + if (!MOD__CTRL_ONLY(event) && this->unimode) { + /* TODO: ISO 14755 (section 3 Definitions) says that we should also + accept the first 6 characters of alphabets other than the latin + alphabet "if the Latin alphabet is not used". The below is also + reasonable (viz. hope that the user's keyboard includes latin + characters and force latin interpretation -- just as we do for our + keyboard shortcuts), but differs from the ISO 14755 + recommendation. */ + switch (group0_keyval) { + case GDK_KEY_space: + case GDK_KEY_KP_Space: { + if (this->unipos) { + insert_uni_char(this); + } + /* Stay in unimode. */ + show_curr_uni_char(this); + return TRUE; + } + + case GDK_KEY_BackSpace: { + g_return_val_if_fail(this->unipos < sizeof(this->uni), TRUE); + if (this->unipos) { + this->uni[--this->unipos] = '\0'; + } + show_curr_uni_char(this); + return TRUE; + } + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + if (this->unipos) { + insert_uni_char(this); + } + /* Exit unimode. */ + this->unimode = false; + this->defaultMessageContext()->clear(); + return TRUE; + } + + case GDK_KEY_Escape: { + // Cancel unimode. + this->unimode = false; + gtk_im_context_reset(this->imc); + this->defaultMessageContext()->clear(); + return TRUE; + } + + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + break; + + default: { + guint32 xdigit = gdk_keyval_to_unicode(group0_keyval); + if (xdigit <= 255 && g_ascii_isxdigit(xdigit)) { + g_return_val_if_fail(this->unipos < sizeof(this->uni) - 1, TRUE); + this->uni[this->unipos++] = xdigit; + this->uni[this->unipos] = '\0'; + if (this->unipos == 8) { + /* This behaviour is partly to allow us to continue to + use a fixed-length buffer for tc->uni. Reason for + choosing the number 8 is that it's the length of + ``canonical form'' mentioned in the ISO 14755 spec. + An advantage over choosing 6 is that it allows using + backspace for typos & misremembering when entering a + 6-digit number. */ + insert_uni_char(this); + } + show_curr_uni_char(this); + return TRUE; + } else { + /* The intent is to ignore but consume characters that could be + typos for hex digits. Gtk seems to ignore & consume all + non-hex-digits, and we do similar here. Though note that some + shortcuts (like keypad +/- for zoom) get processed before + reaching this code. */ + return TRUE; + } + } + } + } + + Inkscape::Text::Layout::iterator old_start = this->text_sel_start; + Inkscape::Text::Layout::iterator old_end = this->text_sel_end; + bool cursor_moved = false; + int screenlines = 1; + if (this->text) { + double spacing = sp_te_get_average_linespacing(this->text); + Geom::Rect const d = _desktop->get_display_area().bounds(); + screenlines = (int) floor(fabs(d.min()[Geom::Y] - d.max()[Geom::Y])/spacing) - 1; + if (screenlines <= 0) + screenlines = 1; + } + + /* Neither unimode nor IM consumed key; process text tool shortcuts */ + switch (group0_keyval) { + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("TextFontFamilyAction_entry"); + return TRUE; + } + break; + case GDK_KEY_space: + if (MOD__CTRL_ONLY(event)) { + /* No-break space */ + if (!this->text) { // printable key; create text if none (i.e. if nascent_object) + sp_text_context_setup_text(this); + this->nascent_object = false; // we don't need it anymore, having created a real <text> + } + this->text_sel_start = this->text_sel_end = sp_te_replace(this->text, this->text_sel_start, this->text_sel_end, "\302\240"); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("No-break space")); + DocumentUndo::done(_desktop->getDocument(), _("Insert no-break space"), INKSCAPE_ICON("draw-text")); + return TRUE; + } + break; + case GDK_KEY_U: + case GDK_KEY_u: + if (MOD__CTRL_ONLY(event) || (MOD__CTRL(event) && MOD__SHIFT(event))) { + if (this->unimode) { + this->unimode = false; + this->defaultMessageContext()->clear(); + } else { + this->unimode = true; + this->unipos = 0; + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): ")); + } + if (this->imc) { + gtk_im_context_reset(this->imc); + } + return TRUE; + } + break; + case GDK_KEY_B: + case GDK_KEY_b: + if (MOD__CTRL_ONLY(event) && this->text) { + SPStyle const *style = sp_te_style_at_position(this->text, std::min(this->text_sel_start, this->text_sel_end)); + SPCSSAttr *css = sp_repr_css_attr_new(); + if (style->font_weight.computed == SP_CSS_FONT_WEIGHT_NORMAL + || style->font_weight.computed == SP_CSS_FONT_WEIGHT_100 + || style->font_weight.computed == SP_CSS_FONT_WEIGHT_200 + || style->font_weight.computed == SP_CSS_FONT_WEIGHT_300 + || style->font_weight.computed == SP_CSS_FONT_WEIGHT_400) + sp_repr_css_set_property(css, "font-weight", "bold"); + else + sp_repr_css_set_property(css, "font-weight", "normal"); + sp_te_apply_style(this->text, this->text_sel_start, this->text_sel_end, css); + sp_repr_css_attr_unref(css); + DocumentUndo::done(_desktop->getDocument(), _("Make bold"), INKSCAPE_ICON("draw-text")); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + break; + case GDK_KEY_I: + case GDK_KEY_i: + if (MOD__CTRL_ONLY(event) && this->text) { + SPStyle const *style = sp_te_style_at_position(this->text, std::min(this->text_sel_start, this->text_sel_end)); + SPCSSAttr *css = sp_repr_css_attr_new(); + if (style->font_style.computed != SP_CSS_FONT_STYLE_NORMAL) + sp_repr_css_set_property(css, "font-style", "normal"); + else + sp_repr_css_set_property(css, "font-style", "italic"); + sp_te_apply_style(this->text, this->text_sel_start, this->text_sel_end, css); + sp_repr_css_attr_unref(css); + DocumentUndo::done(_desktop->getDocument(), _("Make italic"), INKSCAPE_ICON("draw-text")); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + break; + + case GDK_KEY_A: + case GDK_KEY_a: + if (MOD__CTRL_ONLY(event) && this->text) { + Inkscape::Text::Layout const *layout = te_get_layout(this->text); + if (layout) { + this->text_sel_start = layout->begin(); + this->text_sel_end = layout->end(); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + } + break; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + if (!this->text) { // printable key; create text if none (i.e. if nascent_object) + sp_text_context_setup_text(this); + this->nascent_object = false; // we don't need it anymore, having created a real <text> + } + + SPText* text_element = dynamic_cast<SPText*>(text); + if (text_element && (text_element->has_shape_inside() || text_element->has_inline_size())) { + // Handle new line like any other character. + this->text_sel_start = this->text_sel_end = sp_te_insert(this->text, this->text_sel_start, "\n"); + } else { + // Replace new line by either <tspan sodipodi:role="line" or <flowPara>. + iterator_pair enter_pair; + bool success = sp_te_delete(this->text, this->text_sel_start, this->text_sel_end, enter_pair); + (void)success; // TODO cleanup + this->text_sel_start = this->text_sel_end = enter_pair.first; + this->text_sel_start = this->text_sel_end = sp_te_insert_line(this->text, this->text_sel_start); + } + + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::done(_desktop->getDocument(), _("New line"), INKSCAPE_ICON("draw-text")); + return TRUE; + } + case GDK_KEY_BackSpace: + if (this->text) { // if nascent_object, do nothing, but return TRUE; same for all other delete and move keys + + bool noSelection = false; + + if (MOD__CTRL(event)) { + this->text_sel_start = this->text_sel_end; + } + + if (this->text_sel_start == this->text_sel_end) { + if (MOD__CTRL(event)) { + this->text_sel_start.prevStartOfWord(); + } else { + this->text_sel_start.prevCursorPosition(); + } + noSelection = true; + } + + iterator_pair bspace_pair; + bool success = sp_te_delete(this->text, this->text_sel_start, this->text_sel_end, bspace_pair); + + if (noSelection) { + if (success) { + this->text_sel_start = this->text_sel_end = bspace_pair.first; + } else { // nothing deleted + this->text_sel_start = this->text_sel_end = bspace_pair.second; + } + } else { + if (success) { + this->text_sel_start = this->text_sel_end = bspace_pair.first; + } else { // nothing deleted + this->text_sel_start = bspace_pair.first; + this->text_sel_end = bspace_pair.second; + } + } + + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::done(_desktop->getDocument(), _("Backspace"), INKSCAPE_ICON("draw-text")); + } + return TRUE; + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + if (this->text) { + bool noSelection = false; + + if (MOD__CTRL(event)) { + this->text_sel_start = this->text_sel_end; + } + + if (this->text_sel_start == this->text_sel_end) { + if (MOD__CTRL(event)) { + this->text_sel_end.nextEndOfWord(); + } else { + this->text_sel_end.nextCursorPosition(); + } + noSelection = true; + } + + iterator_pair del_pair; + bool success = sp_te_delete(this->text, this->text_sel_start, this->text_sel_end, del_pair); + + if (noSelection) { + this->text_sel_start = this->text_sel_end = del_pair.first; + } else { + if (success) { + this->text_sel_start = this->text_sel_end = del_pair.first; + } else { // nothing deleted + this->text_sel_start = del_pair.first; + this->text_sel_end = del_pair.second; + } + } + + + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::done(_desktop->getDocument(), _("Delete"), INKSCAPE_ICON("draw-text")); + } + return TRUE; + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + if (this->text) { + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events( + get_latin_keyval(&event->key), 0); // with any mask + if (MOD__SHIFT(event)) + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, Geom::Point(mul*-10, 0)); + else + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, Geom::Point(mul*-1, 0)); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::maybeDone(_desktop->getDocument(), "kern:left", _("Kern to the left"), INKSCAPE_ICON("draw-text")); + } else { + if (MOD__CTRL(event)) + this->text_sel_end.cursorLeftWithControl(); + else + this->text_sel_end.cursorLeft(); + cursor_moved = true; + break; + } + } + return TRUE; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + if (this->text) { + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events( + get_latin_keyval(&event->key), 0); // with any mask + if (MOD__SHIFT(event)) + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, Geom::Point(mul*10, 0)); + else + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, Geom::Point(mul*1, 0)); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::maybeDone(_desktop->getDocument(), "kern:right", _("Kern to the right"), INKSCAPE_ICON("draw-text")); + } else { + if (MOD__CTRL(event)) + this->text_sel_end.cursorRightWithControl(); + else + this->text_sel_end.cursorRight(); + cursor_moved = true; + break; + } + } + return TRUE; + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + if (this->text) { + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events( + get_latin_keyval(&event->key), 0); // with any mask + if (MOD__SHIFT(event)) + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, Geom::Point(0, mul*-10)); + else + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, Geom::Point(0, mul*-1)); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::maybeDone(_desktop->getDocument(), "kern:up", _("Kern up"), INKSCAPE_ICON("draw-text")); + } else { + if (MOD__CTRL(event)) + this->text_sel_end.cursorUpWithControl(); + else + this->text_sel_end.cursorUp(); + cursor_moved = true; + break; + } + } + return TRUE; + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + if (this->text) { + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events( + get_latin_keyval(&event->key), 0); // with any mask + if (MOD__SHIFT(event)) + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, Geom::Point(0, mul*10)); + else + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, Geom::Point(0, mul*1)); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::maybeDone(_desktop->getDocument(), "kern:down", _("Kern down"), INKSCAPE_ICON("draw-text")); + } else { + if (MOD__CTRL(event)) + this->text_sel_end.cursorDownWithControl(); + else + this->text_sel_end.cursorDown(); + cursor_moved = true; + break; + } + } + return TRUE; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + if (this->text) { + if (MOD__CTRL(event)) + this->text_sel_end.thisStartOfShape(); + else + this->text_sel_end.thisStartOfLine(); + cursor_moved = true; + break; + } + return TRUE; + case GDK_KEY_End: + case GDK_KEY_KP_End: + if (this->text) { + if (MOD__CTRL(event)) + this->text_sel_end.nextStartOfShape(); + else + this->text_sel_end.thisEndOfLine(); + cursor_moved = true; + break; + } + return TRUE; + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: + if (this->text) { + this->text_sel_end.cursorDown(screenlines); + cursor_moved = true; + break; + } + return TRUE; + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: + if (this->text) { + this->text_sel_end.cursorUp(screenlines); + cursor_moved = true; + break; + } + return TRUE; + case GDK_KEY_Escape: + if (this->creating) { + this->creating = false; + ungrabCanvasEvents(); + Inkscape::Rubberband::get(_desktop)->stop(); + } else { + _desktop->getSelection()->clear(); + } + this->nascent_object = FALSE; + return TRUE; + case GDK_KEY_bracketleft: + if (this->text) { + if (MOD__ALT(event) || MOD__CTRL(event)) { + if (MOD__ALT(event)) { + if (MOD__SHIFT(event)) { + // FIXME: alt+shift+[] does not work, don't know why + sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, -10); + } else { + sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, -1); + } + } else { + sp_te_adjust_rotation(this->text, this->text_sel_start, this->text_sel_end, _desktop, -90); + } + DocumentUndo::maybeDone(_desktop->getDocument(), "textrot:ccw", _("Rotate counterclockwise"), INKSCAPE_ICON("draw-text")); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + } + break; + case GDK_KEY_bracketright: + if (this->text) { + if (MOD__ALT(event) || MOD__CTRL(event)) { + if (MOD__ALT(event)) { + if (MOD__SHIFT(event)) { + // FIXME: alt+shift+[] does not work, don't know why + sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, 10); + } else { + sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, 1); + } + } else { + sp_te_adjust_rotation(this->text, this->text_sel_start, this->text_sel_end, _desktop, 90); + } + DocumentUndo::maybeDone(_desktop->getDocument(), "textrot:cw", _("Rotate clockwise"), INKSCAPE_ICON("draw-text")); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + } + break; + case GDK_KEY_less: + case GDK_KEY_comma: + if (this->text) { + if (MOD__ALT(event)) { + if (MOD__CTRL(event)) { + if (MOD__SHIFT(event)) + sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, -10); + else + sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, -1); + DocumentUndo::maybeDone(_desktop->getDocument(), "linespacing:dec", _("Contract line spacing"), INKSCAPE_ICON("draw-text")); + } else { + if (MOD__SHIFT(event)) + sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, -10); + else + sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, -1); + DocumentUndo::maybeDone(_desktop->getDocument(), "letterspacing:dec", _("Contract letter spacing"), INKSCAPE_ICON("draw-text")); + } + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + } + break; + case GDK_KEY_greater: + case GDK_KEY_period: + if (this->text) { + if (MOD__ALT(event)) { + if (MOD__CTRL(event)) { + if (MOD__SHIFT(event)) + sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, 10); + else + sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, 1); + DocumentUndo::maybeDone(_desktop->getDocument(), "linespacing:inc", _("Expand line spacing"), INKSCAPE_ICON("draw-text")); + } else { + if (MOD__SHIFT(event)) + sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, 10); + else + sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, _desktop, 1); + DocumentUndo::maybeDone(_desktop->getDocument(), "letterspacing:inc", _("Expand letter spacing"), INKSCAPE_ICON("draw-text")); + } + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + } + break; + default: + break; + } + + if (cursor_moved) { + if (!MOD__SHIFT(event)) + this->text_sel_start = this->text_sel_end; + if (old_start != this->text_sel_start || old_end != this->text_sel_end) { + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + } + return TRUE; + } + + } else return TRUE; // return the "I took care of it" value if it was consumed by the IM + } else { // do nothing if there's no object to type in - the key will be sent to parent context, + // except up/down that are swallowed to prevent the zoom field from activation + if ((group0_keyval == GDK_KEY_Up || + group0_keyval == GDK_KEY_Down || + group0_keyval == GDK_KEY_KP_Up || + group0_keyval == GDK_KEY_KP_Down ) + && !MOD__CTRL_ONLY(event)) { + return TRUE; + } else if (group0_keyval == GDK_KEY_Escape) { // cancel rubberband + if (this->creating) { + this->creating = false; + ungrabCanvasEvents(); + Inkscape::Rubberband::get(_desktop)->stop(); + } + } else if ((group0_keyval == GDK_KEY_x || group0_keyval == GDK_KEY_X) && MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("TextFontFamilyAction_entry"); + return TRUE; + } + } + break; + } + + case GDK_KEY_RELEASE: + if (!this->unimode && this->imc && gtk_im_context_filter_keypress(this->imc, (GdkEventKey*) event)) { + return TRUE; + } + break; + default: + break; + } + + // if nobody consumed it so far +// if ((SP_EVENT_CONTEXT_CLASS(sp_text_context_parent_class))->root_handler) { // and there's a handler in parent context, +// return (SP_EVENT_CONTEXT_CLASS(sp_text_context_parent_class))->root_handler(event_context, event); // send event to parent +// } else { +// return FALSE; // return "I did nothing" value so that global shortcuts can be activated +// } + return ToolBase::root_handler(event); + +} + +/** + Attempts to paste system clipboard into the currently edited text, returns true on success + */ +bool sp_text_paste_inline(ToolBase *ec) +{ + if (!SP_IS_TEXT_CONTEXT(ec)) + return false; + TextTool *tc = SP_TEXT_CONTEXT(ec); + + if ((tc->text) || (tc->nascent_object)) { + // there is an active text object in this context, or a new object was just created + + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + Glib::ustring const clip_text = refClipboard->wait_for_text(); + + if (!clip_text.empty()) { + + bool is_svg2 = false; + SPText *textitem = dynamic_cast<SPText *>(tc->text); + if (textitem) { + is_svg2 = textitem->has_shape_inside() /*|| textitem->has_inline_size()*/; // Do now since hiding messes this up. + textitem->hide_shape_inside(); + } + + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(tc->text); + if (flowtext) { + flowtext->fix_overflow_flowregion(false); + } + + // Fix for 244940 + // The XML standard defines the following as valid characters + // (Extensible Markup Language (XML) 1.0 (Fourth Edition) paragraph 2.2) + // char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + // Since what comes in off the paste buffer will go right into XML, clean + // the text here. + Glib::ustring text(clip_text); + Glib::ustring::iterator itr = text.begin(); + gunichar paste_string_uchar; + + while(itr != text.end()) + { + paste_string_uchar = *itr; + + // Make sure we don't have a control character. We should really check + // for the whole range above... Add the rest of the invalid cases from + // above if we find additional issues + if(paste_string_uchar >= 0x00000020 || + paste_string_uchar == 0x00000009 || + paste_string_uchar == 0x0000000A || + paste_string_uchar == 0x0000000D) { + ++itr; + } else { + itr = text.erase(itr); + } + } + + if (!tc->text) { // create text if none (i.e. if nascent_object) + sp_text_context_setup_text(tc); + tc->nascent_object = false; // we don't need it anymore, having created a real <text> + } + + // using indices is slow in ustrings. Whatever. + Glib::ustring::size_type begin = 0; + for ( ; ; ) { + Glib::ustring::size_type end = text.find('\n', begin); + + if (end == Glib::ustring::npos || is_svg2) { + // Paste everything + if (begin != text.length()) + tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, text.substr(begin).c_str()); + break; + } + + // Paste up to new line, add line, repeat. + tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, text.substr(begin, end - begin).c_str()); + tc->text_sel_start = tc->text_sel_end = sp_te_insert_line(tc->text, tc->text_sel_start); + begin = end + 1; + } + if (textitem) { + textitem->show_shape_inside(); + } + if (flowtext) { + flowtext->fix_overflow_flowregion(true); + } + DocumentUndo::done(ec->getDesktop()->getDocument(), _("Paste text"), INKSCAPE_ICON("draw-text")); + + return true; + } + + } // FIXME: else create and select a new object under cursor! + + return false; +} + +/** + Gets the raw characters that comprise the currently selected text, converting line + breaks into lf characters. +*/ +Glib::ustring sp_text_get_selected_text(ToolBase const *ec) +{ + if (!SP_IS_TEXT_CONTEXT(ec)) + return ""; + TextTool const *tc = SP_TEXT_CONTEXT(ec); + if (tc->text == nullptr) + return ""; + + return sp_te_get_string_multiline(tc->text, tc->text_sel_start, tc->text_sel_end); +} + +SPCSSAttr *sp_text_get_style_at_cursor(ToolBase const *ec) +{ + if (!SP_IS_TEXT_CONTEXT(ec)) + return nullptr; + TextTool const *tc = SP_TEXT_CONTEXT(ec); + if (tc->text == nullptr) + return nullptr; + + SPObject const *obj = sp_te_object_at_position(tc->text, tc->text_sel_end); + + if (obj) { + return take_style_from_item(const_cast<SPObject*>(obj)); + } + + return nullptr; +} +// this two functions are commented because are used on clipboard +// and because slow the text pastinbg and usage a lot +// and couldn't get it working properly we miss font size font style or never work +// and user usually want paste as plain text and get the position context +// style. Anyway I retain for further usage. + +/* static bool css_attrs_are_equal(SPCSSAttr const *first, SPCSSAttr const *second) +{ +// Inkscape::Util::List<Inkscape::XML::AttributeRecord const> attrs = first->attributeList(); + for ( ; attrs ; attrs++) { + gchar const *other_attr = second->attribute(g_quark_to_string(attrs->key)); + if (other_attr == nullptr || strcmp(attrs->value, other_attr)) + return false; + } + attrs = second->attributeList(); + for ( ; attrs ; attrs++) { + gchar const *other_attr = first->attribute(g_quark_to_string(attrs->key)); + if (other_attr == nullptr || strcmp(attrs->value, other_attr)) + return false; + } + return true; +} + +std::vector<SPCSSAttr*> sp_text_get_selected_style(ToolBase const *ec, unsigned *k, int *b, std::vector<unsigned> +*positions) +{ + std::vector<SPCSSAttr*> vec; + SPCSSAttr *css, *css_new; + TextTool *tc = SP_TEXT_CONTEXT(ec); + Inkscape::Text::Layout::iterator i = std::min(tc->text_sel_start, tc->text_sel_end); + SPObject const *obj = sp_te_object_at_position(tc->text, i); + if (obj) { + css = take_style_from_item(const_cast<SPObject*>(obj)); + } + vec.push_back(css); + positions->push_back(0); + i.nextCharacter(); + *k = 1; + *b = 1; + while (i != std::max(tc->text_sel_start, tc->text_sel_end)) + { + obj = sp_te_object_at_position(tc->text, i); + if (obj) { + css_new = take_style_from_item(const_cast<SPObject*>(obj)); + } + if(!css_attrs_are_equal(css, css_new)) + { + vec.push_back(css_new); + css = sp_repr_css_attr_new(); + sp_repr_css_merge(css, css_new); + positions->push_back(*k); + (*b)++; + } + i.nextCharacter(); + (*k)++; + } + positions->push_back(*k); + return vec; +} + */ + +/** + Deletes the currently selected characters. Returns false if there is no + text selection currently. +*/ +bool sp_text_delete_selection(ToolBase *ec) +{ + if (!SP_IS_TEXT_CONTEXT(ec)) + return false; + TextTool *tc = SP_TEXT_CONTEXT(ec); + if (tc->text == nullptr) + return false; + + if (tc->text_sel_start == tc->text_sel_end) + return false; + + iterator_pair pair; + bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, pair); + + + if (success) { + tc->text_sel_start = tc->text_sel_end = pair.first; + } else { // nothing deleted + tc->text_sel_start = pair.first; + tc->text_sel_end = pair.second; + } + + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); + + return true; +} + +/** + * \param selection Should not be NULL. + */ +void TextTool::_selectionChanged(Inkscape::Selection *selection) +{ + g_assert(selection != nullptr); + SPItem *item = selection->singleItem(); + + if (this->text && (item != this->text)) { + sp_text_context_forget_text(this); + } + this->text = nullptr; + + shape_editor->unset_item(); + if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + shape_editor->set_item(item); + + this->text = item; + Inkscape::Text::Layout const *layout = te_get_layout(this->text); + if (layout) + this->text_sel_start = this->text_sel_end = layout->end(); + } else { + this->text = nullptr; + } + + // we update cursor without scrolling, because this position may not be final; + // item_handler moves cusros to the point of click immediately + sp_text_context_update_cursor(this, false); + sp_text_context_update_text_selection(this); +} + +void TextTool::_selectionModified(Inkscape::Selection */*selection*/, guint /*flags*/) +{ + bool scroll = !this->shape_editor->has_knotholder() || + !this->shape_editor->knotholder->is_dragging(); + sp_text_context_update_cursor(this, scroll); + sp_text_context_update_text_selection(this); +} + +bool TextTool::_styleSet(SPCSSAttr const *css) +{ + if (this->text == nullptr) + return false; + if (this->text_sel_start == this->text_sel_end) + return false; // will get picked up by the parent and applied to the whole text object + + sp_te_apply_style(this->text, this->text_sel_start, this->text_sel_end, css); + + // This is a bandaid fix... whenever a style is changed it might cause the text layout to + // change which requires rewriting the 'x' and 'y' attributes of the tpsans for Inkscape + // multi-line text (with sodipodi:role="line"). We need to rewrite the repr after this is + // done. rebuldLayout() will be called a second time unnecessarily. + SPText* sptext = dynamic_cast<SPText*>(text); + if (sptext) { + sptext->rebuildLayout(); + sptext->updateRepr(); + } + + DocumentUndo::done(_desktop->getDocument(), _("Set text style"), INKSCAPE_ICON("draw-text")); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return true; +} + +int TextTool::_styleQueried(SPStyle *style, int property) +{ + if (this->text == nullptr) { + return QUERY_STYLE_NOTHING; + } + const Inkscape::Text::Layout *layout = te_get_layout(this->text); + if (layout == nullptr) { + return QUERY_STYLE_NOTHING; + } + sp_text_context_validate_cursor_iterators(this); + + std::vector<SPItem*> styles_list; + + Inkscape::Text::Layout::iterator begin_it, end_it; + if (this->text_sel_start < this->text_sel_end) { + begin_it = this->text_sel_start; + end_it = this->text_sel_end; + } else { + begin_it = this->text_sel_end; + end_it = this->text_sel_start; + } + if (begin_it == end_it) { + if (!begin_it.prevCharacter()) { + end_it.nextCharacter(); + } + } + for (Inkscape::Text::Layout::iterator it = begin_it ; it < end_it ; it.nextStartOfSpan()) { + SPObject *pos_obj = nullptr; + layout->getSourceOfCharacter(it, &pos_obj); + if (!pos_obj) { + continue; + } + if (! pos_obj->parent) // the string is not in the document anymore (deleted) + return 0; + + if ( SP_IS_STRING(pos_obj) ) { + pos_obj = pos_obj->parent; // SPStrings don't have style + } + styles_list.insert(styles_list.begin(),(SPItem*)pos_obj); + } + + int result = sp_desktop_query_style_from_list (styles_list, style, property); + + return result; +} + +static void sp_text_context_validate_cursor_iterators(TextTool *tc) +{ + if (tc->text == nullptr) + return; + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + if (layout) { // undo can change the text length without us knowing it + layout->validateIterator(&tc->text_sel_start); + layout->validateIterator(&tc->text_sel_end); + } +} + +static void sp_text_context_update_cursor(TextTool *tc, bool scroll_to_see) +{ + // due to interruptible display, tc may already be destroyed during a display update before + // the cursor update (can't do both atomically, alas) + if (!tc->getDesktop()) return; + auto desktop = tc->getDesktop(); + + if (tc->text) { + Geom::Point p0, p1; + sp_te_get_cursor_coords(tc->text, tc->text_sel_end, p0, p1); + Geom::Point const d0 = p0 * tc->text->i2dt_affine(); + Geom::Point const d1 = p1 * tc->text->i2dt_affine(); + + // scroll to show cursor + if (scroll_to_see) { + + // We don't want to scroll outside the text box area (i.e. when there is hidden text) + // or we could end up in Timbuktu. + bool scroll = true; + if (SP_IS_TEXT(tc->text)) { + Geom::OptRect opt_frame = SP_TEXT(tc->text)->get_frame(); + if (opt_frame && (!opt_frame->contains(p0))) { + scroll = false; + } + } else if (SP_IS_FLOWTEXT(tc->text)) { + SPItem *frame = SP_FLOWTEXT(tc->text)->get_frame(nullptr); // first frame only + Geom::OptRect opt_frame = frame->geometricBounds(); + if (opt_frame && (!opt_frame->contains(p0))) { + scroll = false; + } + } + + if (scroll) { + Geom::Point const center = desktop->current_center(); + if (Geom::L2(d0 - center) > Geom::L2(d1 - center)) + // unlike mouse moves, here we must scroll all the way at first shot, so we override the autoscrollspeed + desktop->scroll_to_point(d0, 1.0); + else + desktop->scroll_to_point(d1, 1.0); + } + } + + tc->cursor->set_coords(d0, d1); + tc->cursor->show(); + + /* fixme: ... need another transformation to get canvas widget coordinate space? */ + if (tc->imc) { + GdkRectangle im_cursor = { 0, 0, 1, 1 }; + Geom::Point const top_left = desktop->get_display_area().corner(0); + Geom::Point const im_d0 = desktop->d2w(d0 - top_left); + Geom::Point const im_d1 = desktop->d2w(d1 - top_left); + Geom::Rect const im_rect(im_d0, im_d1); + im_cursor.x = (int) floor(im_rect.left()); + im_cursor.y = (int) floor(im_rect.top()); + im_cursor.width = (int) floor(im_rect.width()); + im_cursor.height = (int) floor(im_rect.height()); + gtk_im_context_set_cursor_location(tc->imc, &im_cursor); + } + + tc->show = TRUE; + tc->phase = true; + + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + int const nChars = layout->iteratorToCharIndex(layout->end()); + char const *edit_message = ngettext("Type or edit text (%d character%s); <b>Enter</b> to start new line.", "Type or edit text (%d characters%s); <b>Enter</b> to start new line.", nChars); + char const *edit_message_flowed = ngettext("Type or edit flowed text (%d character%s); <b>Enter</b> to start new paragraph.", "Type or edit flowed text (%d characters%s); <b>Enter</b> to start new paragraph.", nChars); + bool truncated = layout->inputTruncated(); + char const *trunc = truncated ? _(" [truncated]") : ""; + + if (truncated) { + tc->frame->set_stroke(0xff0000ff); + } else { + tc->frame->set_stroke(0x0000ff7f); + } + + std::vector<SPItem const *> shapes; + Shape *exclusion_shape = nullptr; + double padding = 0.0; + + // Frame around text + if (SP_IS_FLOWTEXT(tc->text)) { + SPItem *frame = SP_FLOWTEXT(tc->text)->get_frame (nullptr); // first frame only + shapes.push_back(frame); + + tc->message_context->setF(Inkscape::NORMAL_MESSAGE, edit_message_flowed, nChars, trunc); + + } else if (auto text = dynamic_cast<SPText *>(tc->text)) { + if (text->style->shape_inside.set) { + for (auto const *href : text->style->shape_inside.hrefs) { + shapes.push_back(href->getObject()); + } + if (text->style->shape_padding.set) { + // Calculate it here so we never show padding on FlowText or non-flowed Text (even if set) + padding = text->style->shape_padding.computed; + } + if(text->style->shape_subtract.set) { + // Find union of all exclusion shapes for later use + exclusion_shape = text->getExclusionShape(); + } + tc->message_context->setF(Inkscape::NORMAL_MESSAGE, edit_message_flowed, nChars, trunc); + } else { + for (SPObject &child : tc->text->children) { + if (auto textpath = dynamic_cast<SPTextPath *>(&child)) { + shapes.push_back(sp_textpath_get_path_item(textpath)); + } + } + tc->message_context->setF(Inkscape::NORMAL_MESSAGE, edit_message, nChars, trunc); + } + } + + SPCurve curve; + for (auto const *shape_item : shapes) { + if (auto shape = dynamic_cast<SPShape const *>(shape_item)) { + auto c = SPCurve::copy(shape->curve()); + if (c) { + c->transform(shape->transform); + curve.append(*c); + } + } + } + + if (!curve.is_empty()) { + bool has_padding = std::fabs(padding) > 1e-12; + bool has_exlusions = exclusion_shape; + + if (has_padding || has_exlusions) { + // Should only occur for SVG2 autoflowed text + // See sp-text.cpp function _buildLayoutInit() + Path *temp = new Path; + temp->LoadPathVector(curve.get_pathvector()); + + // Get initial shape-inside curve + Shape *uncross = new Shape; + { + Shape *sh = new Shape; + temp->ConvertWithBackData(0.25); // Convert to polyline + temp->Fill(sh, 0); + uncross->ConvertToShape(sh); + delete sh; + } + + // Get padded shape exclusion + if (has_padding) { + Shape *pad_shape = new Shape; + Path *padded = new Path; + Path *padt = new Path; + Shape *sh = new Shape; + padt->LoadPathVector(curve.get_pathvector()); + padt->Outline(padded, padding, join_round, butt_straight, 20.0); + padded->ConvertWithBackData(1.0); // Convert to polyline + padded->Fill(sh, 0); + pad_shape->ConvertToShape(sh); + delete sh; + delete padt; + delete padded; + + Shape *copy = new Shape; + copy->Booleen(uncross, pad_shape, (padding > 0.0) ? bool_op_diff : bool_op_union); + delete uncross; + delete pad_shape; + uncross = copy; + } + + // Remove exclusions plus margins from padding frame + if (exclusion_shape && exclusion_shape->hasEdges()) { + Shape *copy = new Shape; + copy->Booleen(uncross, const_cast<Shape*>(exclusion_shape), bool_op_diff); + delete uncross; + uncross = copy; + } + + uncross->ConvertToForme(temp); + tc->padding_frame->set_bpath(temp->MakePathVector() * tc->text->i2dt_affine()); + tc->padding_frame->show(); + + delete temp; + delete uncross; + } else { + tc->padding_frame->hide(); + } + + // Transform curve after doing padding. + curve.transform(tc->text->i2dt_affine()); + tc->frame->set_bpath(&curve); + tc->frame->show(); + } else { + tc->frame->hide(); + tc->padding_frame->hide(); + } + + } else { + tc->cursor->hide(); + tc->frame->hide(); + tc->show = FALSE; + if (!tc->nascent_object) { + tc->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> to select or create text, <b>drag</b> to create flowed text; then type.")); // FIXME: this is a copy of string from tools-switch, do not desync + } + } + + desktop->emit_text_cursor_moved(tc, tc); +} + +static void sp_text_context_update_text_selection(TextTool *tc) +{ + // due to interruptible display, tc may already be destroyed during a display update before + // the selection update (can't do both atomically, alas) + if (!tc->getDesktop()) return; + + for (auto & text_selection_quad : tc->text_selection_quads) { + text_selection_quad->hide(); + delete text_selection_quad; + } + tc->text_selection_quads.clear(); + + std::vector<Geom::Point> quads; + if (tc->text != nullptr) + quads = sp_te_create_selection_quads(tc->text, tc->text_sel_start, tc->text_sel_end, (tc->text)->i2dt_affine()); + for (unsigned i = 0 ; i < quads.size() ; i += 4) { + auto quad = new CanvasItemQuad(tc->getDesktop()->getCanvasControls(), quads[i], quads[i+1], quads[i+2], quads[i+3]); + quad->set_fill(0x00777777); // Semi-transparent blue as Cairo cannot do inversion. + quad->show(); + tc->text_selection_quads.push_back(quad); + } + + if (tc->shape_editor != nullptr) { + if (tc->shape_editor->knotholder) { + tc->shape_editor->knotholder->update_knots(); + } + } +} + +static gint sp_text_context_timeout(TextTool *tc) +{ + if (tc->show) { + if (tc->phase) { + tc->phase = false; + tc->cursor->set_stroke(0x000000ff); + } else { + tc->phase = true; + tc->cursor->set_stroke(0xffffffff); + } + tc->cursor->show(); + } + + return TRUE; +} + +static void sp_text_context_forget_text(TextTool *tc) +{ + if (! tc->text) return; + SPItem *ti = tc->text; + (void)ti; + /* We have to set it to zero, + * or selection changed signal messes everything up */ + tc->text = nullptr; + +/* FIXME: this automatic deletion when nothing is inputted crashes the XML editor and also crashes when duplicating an empty flowtext. + So don't create an empty flowtext in the first place? Create it when first character is typed. + */ +/* + if ((SP_IS_TEXT(ti) || SP_IS_FLOWTEXT(ti)) && sp_te_input_is_empty(ti)) { + Inkscape::XML::Node *text_repr = ti->getRepr(); + // the repr may already have been unparented + // if we were called e.g. as the result of + // an undo or the element being removed from + // the XML editor + if ( text_repr && text_repr->parent() ) { + sp_repr_unparent(text_repr); + SPDocumentUndo::done(tc->desktop->getDocument(), _("Remove empty text"), INKSCAPE_ICON("draw-text")); + } + } +*/ +} + +gint sptc_focus_in(GtkWidget *widget, GdkEventFocus */*event*/, TextTool *tc) +{ + gtk_im_context_focus_in(tc->imc); + return FALSE; +} + +gint sptc_focus_out(GtkWidget */*widget*/, GdkEventFocus */*event*/, TextTool *tc) +{ + gtk_im_context_focus_out(tc->imc); + return FALSE; +} + +static void sptc_commit(GtkIMContext */*imc*/, gchar *string, TextTool *tc) +{ + if (!tc->text) { + sp_text_context_setup_text(tc); + tc->nascent_object = false; // we don't need it anymore, having created a real <text> + } + + tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, string); + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); + + DocumentUndo::done(tc->text->document, _("Type text"), INKSCAPE_ICON("draw-text")); +} + +void sp_text_context_place_cursor (TextTool *tc, SPObject *text, Inkscape::Text::Layout::iterator where) +{ + tc->getDesktop()->selection->set (text); + tc->text_sel_start = tc->text_sel_end = where; + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); +} + +void sp_text_context_place_cursor_at (TextTool *tc, SPObject *text, Geom::Point const p) +{ + tc->getDesktop()->selection->set (text); + sp_text_context_place_cursor (tc, text, sp_te_get_position_by_coords(tc->text, p)); +} + +Inkscape::Text::Layout::iterator *sp_text_context_get_cursor_position(TextTool *tc, SPObject *text) +{ + if (text != tc->text) + return nullptr; + return &(tc->text_sel_end); +} + +} +} +} + +/* + 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/tools/text-tool.h b/src/ui/tools/text-tool.h new file mode 100644 index 0000000..12a4cee --- /dev/null +++ b/src/ui/tools/text-tool.h @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_TEXT_CONTEXT_H__ +#define __SP_TEXT_CONTEXT_H__ + +/* + * TextTool + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" +#include <2geom/point.h> +#include "libnrtype/Layout-TNG.h" + +#define SP_TEXT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::TextTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_TEXT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::TextTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +typedef struct _GtkIMContext GtkIMContext; + +namespace Inkscape { + +class CanvasItemCurve; // Cursor +class CanvasItemQuad; // Highlighted text +class CanvasItemRect; // Indicator, Frame +class CanvasItemBpath; +class Selection; + +namespace UI { +namespace Tools { + +class TextTool : public ToolBase { +public: + TextTool(SPDesktop *desktop); + ~TextTool() override; + + sigc::connection sel_changed_connection; + sigc::connection sel_modified_connection; + sigc::connection style_set_connection; + sigc::connection style_query_connection; + + GtkIMContext *imc = nullptr; + + SPItem *text = nullptr; // the text we're editing, or NULL if none selected + + /* Text item position in root coordinates */ + Geom::Point pdoc; + /* Insertion point position */ + Inkscape::Text::Layout::iterator text_sel_start; + Inkscape::Text::Layout::iterator text_sel_end; + + gchar uni[9]; + bool unimode = false; + guint unipos = 0; + + // ---- On canvas editing --- + Inkscape::CanvasItemCurve *cursor = nullptr; + Inkscape::CanvasItemRect *indicator = nullptr; + Inkscape::CanvasItemBpath *frame = nullptr; // Highlighting flowtext shapes or textpath path + Inkscape::CanvasItemBpath *padding_frame = nullptr; // Highlighting flowtext padding + std::vector<CanvasItemQuad*> text_selection_quads; + + gint timeout = 0; + bool show = false; + bool phase = false; + bool nascent_object = false; // true if we're clicked on canvas to put cursor, + // but no text typed yet so ->text is still NULL + + bool over_text = false; // true if cursor is over a text object + + guint dragging = 0; // dragging selection over text + bool creating = false; // dragging rubberband to create flowtext + Geom::Point p0; // initial point if the flowtext rect + + /* Preedit String */ + gchar* preedit_string = nullptr; + + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + void deleteSelected(); +private: + void _selectionChanged(Inkscape::Selection *selection); + void _selectionModified(Inkscape::Selection *selection, guint flags); + bool _styleSet(SPCSSAttr const *css); + int _styleQueried(SPStyle *style, int property); +}; + +bool sp_text_paste_inline(ToolBase *ec); +Glib::ustring sp_text_get_selected_text(ToolBase const *ec); +SPCSSAttr *sp_text_get_style_at_cursor(ToolBase const *ec); +// std::vector<SPCSSAttr*> sp_text_get_selected_style(ToolBase const *ec, unsigned *k, int *b, std::vector<unsigned> +// *positions); +bool sp_text_delete_selection(ToolBase *ec); +void sp_text_context_place_cursor (TextTool *tc, SPObject *text, Inkscape::Text::Layout::iterator where); +void sp_text_context_place_cursor_at (TextTool *tc, SPObject *text, Geom::Point const p); +Inkscape::Text::Layout::iterator *sp_text_context_get_cursor_position(TextTool *tc, SPObject *text); + +} +} +} + +#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/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp new file mode 100644 index 0000000..30e3651 --- /dev/null +++ b/src/ui/tools/tool-base.cpp @@ -0,0 +1,1649 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Main event handling, and related helper functions. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 1999-2012 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdkkeysyms.h> +#include <gdkmm/display.h> +#include <glibmm/i18n.h> + +#include <set> + +#include "desktop-events.h" +#include "desktop-style.h" +#include "desktop.h" +#include "file.h" +#include "gradient-drag.h" +#include "layer-manager.h" +#include "message-context.h" +#include "rubberband.h" +#include "selcue.h" +#include "selection-chemistry.h" +#include "selection.h" + +#include "actions/actions-tools.h" + +#include "display/control/canvas-item-catchall.h" // Grab/Ungrab +#include "display/control/canvas-item-rotate.h" +#include "display/control/snap-indicator.h" + +#include "include/gtkmm_version.h" +#include "include/macros.h" + +#include "object/sp-guide.h" + +#include "ui/contextmenu.h" +#include "ui/cursor-utils.h" +#include "ui/event-debug.h" +#include "ui/interface.h" +#include "ui/knot/knot.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-ptr.h" +#include "ui/modifiers.h" +#include "ui/shape-editor.h" +#include "ui/shortcuts.h" + +#include "ui/tool/commit-events.h" +#include "ui/tool/control-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/shape-record.h" +#include "ui/tools/calligraphic-tool.h" +#include "ui/tools/dropper-tool.h" +#include "ui/tools/lpe-tool.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/select-tool.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/canvas.h" + +#include "widgets/desktop-widget.h" + +#include "xml/node-event-vector.h" + +// globals for temporary switching to selector by space +static bool selector_toggled = FALSE; +static Glib::ustring switch_selector_to; + +// globals for temporary switching to dropper by 'D' +static bool dropper_toggled = FALSE; +static Glib::ustring switch_dropper_to; + +// globals for keeping track of keyboard scroll events in order to accelerate +static guint32 scroll_event_time = 0; +static gdouble scroll_multiply = 1; +static guint scroll_keyval = 0; + +// globals for key processing +static bool latin_keys_group_valid = FALSE; +static gint latin_keys_group; +static std::set<int> latin_keys_groups; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void set_event_location(SPDesktop * desktop, GdkEvent * event); + +ToolBase::ToolBase(SPDesktop *desktop, std::string prefs_path, std::string cursor_filename, bool uses_snap) + : _desktop(desktop) + , _prefs_path(std::move(prefs_path)) + , _cursor_default(std::move(cursor_filename)) + , _cursor_filename("none") + , _uses_snap(uses_snap) +{ + pref_observer = new ToolPrefObserver(_prefs_path, this); + Inkscape::Preferences::get()->addObserver(*(this->pref_observer)); + this->set_cursor(_cursor_default); + _desktop->getCanvas()->grab_focus(); + + message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(desktop->messageStack())); + + // Make sure no delayed snapping events are carried over after switching tools + // (this is only an additional safety measure against sloppy coding, because each + // tool should take care of this by itself) + discard_delayed_snap_event(); +} + +ToolBase::~ToolBase() { + this->enableSelectionCue(false); + + if (this->pref_observer) { + delete this->pref_observer; + } + + if (this->_delayed_snap_event) { + delete this->_delayed_snap_event; + } +} + +/** + * Called by our pref_observer if a preference has been changed. + */ +void ToolBase::set(const Inkscape::Preferences::Entry& /*val*/) { +} + + +SPGroup *ToolBase::currentLayer() const +{ + return _desktop->layerManager().currentLayer(); +} + +/** + * Sets the current cursor to the given filename. Does not readload if not changed. + */ +void ToolBase::set_cursor(std::string filename) +{ + if (filename != _cursor_filename) { + _cursor_filename = filename; + use_tool_cursor(); + } +} + +/** + * Returns the Gdk Cursor for the given filename + * + * WARNING: currently this changes the window cursor, see load_svg_cursor + */ +Glib::RefPtr<Gdk::Cursor> ToolBase::get_cursor(Glib::RefPtr<Gdk::Window> window, std::string filename) +{ + bool fillHasColor = false; + bool strokeHasColor = false; + guint32 fillColor = sp_desktop_get_color_tool(_desktop, getPrefsPath(), true, &fillHasColor); + guint32 strokeColor = sp_desktop_get_color_tool(_desktop, getPrefsPath(), false, &strokeHasColor); + double fillOpacity = fillHasColor ? sp_desktop_get_opacity_tool(_desktop, getPrefsPath(), true) : 1.0; + double strokeOpacity = strokeHasColor ? sp_desktop_get_opacity_tool(_desktop, getPrefsPath(), false) : 1.0; + + return load_svg_cursor(window->get_display(), window, filename, + fillColor, strokeColor, fillOpacity, strokeOpacity); +} + +/** + * Uses the saved cursor, based on the saved filename. + */ +void ToolBase::use_tool_cursor() { + if (auto window = _desktop->getCanvas()->get_window()) { + _cursor = get_cursor(window, _cursor_filename); + window->set_cursor(_cursor); + } + _desktop->waiting_cursor = false; +} + +/** + * Set the cursor to this specific one, don't remember it. + * + * If RefPtr is empty, sets the remembered cursor (reverting it) + */ +void ToolBase::use_cursor(Glib::RefPtr<Gdk::Cursor> cursor) +{ + if (auto window = _desktop->getCanvas()->get_window()) { + window->set_cursor(cursor ? cursor : _cursor); + } +} + +/** + * Toggles current tool between active tool and selector tool. + * Subroutine of sp_event_context_private_root_handler(). + */ +static void sp_toggle_selector(SPDesktop *dt) { + + if (!dt->event_context) { + return; + } + + if (dynamic_cast<Inkscape::UI::Tools::SelectTool *>(dt->event_context)) { + if (selector_toggled) { + set_active_tool(dt, switch_selector_to); + selector_toggled = false; + } + } else { + selector_toggled = TRUE; + switch_selector_to = get_active_tool(dt); + set_active_tool(dt, "Select"); + } +} + +/** + * Toggles current tool between active tool and dropper tool. + * Subroutine of sp_event_context_private_root_handler(). + */ +void sp_toggle_dropper(SPDesktop *dt) { + + if (!dt->event_context) { + return; + } + + if (dynamic_cast<Inkscape::UI::Tools::DropperTool *>(dt->event_context)) { + if (dropper_toggled) { + set_active_tool(dt, switch_dropper_to); + dropper_toggled = FALSE; + } + } else { + dropper_toggled = TRUE; + switch_dropper_to = get_active_tool(dt); + set_active_tool(dt, "Dropper"); + } +} + +/** + * Calculates and keeps track of scroll acceleration. + * Subroutine of sp_event_context_private_root_handler(). + */ +static gdouble accelerate_scroll(GdkEvent *event, gdouble acceleration) +{ + guint32 time_diff = ((GdkEventKey *) event)->time - scroll_event_time; + + /* key pressed within 500ms ? (1/2 second) */ + if (time_diff > 500 || event->key.keyval != scroll_keyval) { + scroll_multiply = 1; // abort acceleration + } else { + scroll_multiply += acceleration; // continue acceleration + } + + scroll_event_time = ((GdkEventKey *) event)->time; + scroll_keyval = event->key.keyval; + + return scroll_multiply; +} + +/** Moves the selected points along the supplied unit vector according to + * the modifier state of the supplied event. */ +bool ToolBase::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir) +{ + if (held_control(event)) return false; + unsigned num = 1 + 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; + } + + bool moved = false; + if (shape_editor && shape_editor->has_knotholder()) { + KnotHolder * knotholder = shape_editor->knotholder; + if (knotholder && knotholder->knot_selected()) { + knotholder->transform_selected(Geom::Translate(delta)); + moved = true; + } + } else { + auto nt = dynamic_cast<Inkscape::UI::Tools::NodeTool *>(_desktop->event_context); + if (nt) { + for (auto &_shape_editor : nt->_shape_editors) { + ShapeEditor *shape_editor = _shape_editor.second.get(); + if (shape_editor && shape_editor->has_knotholder()) { + KnotHolder * knotholder = shape_editor->knotholder; + if (knotholder && knotholder->knot_selected()) { + knotholder->transform_selected(Geom::Translate(delta)); + moved = true; + } + } + } + } + } + + return moved; +} + + +bool ToolBase::root_handler(GdkEvent* event) { + +#ifdef EVENT_DUMP + ui_dump_event (event, "ToolBase::root_handler"); +#endif + + static Geom::Point button_w; + static unsigned int panning_cursor = 0; + static unsigned int zoom_rb = 0; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /// @todo REmove redundant /value in preference keys + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + bool allow_panning = prefs->getBool("/options/spacebarpans/value"); + gint ret = FALSE; + + switch (event->type) { + case GDK_2BUTTON_PRESS: + if (panning) { + panning = PANNING_NONE; + ungrabCanvasEvents(); + ret = TRUE; + } else { + /* sp_desktop_dialog(); */ + } + break; + + case GDK_BUTTON_PRESS: + // save drag origin + xp = (gint) event->button.x; + yp = (gint) event->button.y; + within_tolerance = true; + + button_w = Geom::Point(event->button.x, event->button.y); + + switch (event->button.button) { + case 1: + // TODO Does this make sense? Panning starts on passive mouse motion while space + // bar is pressed, it's not necessary to press the mouse button. + if (this->is_space_panning()) { + // When starting panning, make sure there are no snap events pending because these might disable the panning again + if (_uses_snap) { + this->discard_delayed_snap_event(); + } + panning = PANNING_SPACE_BUTTON1; + + grabCanvasEvents(Gdk::KEY_RELEASE_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK ); + + ret = TRUE; + } + break; + + case 2: + if ((event->button.state & GDK_CONTROL_MASK) && !_desktop->get_rotation_lock()) { + // On screen canvas rotation preview + + // Grab background before doing anything else + _desktop->getCanvasRotate()->start(_desktop); + _desktop->getCanvasRotate()->show(); + + // CanvasItemRotate code takes over! + ungrabCanvasEvents(); + + _desktop->getCanvasRotate()->grab(Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK, + nullptr); // Cursor is null. + + } else if (event->button.state & GDK_SHIFT_MASK) { + zoom_rb = 2; + } else { + // When starting panning, make sure there are no snap events pending because these might disable the panning again + if (_uses_snap) { + this->discard_delayed_snap_event(); + } + panning = PANNING_BUTTON2; + + grabCanvasEvents(Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK ); + } + + ret = TRUE; + break; + + case 3: + if (event->button.state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) { + // When starting panning, make sure there are no snap events pending because these might disable the panning again + if (_uses_snap) { + this->discard_delayed_snap_event(); + } + panning = PANNING_BUTTON3; + + grabCanvasEvents(Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK ); + ret = TRUE; + } else if (!are_buttons_1_and_3_on(event)) { + this->menu_popup(event); + ret = TRUE; + } + break; + + default: + break; + } + break; + + case GDK_MOTION_NOTIFY: + if (panning) { + if (panning == 4 && !xp && !yp ) { + // <Space> + mouse panning started, save location and grab canvas + xp = event->motion.x; + yp = event->motion.y; + button_w = Geom::Point(event->motion.x, event->motion.y); + + grabCanvasEvents(Gdk::KEY_RELEASE_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK ); + } + + if ((panning == 2 && !(event->motion.state & GDK_BUTTON2_MASK)) + || (panning == 1 && !(event->motion.state & GDK_BUTTON1_MASK)) + || (panning == 3 && !(event->motion.state & GDK_BUTTON3_MASK))) { + /* Gdk seems to lose button release for us sometimes :-( */ + panning = PANNING_NONE; + ungrabCanvasEvents(); + ret = TRUE; + } else { + // To fix https://bugs.launchpad.net/inkscape/+bug/1458200 + // we increase the tolerance because no sensible data for panning + if (within_tolerance && (abs((gint) event->motion.x - xp) + < tolerance * 3) && (abs((gint) event->motion.y - yp) + < tolerance * 3)) { + // do not drag if we're within tolerance from origin + break; + } + + // 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; + + // gobble subsequent motion events to prevent "sticking" + // when scrolling is slow + gobble_motion_events(panning == 2 ? GDK_BUTTON2_MASK : (panning + == 1 ? GDK_BUTTON1_MASK : GDK_BUTTON3_MASK)); + + if (panning_cursor == 0) { + panning_cursor = 1; + auto display = _desktop->getCanvas()->get_display(); + auto window = _desktop->getCanvas()->get_window(); + auto cursor = Gdk::Cursor::create(display, "move"); + window->set_cursor(cursor); + } + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const moved_w(motion_w - button_w); + _desktop->scroll_relative(moved_w, true); // we're still scrolling, do not redraw + ret = TRUE; + } + } else if (zoom_rb) { + if (within_tolerance && (abs((gint) event->motion.x - xp) + < tolerance) && (abs((gint) event->motion.y - yp) + < tolerance)) { + break; // do not drag if we're within tolerance from origin + } + + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + within_tolerance = false; + + if (Inkscape::Rubberband::get(_desktop)->is_started()) { + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + + Inkscape::Rubberband::get(_desktop)->move(motion_dt); + } else { + // Start the box where the mouse was clicked, not where it is now + // because otherwise our box would be offset by the amount of tolerance. + Geom::Point const motion_w(xp, yp); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + + Inkscape::Rubberband::get(_desktop)->start(_desktop, motion_dt); + } + + if (zoom_rb == 2) { + gobble_motion_events(GDK_BUTTON2_MASK); + } + } + break; + + case GDK_BUTTON_RELEASE: { + bool middle_mouse_zoom = prefs->getBool("/options/middlemousezoom/value"); + + xp = yp = 0; + + if (panning_cursor == 1) { + panning_cursor = 0; + _desktop->getCanvas()->get_window()->set_cursor(_cursor); + } + + if (middle_mouse_zoom && within_tolerance && (panning || zoom_rb)) { + zoom_rb = 0; + + if (panning) { + panning = PANNING_NONE; + ungrabCanvasEvents(); + } + + Geom::Point const event_w(event->button.x, event->button.y); + Geom::Point const event_dt(_desktop->w2d(event_w)); + + double const zoom_inc = prefs->getDoubleLimited( + "/options/zoomincrement/value", M_SQRT2, 1.01, 10); + + _desktop->zoom_relative(event_dt, (event->button.state & GDK_SHIFT_MASK) ? 1 / zoom_inc : zoom_inc); + ret = TRUE; + } else if (panning == event->button.button) { + panning = PANNING_NONE; + ungrabCanvasEvents(); + + // in slow complex drawings, some of the motion events are lost; + // to make up for this, we scroll it once again to the button-up event coordinates + // (i.e. canvas will always get scrolled all the way to the mouse release point, + // even if few intermediate steps were visible) + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const moved_w(motion_w - button_w); + + _desktop->scroll_relative(moved_w); + ret = TRUE; + } else if (zoom_rb == event->button.button) { + zoom_rb = 0; + + Geom::OptRect const b = Inkscape::Rubberband::get(_desktop)->getRectangle(); + Inkscape::Rubberband::get(_desktop)->stop(); + + if (b && !within_tolerance) { + _desktop->set_display_area(*b, 10); + } + + ret = TRUE; + } + } + break; + + case GDK_KEY_PRESS: { + double const acceleration = prefs->getDoubleLimited( + "/options/scrollingacceleration/value", 0, 0, 6); + int const key_scroll = prefs->getIntLimited("/options/keyscroll/value", + 10, 0, 1000); + + switch (get_latin_keyval(&event->key)) { + // GDK insists on stealing these keys (F1 for no idea what, tab for cycling widgets + // in the editing window). So we resteal them back and run our regular shortcut + // invoker on them. Tab is hardcoded. When actions are triggered by tab, + // we end up stealing events from GTK widgets. + case GDK_KEY_F1: + ret = Inkscape::Shortcuts::getInstance().invoke_action(&event->key); + break; + case GDK_KEY_Tab: + sp_selection_item_next(_desktop); + ret = true; + break; + case GDK_KEY_ISO_Left_Tab: + sp_selection_item_prev(_desktop); + ret = true; + break; + + case GDK_KEY_Q: + case GDK_KEY_q: + if (_desktop->quick_zoomed()) { + ret = TRUE; + } + if (!MOD__SHIFT(event) && !MOD__CTRL(event) && !MOD__ALT(event)) { + _desktop->zoom_quick(true); + ret = TRUE; + } + break; + + case GDK_KEY_W: + case GDK_KEY_w: + case GDK_KEY_F4: + /* Close view */ + if (MOD__CTRL_ONLY(event)) { + sp_ui_close_view(nullptr); + ret = TRUE; + } + break; + + case GDK_KEY_Left: // Ctrl Left + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, acceleration)); + + gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK); + _desktop->scroll_relative(Geom::Point(i, 0)); + } else if (!_keyboardMove(event->key, Geom::Point(-1, 0))) { + Inkscape::Shortcuts::getInstance().invoke_action(&event->key); + } + ret = true; + break; + + case GDK_KEY_Up: // Ctrl Up + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, acceleration)); + + gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK); + _desktop->scroll_relative(Geom::Point(0, i)); + } else if (!_keyboardMove(event->key, Geom::Point(0, -_desktop->yaxisdir()))) { + Inkscape::Shortcuts::getInstance().invoke_action(&event->key); + } + ret = true; + break; + + case GDK_KEY_Right: // Ctrl Right + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, acceleration)); + + gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK); + _desktop->scroll_relative(Geom::Point(-i, 0)); + } else if (!_keyboardMove(event->key, Geom::Point(1, 0))) { + Inkscape::Shortcuts::getInstance().invoke_action(&event->key); + } + ret = true; + break; + + case GDK_KEY_Down: // Ctrl Down + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, acceleration)); + + gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK); + _desktop->scroll_relative(Geom::Point(0, -i)); + } else if (!_keyboardMove(event->key, Geom::Point(0, _desktop->yaxisdir()))) { + Inkscape::Shortcuts::getInstance().invoke_action(&event->key); + } + ret = true; + break; + + case GDK_KEY_Menu: + this->menu_popup(event); + ret = TRUE; + break; + + case GDK_KEY_F10: + if (MOD__SHIFT_ONLY(event)) { + this->menu_popup(event); + ret = TRUE; + } + break; + + case GDK_KEY_space: + within_tolerance = true; + xp = yp = 0; + if (!allow_panning) break; + panning = PANNING_SPACE; + this->message_context->set(Inkscape::INFORMATION_MESSAGE, + _("<b>Space+mouse move</b> to pan canvas")); + + ret = TRUE; + break; + + case GDK_KEY_z: + case GDK_KEY_Z: + if (MOD__ALT_ONLY(event)) { + _desktop->zoom_grab_focus(); + ret = TRUE; + } + break; + + default: + break; + } + } + break; + + case GDK_KEY_RELEASE: + // Stop panning on any key release + if (this->is_space_panning()) { + this->message_context->clear(); + } + + if (panning) { + panning = PANNING_NONE; + xp = yp = 0; + + ungrabCanvasEvents(); + } + + if (panning_cursor == 1) { + panning_cursor = 0; + _desktop->getCanvas()->get_window()->set_cursor(_cursor); + } + + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_space: + if (within_tolerance) { + // Space was pressed, but not panned + sp_toggle_selector(_desktop); + + // Be careful, sp_toggle_selector will delete ourselves. + // Thus, make sure we return immediately. + return true; + } + + break; + + case GDK_KEY_Q: + case GDK_KEY_q: + if (_desktop->quick_zoomed()) { + _desktop->zoom_quick(false); + ret = TRUE; + } + break; + + default: + break; + } + break; + + case GDK_SCROLL: { + int constexpr WHEEL_SCROLL_DEFAULT = 40; + + // previously we did two wheel_scrolls for each mouse scroll + int const wheel_scroll = prefs->getIntLimited( + "/options/wheelscroll/value", WHEEL_SCROLL_DEFAULT, 0, 1000) * 2; + + // Size of smooth-scrolls (only used in GTK+ 3) + gdouble delta_x = 0; + gdouble delta_y = 0; + + using Modifiers::Type; + using Modifiers::Triggers; + Type action = Modifiers::Modifier::which(Triggers::CANVAS | Triggers::SCROLL, event->scroll.state); + + if (action == Type::CANVAS_ROTATE && !_desktop->get_rotation_lock()) { + double rotate_inc = prefs->getDoubleLimited( + "/options/rotateincrement/value", 15, 1, 90, "°" ); + rotate_inc *= M_PI/180.0; + + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + // Do nothing + break; + + case GDK_SCROLL_DOWN: + rotate_inc = -rotate_inc; + break; + + case GDK_SCROLL_SMOOTH: { + gdk_event_get_scroll_deltas(event, &delta_x, &delta_y); +#ifdef GDK_WINDOWING_QUARTZ + // MacBook trackpad scroll event gives pixel delta + delta_y /= WHEEL_SCROLL_DEFAULT; +#endif + double delta_y_clamped = CLAMP(delta_y, -1.0, 1.0); // values > 1 result in excessive rotating + rotate_inc = rotate_inc * -delta_y_clamped; + break; + } + + default: + rotate_inc = 0.0; + break; + } + + if (rotate_inc != 0.0) { + Geom::Point const scroll_dt = _desktop->point(); + _desktop->rotate_relative_keep_point(scroll_dt, rotate_inc); + ret = TRUE; + } + + } else if (action == Type::CANVAS_PAN_X) { + /* shift + wheel, pan left--right */ + + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + case GDK_SCROLL_LEFT: + _desktop->scroll_relative(Geom::Point(wheel_scroll, 0)); + ret = TRUE; + break; + + case GDK_SCROLL_DOWN: + case GDK_SCROLL_RIGHT: + _desktop->scroll_relative(Geom::Point(-wheel_scroll, 0)); + ret = TRUE; + break; + + case GDK_SCROLL_SMOOTH: { + gdk_event_get_scroll_deltas(event, &delta_x, &delta_y); +#ifdef GDK_WINDOWING_QUARTZ + // MacBook trackpad scroll event gives pixel delta + delta_y /= WHEEL_SCROLL_DEFAULT; +#endif + _desktop->scroll_relative(Geom::Point(wheel_scroll * -delta_y, 0)); + ret = TRUE; + break; + } + + default: + break; + } + + } else if (action == Type::CANVAS_ZOOM) { + /* ctrl + wheel, zoom in--out */ + double rel_zoom; + double const zoom_inc = prefs->getDoubleLimited( + "/options/zoomincrement/value", M_SQRT2, 1.01, 10); + + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + rel_zoom = zoom_inc; + break; + + case GDK_SCROLL_DOWN: + rel_zoom = 1 / zoom_inc; + break; + + case GDK_SCROLL_SMOOTH: { + gdk_event_get_scroll_deltas(event, &delta_x, &delta_y); +#ifdef GDK_WINDOWING_QUARTZ + // MacBook trackpad scroll event gives pixel delta + delta_y /= WHEEL_SCROLL_DEFAULT; +#endif + double delta_y_clamped = CLAMP(std::abs(delta_y), 0.0, 1.0); // values > 1 result in excessive zooming + double zoom_inc_scaled = (zoom_inc-1) * delta_y_clamped + 1; + if (delta_y < 0) { + rel_zoom = zoom_inc_scaled; + } else { + rel_zoom = 1 / zoom_inc_scaled; + } + break; + } + + default: + rel_zoom = 0.0; + break; + } + + if (rel_zoom != 0.0) { + Geom::Point const scroll_dt = _desktop->point(); + _desktop->zoom_relative(scroll_dt, rel_zoom); + ret = TRUE; + } + + /* no modifier, pan up--down (left--right on multiwheel mice?) */ + } else if (action == Type::CANVAS_PAN_Y) { + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + _desktop->scroll_relative(Geom::Point(0, wheel_scroll)); + break; + + case GDK_SCROLL_DOWN: + _desktop->scroll_relative(Geom::Point(0, -wheel_scroll)); + break; + + case GDK_SCROLL_LEFT: + _desktop->scroll_relative(Geom::Point(wheel_scroll, 0)); + break; + + case GDK_SCROLL_RIGHT: + _desktop->scroll_relative(Geom::Point(-wheel_scroll, 0)); + break; + + case GDK_SCROLL_SMOOTH: + gdk_event_get_scroll_deltas(event, &delta_x, &delta_y); +#ifdef GDK_WINDOWING_QUARTZ + // MacBook trackpad scroll event gives pixel delta + delta_x /= WHEEL_SCROLL_DEFAULT; + delta_y /= WHEEL_SCROLL_DEFAULT; +#endif + _desktop->scroll_relative(Geom::Point(-wheel_scroll * delta_x, -wheel_scroll * delta_y)); + break; + } + ret = TRUE; + } else { + g_warning("unhandled scroll event with scroll.state=0x%x", event->scroll.state); + } + break; + } + + default: + break; + } + + return ret; +} + +/** + * This function allows to handle global tool events if _pre function is not fully overridden. + */ + +void ToolBase::set_on_buttons(GdkEvent *event) +{ + switch (event->type) { + case GDK_BUTTON_PRESS: + switch (event->button.button) { + case 1: + this->_button1on = true; + break; + case 2: + this->_button2on = true; + break; + case 3: + this->_button3on = true; + break; + } + break; + case GDK_BUTTON_RELEASE: + switch (event->button.button) { + case 1: + this->_button1on = false; + break; + case 2: + this->_button2on = false; + break; + case 3: + this->_button3on = false; + break; + } + break; + case GDK_MOTION_NOTIFY: + if (event->motion.state & Gdk::ModifierType::BUTTON1_MASK) { + this->_button1on = true; + } else { + this->_button1on = false; + } + if (event->motion.state & Gdk::ModifierType::BUTTON2_MASK) { + this->_button2on = true; + } else { + this->_button2on = false; + } + if (event->motion.state & Gdk::ModifierType::BUTTON3_MASK) { + this->_button3on = true; + } else { + this->_button3on = false; + } + } +} + +bool ToolBase::are_buttons_1_and_3_on() const +{ + return this->_button1on && this->_button3on; +} + +bool ToolBase::are_buttons_1_and_3_on(GdkEvent* event) +{ + set_on_buttons(event); + return are_buttons_1_and_3_on(); +} + +/** + * Handles item specific events. Gets called from Gdk. + * + * Only reacts to right mouse button at the moment. + * \todo Fixme: do context sensitive popup menu on items. + */ +bool ToolBase::item_handler(SPItem* item, GdkEvent* event) { + int ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (!are_buttons_1_and_3_on(event) && event->button.button == 3 && + !((event->button.state & GDK_SHIFT_MASK) || (event->button.state & GDK_CONTROL_MASK))) { + this->menu_popup(event); + ret = TRUE; + } + break; + + default: + break; + } + + return ret; +} + +/** + * Returns true if we're hovering above a knot (needed because we don't want to pre-snap in that case). + */ +bool ToolBase::sp_event_context_knot_mouseover() const { + if (this->shape_editor) { + return this->shape_editor->knot_mouseover(); + } + + return false; +} + +/** + * Enables/disables the ToolBase's SelCue. + */ +void ToolBase::enableSelectionCue(bool enable) { + if (enable) { + if (!_selcue) { + _selcue = new Inkscape::SelCue(_desktop); + } + } else { + delete _selcue; + _selcue = nullptr; + } +} + +/* + * Enables/disables the ToolBase's GrDrag. + */ +void ToolBase::enableGrDrag(bool enable) { + if (enable) { + if (!_grdrag) { + _grdrag = new GrDrag(_desktop); + } + } else { + if (_grdrag) { + delete _grdrag; + _grdrag = nullptr; + } + } +} + +/** + * Delete a selected GrDrag point + */ +bool ToolBase::deleteSelectedDrag(bool just_one) +{ + if (_grdrag && !_grdrag->selected.empty()) { + _grdrag->deleteSelected(just_one); + return true; + } + return false; +} + +/** + * Return true if there is a gradient drag. + */ +bool ToolBase::hasGradientDrag() const +{ + return _grdrag && _grdrag->isNonEmpty(); +} + +/** + * Grab events from the Canvas Catchall. (Common configuration.) + */ +void ToolBase::grabCanvasEvents(Gdk::EventMask mask) +{ + _desktop->getCanvasCatchall()->grab(mask, nullptr); // Cursor is null. +} + +/** + * Ungrab events from the Canvas Catchall. (Common configuration.) + */ +void ToolBase::ungrabCanvasEvents() +{ + _desktop->snapindicator->remove_snaptarget(); + _desktop->getCanvasCatchall()->ungrab(); +} + +/** Enable (or disable) high precision for motion events + * + * This is intended to be used by drawing tools, that need to process motion events with high accuracy + * and high update rate (for example free hand tools) + * + * With standard accuracy some intermediate motion events might be discarded + * + * Call this function when an operation that requires high accuracy is started (e.g. mouse button is pressed + * to draw a line). Make sure to call it again and restore standard precision afterwards. **/ +void ToolBase::set_high_motion_precision(bool high_precision) { + Glib::RefPtr<Gdk::Window> window = _desktop->getToplevel()->get_window(); + + if (window) { + window->set_event_compression(!high_precision); + } +} + +Geom::Point ToolBase::setup_for_drag_start(GdkEvent *ev) +{ + this->xp = static_cast<gint>(ev->button.x); + this->yp = static_cast<gint>(ev->button.y); + this->within_tolerance = true; + + Geom::Point const p(ev->button.x, ev->button.y); + item_to_select = Inkscape::UI::Tools::sp_event_context_find_item(_desktop, p, ev->button.state & GDK_MOD1_MASK, TRUE); + return _desktop->w2d(p); +} + +/** + * Discard and count matching key events from top of event bucket. + * Convenience function that just passes request to canvas. + */ +int ToolBase::gobble_key_events(guint keyval, guint mask) const +{ + return _desktop->canvas->gobble_key_events(keyval, mask); +} + +/** + * Discard matching motion events from top of event bucket. + * Convenience function that just passes request to canvas. + */ +void ToolBase::gobble_motion_events(guint mask) const +{ + _desktop->canvas->gobble_motion_events(mask); +} + +/** + * Calls virtual set() function of ToolBase. + */ +void sp_event_context_read(ToolBase *ec, gchar const *key) { + g_return_if_fail(ec != nullptr); + g_return_if_fail(key != nullptr); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::Preferences::Entry val = prefs->getEntry(ec->getPrefsPath() + '/' + key); + ec->set(val); +} + +/** + * Handles snapping events for all tools and then passes to tool_root_handler. + */ +gint ToolBase::start_root_handler(GdkEvent *event) +{ +#ifdef EVENT_DEBUG + ui_dump_event(reinterpret_cast<GdkEvent *>(event), "ToolBase::start_root_handler"); +#endif + + if (!_uses_snap) { + return this->tool_root_handler(event); + } + + switch (event->type) { + case GDK_MOTION_NOTIFY: + sp_event_context_snap_delay_handler(this, nullptr, nullptr, + (GdkEventMotion *) event, + DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER); + break; + case GDK_BUTTON_RELEASE: + if (_delayed_snap_event) { + // If we have any pending snapping action, then invoke it now + sp_event_context_snap_watchdog_callback(_delayed_snap_event); + } + break; + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + // Snapping will be on hold if we're moving the mouse at high speeds. When starting + // drawing a new shape we really should snap though. + _desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); + break; + default: + break; + } + + return this->tool_root_handler(event); +} + +/** + * Calls the right tool's event handler, depending on the selected tool and state. + */ +gint ToolBase::tool_root_handler(GdkEvent *event) +{ +#ifdef EVENT_DEBUG + ui_dump_event(reinterpret_cast<GdkEvent *>(event), "tool_root_handler"); +#endif + gint ret = false; + + // Just set the on buttons for now. later, behave as intended. + this->set_on_buttons(event); + + // refresh coordinates UI here while 'event' is still valid + set_event_location(_desktop, event); + + // Panning has priority over tool-specific event handling + if (this->is_panning()) { + ret = ToolBase::root_handler(event); + } else { + ret = this->root_handler(event); + } + + // at this point 'event' could be deleted already (after ctrl+w document close) + + return ret; +} + +/** + * Starts handling item snapping and pass to virtual_item_handler afterwards. + */ +gint ToolBase::start_item_handler(SPItem *item, GdkEvent *event) +{ + if (!_uses_snap) { + return this->virtual_item_handler(item, event); + } + + switch (event->type) { + case GDK_MOTION_NOTIFY: + sp_event_context_snap_delay_handler(this, (gpointer) item, nullptr, (GdkEventMotion *) event, DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER); + break; + case GDK_BUTTON_RELEASE: + if (_delayed_snap_event) { + // If we have any pending snapping action, then invoke it now + sp_event_context_snap_watchdog_callback(_delayed_snap_event); + } + break; + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + // Snapping will be on hold if we're moving the mouse at high speeds. When starting + // drawing a new shape we really should snap though. + _desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); + break; + default: + break; + } + + return this->virtual_item_handler(item, event); +} + +gint ToolBase::virtual_item_handler(SPItem *item, GdkEvent *event) +{ + gint ret = false; + + // Just set the on buttons for now. later, behave as intended. + this->set_on_buttons(event); + + // Panning has priority over tool-specific event handling + if (is_panning()) { + ret = ToolBase::item_handler(item, event); + } else { + ret = item_handler(item, event); + } + + if (!ret) { + ret = this->tool_root_handler(event); + } else { + set_event_location(_desktop, event); + } + + return ret; +} + +/** + * Shows coordinates on status bar. + */ +static void set_event_location(SPDesktop *desktop, GdkEvent *event) { + if (event->type != GDK_MOTION_NOTIFY) { + return; + } + + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point const button_dt(desktop->w2d(button_w)); + desktop->set_coordinate_status(button_dt); +} + +//------------------------------------------------------------------- +/** + * Create popup menu and tell Gtk to show it. + */ +void ToolBase::menu_popup(GdkEvent *event, SPObject *obj) { + + if (!obj) { + if (event->type == GDK_KEY_PRESS && !_desktop->getSelection()->isEmpty()) { + obj = _desktop->getSelection()->items().front(); + } else { + // Using the same function call used on left click in sp_select_context_item_handler() to get top of z-order + // fixme: sp_canvas_arena should set the top z-order object as arena->active + auto p = Geom::Point(event->button.x, event->button.y); + obj = sp_event_context_find_item (_desktop, p, false, false); + } + } + + ContextMenu* menu = new ContextMenu(_desktop, obj); + menu->attach_to_widget(*(_desktop->getCanvas())); // So actions work! + menu->show(); + + switch (event->type) { + case GDK_BUTTON_PRESS: + case GDK_KEY_PRESS: + menu->popup_at_pointer(event); + break; + default: + break; + } +} + +/** + * Show tool context specific modifier tip. + */ +void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, + GdkEvent *event, gchar const *ctrl_tip, gchar const *shift_tip, + gchar const *alt_tip) { + guint keyval = get_latin_keyval(&event->key); + + bool ctrl = ctrl_tip && (MOD__CTRL(event) || (keyval == GDK_KEY_Control_L) || (keyval + == GDK_KEY_Control_R)); + bool shift = shift_tip && (MOD__SHIFT(event) || (keyval == GDK_KEY_Shift_L) || (keyval + == GDK_KEY_Shift_R)); + bool alt = alt_tip && (MOD__ALT(event) || (keyval == GDK_KEY_Alt_L) || (keyval + == GDK_KEY_Alt_R) || (keyval == GDK_KEY_Meta_L) || (keyval == GDK_KEY_Meta_R)); + + gchar *tip = g_strdup_printf("%s%s%s%s%s", (ctrl ? ctrl_tip : ""), (ctrl + && (shift || alt) ? "; " : ""), (shift ? shift_tip : ""), ((ctrl + || shift) && alt ? "; " : ""), (alt ? alt_tip : "")); + + if (strlen(tip) > 0) { + message_context->flash(Inkscape::INFORMATION_MESSAGE, tip); + } + + g_free(tip); +} + +/** + * Try to determine the keys group of Latin layout. + * Check available keymap entries for Latin 'a' key and find the minimal integer value. + */ +static void update_latin_keys_group() { + GdkKeymapKey* keys; + gint n_keys; + + latin_keys_group_valid = FALSE; + latin_keys_groups.clear(); + + if (gdk_keymap_get_entries_for_keyval(Gdk::Display::get_default()->get_keymap(), GDK_KEY_a, &keys, &n_keys)) { + for (gint i = 0; i < n_keys; i++) { + latin_keys_groups.insert(keys[i].group); + + if (!latin_keys_group_valid || keys[i].group < latin_keys_group) { + latin_keys_group = keys[i].group; + latin_keys_group_valid = TRUE; + } + } + g_free(keys); + } +} + +/** + * Initialize Latin keys group handling. + */ +void init_latin_keys_group() { + g_signal_connect(G_OBJECT(Gdk::Display::get_default()->get_keymap()), + "keys-changed", G_CALLBACK(update_latin_keys_group), NULL); + update_latin_keys_group(); +} + +/** + * Return the keyval corresponding to the key event in Latin group. + * + * Use this instead of simply event->keyval, so that your keyboard shortcuts + * work regardless of layouts (e.g., in Cyrillic). + */ +guint get_latin_keyval(GdkEventKey const *event, guint *consumed_modifiers /*= NULL*/) { + guint keyval = 0; + GdkModifierType modifiers; + gint group = latin_keys_group_valid ? latin_keys_group : event->group; + + if (latin_keys_groups.count(event->group)) { + // Keyboard group is a latin layout, so just use it. + group = event->group; + } + + gdk_keymap_translate_keyboard_state( + Gdk::Display::get_default()->get_keymap(), + event->hardware_keycode, (GdkModifierType) event->state, group, + &keyval, nullptr, nullptr, &modifiers); + + if (consumed_modifiers) { + *consumed_modifiers = modifiers; + } + if (keyval != event->keyval) { + std::cerr << "get_latin_keyval: OH OH OH keyval did change! " + << " keyval: " << keyval << " (" << (char)keyval << ")" + << " event->keyval: " << event->keyval << "(" << (char)event->keyval << ")" << std::endl; + } + return keyval; +} + +/** + * Returns item at point p in desktop. + * + * If state includes alt key mask, cyclically selects under; honors + * into_groups. + */ +SPItem *sp_event_context_find_item(SPDesktop *desktop, Geom::Point const &p, + bool select_under, bool into_groups) +{ + SPItem *item = nullptr; + + if (select_under) { + auto tmp = desktop->selection->items(); + std::vector<SPItem *> vec(tmp.begin(), tmp.end()); + SPItem *selected_at_point = desktop->getItemFromListAtPointBottom(vec, p); + item = desktop->getItemAtPoint(p, into_groups, selected_at_point); + if (item == nullptr) { // we may have reached bottom, flip over to the top + item = desktop->getItemAtPoint(p, into_groups, nullptr); + } + } else { + item = desktop->getItemAtPoint(p, into_groups, nullptr); + } + + return item; +} + +/** + * Returns item if it is under point p in desktop, at any depth; otherwise returns NULL. + * + * Honors into_groups. + */ +SPItem * +sp_event_context_over_item(SPDesktop *desktop, SPItem *item, + Geom::Point const &p) { + std::vector<SPItem*> temp; + temp.push_back(item); + SPItem *item_at_point = desktop->getItemFromListAtPointBottom(temp, p); + return item_at_point; +} + +ShapeEditor * +sp_event_context_get_shape_editor(ToolBase *ec) { + return ec->shape_editor; +} + + +/** + * Analyses the current event, calculates the mouse speed, turns snapping off (temporarily) if the + * mouse speed is above a threshold, and stores the current event such that it can be re-triggered when needed + * (re-triggering is controlled by a watchdog timer). + * + * @param ec Pointer to the event context. + * @param dse_item Pointer that store a reference to a canvas or to an item. + * @param dse_item2 Another pointer, storing a reference to a knot or controlpoint. + * @param event Pointer to the motion event. + * @param origin Identifier (enum) specifying where the delay (and the call to this method) were initiated. + */ +void sp_event_context_snap_delay_handler(ToolBase *ec, + gpointer const dse_item, gpointer const dse_item2, GdkEventMotion *event, + DelayedSnapEvent::DelayedSnapEventOrigin origin) +{ + static guint32 prev_time; + static std::optional<Geom::Point> prev_pos; + + if (!ec->_uses_snap || ec->_dse_callback_in_process) { + return; + } + + // Snapping occurs when dragging with the left mouse button down, or when hovering e.g. in the pen tool with left mouse button up + bool const c1 = event->state & GDK_BUTTON2_MASK; // We shouldn't hold back any events when other mouse buttons have been + bool const c2 = event->state & GDK_BUTTON3_MASK; // pressed, e.g. when scrolling with the middle mouse button; if we do then + // Inkscape will get stuck in an unresponsive state + bool const c3 = dynamic_cast<Inkscape::UI::Tools::CalligraphicTool *>(ec); + // The snap delay will repeat the last motion event, which will lead to + // erroneous points in the calligraphy context. And because we don't snap + // in this context, we might just as well disable the snap delay all together + bool const c4 = ec->is_panning(); // Don't snap while panning + + if (c1 || c2 || c3 || c4) { + // Make sure that we don't send any pending snap events to a context if we know in advance + // that we're not going to snap any way (e.g. while scrolling with middle mouse button) + // Any motion event might affect the state of the context, leading to unexpected behavior + ec->discard_delayed_snap_event(); + } else if (ec->getDesktop() && + ec->getDesktop()->namedview->snap_manager.snapprefs.getSnapEnabledGlobally()) { + // Snap when speed drops below e.g. 0.02 px/msec, or when no motion events have occurred for some period. + // i.e. snap when we're at stand still. A speed threshold enforces snapping for tablets, which might never + // be fully at stand still and might keep spitting out motion events. + ec->getDesktop()->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(true); // put snapping on hold + + Geom::Point event_pos(event->x, event->y); + guint32 event_t = gdk_event_get_time((GdkEvent *) event); + + if (prev_pos) { + Geom::Coord dist = Geom::L2(event_pos - *prev_pos); + guint32 delta_t = event_t - prev_time; + gdouble speed = delta_t > 0 ? dist / delta_t : 1000; + //std::cout << "Mouse speed = " << speed << " px/msec " << std::endl; + if (speed > 0.02) { // Jitter threshold, might be needed for tablets + // We're moving fast, so postpone any snapping until the next GDK_MOTION_NOTIFY event. We + // will keep on postponing the snapping as long as the speed is high. + // We must snap at some point in time though, so set a watchdog timer at some time from + // now, just in case there's no future motion event that drops under the speed limit (when + // stopping abruptly) + delete ec->_delayed_snap_event; + ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin); // watchdog is reset, i.e. pushed forward in time + // If the watchdog expires before a new motion event is received, we will snap (as explained + // above). This means however that when the timer is too short, we will always snap and that the + // speed threshold is ineffective. In the extreme case the delay is set to zero, and snapping will + // be immediate, as it used to be in the old days ;-). + } else { // Speed is very low, so we're virtually at stand still + // But if we're really standing still, then we should snap now. We could use some low-pass filtering, + // otherwise snapping occurs for each jitter movement. For this filtering we'll leave the watchdog to expire, + // snap, and set a new watchdog again. + if (ec->_delayed_snap_event == nullptr) { // no watchdog has been set + // it might have already expired, so we'll set a new one; the snapping frequency will be limited this way + ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin); + } // else: watchdog has been set before and we'll wait for it to expire + } + } else { + // This is the first GDK_MOTION_NOTIFY event, so postpone snapping and set the watchdog + g_assert(ec->_delayed_snap_event == nullptr); + ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin); + } + + prev_pos = event_pos; + prev_time = event_t; + } +} + +/** + * When the snap delay watchdog timer barks, this method will be called and will re-inject the last motion + * event in an appropriate place, with snapping being turned on again. + */ +gboolean sp_event_context_snap_watchdog_callback(gpointer data) { + // Snap NOW! For this the "postponed" flag will be reset and the last motion event will be repeated + DelayedSnapEvent *dse = reinterpret_cast<DelayedSnapEvent*> (data); + + if (dse == nullptr) { + // This might occur when this method is called directly, i.e. not through the timer + // E.g. on GDK_BUTTON_RELEASE in start_root_handler() + return FALSE; + } + + ToolBase *ec = dse->getEventContext(); + if (ec == nullptr) { + delete dse; + return false; + } + + const SPDesktop *dt = ec->getDesktop(); + if (dt == nullptr) { + ec->_delayed_snap_event = nullptr; + delete dse; + return false; + } + + ec->_dse_callback_in_process = true; + + dt->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); + + // Depending on where the delayed snap event originated from, we will inject it back at it's origin + // The switch below takes care of that and prepares the relevant parameters + switch (dse->getOrigin()) { + case DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER: + ec->tool_root_handler(dse->getEvent()); + break; + case DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER: { + auto item = static_cast<SPItem *>(dse->getItem()); + if (item) { + ec->virtual_item_handler(item, dse->getEvent()); + } + } + break; + case DelayedSnapEvent::KNOT_HANDLER: { + gpointer knot = dse->getItem2(); + check_if_knot_deleted(knot); + if (knot && SP_IS_KNOT(knot)) { + bool was_grabbed = SP_KNOT(knot)->is_grabbed(); + SP_KNOT(knot)->setFlag(SP_KNOT_GRABBED, true); // Must be grabbed for Inkscape::SelTrans::handleRequest() to pass + sp_knot_handler_request_position(dse->getEvent(), SP_KNOT(knot)); + SP_KNOT(knot)->setFlag(SP_KNOT_GRABBED, was_grabbed); + } + } + break; + case DelayedSnapEvent::CONTROL_POINT_HANDLER: { + using Inkscape::UI::ControlPoint; + gpointer pitem2 = dse->getItem2(); + if (!pitem2) + { + ec->_delayed_snap_event = nullptr; + delete dse; + return false; + } + ControlPoint *point = reinterpret_cast<ControlPoint*> (pitem2); + if (point) { + if (point->position().isFinite() && (dt == point->_desktop)) { + point->_eventHandler(ec, dse->getEvent()); + } + else { + //workaround: + //[Bug 781893] Crash after moving a Bezier node after Knot path effect? + // --> at some time, some point with X = 0 and Y = nan (not a number) is created ... + // even so, the desktop pointer is invalid and equal to 0xff + g_warning ("encountered non finite point when evaluating snapping callback"); + } + } + } + break; + case DelayedSnapEvent::GUIDE_HANDLER: { + auto item = static_cast<CanvasItemGuideLine *>(dse->getItem()); + auto item2 = static_cast<SPGuide *>(dse->getItem2()); + if (item && item2) { + sp_dt_guide_event(dse->getEvent(), item, item2); + } + } + break; + case DelayedSnapEvent::GUIDE_HRULER: + case DelayedSnapEvent::GUIDE_VRULER: { + gpointer item = dse->getItem(); + auto item2 = static_cast<Gtk::Widget *>(dse->getItem2()); + if (item && item2) { + g_assert(GTK_IS_WIDGET(item)); + if (dse->getOrigin() == DelayedSnapEvent::GUIDE_HRULER) { + SPDesktopWidget::ruler_event(GTK_WIDGET(item), dse->getEvent(), SP_DESKTOP_WIDGET(item2), true); + } else { + SPDesktopWidget::ruler_event(GTK_WIDGET(item), dse->getEvent(), SP_DESKTOP_WIDGET(item2), false); + } + } + } + break; + default: + g_warning("Origin of snap-delay event has not been defined!;"); + break; + } + + ec->_delayed_snap_event = nullptr; + delete dse; + + ec->_dse_callback_in_process = false; + + return FALSE; //Kills the timer and stops it from executing this callback over and over again. +} + +void ToolBase::discard_delayed_snap_event() +{ + delete _delayed_snap_event; + _delayed_snap_event = nullptr; + _desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); +} +} +} +} + +/* + 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/tools/tool-base.h b/src/ui/tools/tool-base.h new file mode 100644 index 0000000..c384d09 --- /dev/null +++ b/src/ui/tools/tool-base.h @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_EVENT_CONTEXT_H +#define SEEN_SP_EVENT_CONTEXT_H + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.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 <cstddef> +#include <string> +#include <memory> + +#include <gdkmm/device.h> // EventMask +#include <gdkmm/cursor.h> +#include <glib-object.h> +#include <sigc++/trackable.h> + +#include <2geom/point.h> + +#include "preferences.h" + +class GrDrag; +class SPDesktop; +class SPObject; +class SPItem; +class SPGroup; +class KnotHolder; +namespace Inkscape { + class MessageContext; + class SelCue; +} + +namespace Inkscape { +namespace UI { + +class ShapeEditor; + +namespace Tools { + +class ToolBase; + +gboolean sp_event_context_snap_watchdog_callback(gpointer data); + +class DelayedSnapEvent { +public: + enum DelayedSnapEventOrigin { + UNDEFINED_HANDLER = 0, + EVENTCONTEXT_ROOT_HANDLER, + EVENTCONTEXT_ITEM_HANDLER, + KNOT_HANDLER, + CONTROL_POINT_HANDLER, + GUIDE_HANDLER, + GUIDE_HRULER, + GUIDE_VRULER + }; + + DelayedSnapEvent(ToolBase *event_context, gpointer const dse_item, gpointer dse_item2, GdkEventMotion const *event, DelayedSnapEvent::DelayedSnapEventOrigin const origin) + : _timer_id(0) + , _event(nullptr) + , _item(dse_item) + , _item2(dse_item2) + , _origin(origin) + , _event_context(event_context) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value = prefs->getDoubleLimited("/options/snapdelay/value", 0, 0, 1000); + + // We used to have this specified in milliseconds; this has changed to seconds now for consistency's sake + if (value > 1) { // Apparently we have an old preference file, this value must have been in milliseconds; + value = value / 1000.0; // now convert this value to seconds + } + + _timer_id = g_timeout_add(value*1000.0, &sp_event_context_snap_watchdog_callback, this); + _event = gdk_event_copy((GdkEvent*) event); + + ((GdkEventMotion *)_event)->time = GDK_CURRENT_TIME; + } + + ~DelayedSnapEvent() { + if (_timer_id > 0) g_source_remove(_timer_id); // Kill the watchdog + if (_event != nullptr) gdk_event_free(_event); // Remove the copy of the original event + } + + ToolBase* getEventContext() { + return _event_context; + } + + DelayedSnapEventOrigin getOrigin() { + return _origin; + } + + GdkEvent* getEvent() { + return _event; + } + + gpointer getItem() { + return _item; + } + + gpointer getItem2() { + return _item2; + } + +private: + guint _timer_id; + GdkEvent* _event; + gpointer _item; + gpointer _item2; + DelayedSnapEventOrigin _origin; + ToolBase* _event_context; +}; + +void sp_event_context_snap_delay_handler(ToolBase *ec, gpointer const dse_item, gpointer const dse_item2, GdkEventMotion *event, DelayedSnapEvent::DelayedSnapEventOrigin origin); + + +/** + * Base class for Event processors. + * + * This is per desktop object, which (its derivatives) implements + * different actions bound to mouse events. + * + * ToolBase is an abstract base class of all tools. As the name + * indicates, event context implementations process UI events (mouse + * movements and keypresses) and take actions (like creating or modifying + * objects). There is one event context implementation for each tool, + * plus few abstract base classes. Writing a new tool involves + * subclassing ToolBase. + */ +class ToolBase : public sigc::trackable +{ +public: + ToolBase(SPDesktop *desktop, std::string prefs_path, std::string cursor_filename, bool uses_snap = true); + + virtual ~ToolBase(); + + ToolBase(const ToolBase&) = delete; + ToolBase& operator=(const ToolBase&) = delete; + + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent *event); + virtual bool item_handler(SPItem *item, GdkEvent *event); + virtual void menu_popup(GdkEvent *event, SPObject *obj = nullptr); + + void set_on_buttons(GdkEvent *event); + bool are_buttons_1_and_3_on() const; + bool are_buttons_1_and_3_on(GdkEvent *event); + + std::string getPrefsPath() { return _prefs_path; }; + void enableSelectionCue (bool enable=true); + + Inkscape::MessageContext *defaultMessageContext() const { + return message_context.get(); + } + + SPDesktop *getDesktop() { return _desktop; } + SPGroup *currentLayer() const; + + // Commonly used CanvasItemCatchall grab/ungrab. + void grabCanvasEvents(Gdk::EventMask mask = + Gdk::KEY_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | + Gdk::BUTTON_PRESS_MASK); + void ungrabCanvasEvents(); + + /** + * An observer that relays pref changes to the derived classes. + */ + class ToolPrefObserver: public Inkscape::Preferences::Observer { + public: + ToolPrefObserver(Glib::ustring const &path, ToolBase *ec) + : Inkscape::Preferences::Observer(path) + , ec(ec) + { + } + + void notify(Inkscape::Preferences::Entry const &val) override { + ec->set(val); + } + + private: + ToolBase * const ec; + }; + +private: + Inkscape::Preferences::Observer *pref_observer = nullptr; + std::string _prefs_path; + +protected: + Glib::RefPtr<Gdk::Cursor> _cursor; + std::string _cursor_filename = "select.svg"; + std::string _cursor_default = "select.svg"; + + gint xp = 0; ///< where drag started + gint yp = 0; ///< where drag started + gint tolerance = 0; + bool within_tolerance = false; ///< are we still within tolerance of origin + bool _button1on = false; + bool _button2on = false; + bool _button3on = false; + SPItem *item_to_select = nullptr; ///< the item where mouse_press occurred, to + ///< be selected if this is a click not drag + + Geom::Point setup_for_drag_start(GdkEvent *ev); + +private: + enum + { + PANNING_NONE = 0, // + PANNING_SPACE_BUTTON1 = 1, // TODO is this mode relevant? + PANNING_BUTTON2 = 2, // + PANNING_BUTTON3 = 3, // + PANNING_SPACE = 4, + } panning = PANNING_NONE; + +public: + gint start_root_handler(GdkEvent *event); + gint tool_root_handler(GdkEvent *event); + gint start_item_handler(SPItem *item, GdkEvent *event); + gint virtual_item_handler(SPItem *item, GdkEvent *event); + + /// True if we're panning with any method (space bar, middle-mouse, right-mouse+Ctrl) + bool is_panning() const { return panning != 0; } + + /// True if we're panning with the space bar + bool is_space_panning() const { return panning == PANNING_SPACE || panning == PANNING_SPACE_BUTTON1; } + + bool rotating_mode = false;; + + std::unique_ptr<Inkscape::MessageContext> message_context; + Inkscape::SelCue *_selcue = nullptr; + + GrDrag *_grdrag = nullptr; + + ShapeEditor* shape_editor = nullptr; + + bool _dse_callback_in_process = false; + + bool _uses_snap = false; + DelayedSnapEvent *_delayed_snap_event = nullptr; + + void discard_delayed_snap_event(); + void set_cursor(std::string filename); + void use_cursor(Glib::RefPtr<Gdk::Cursor> cursor); + Glib::RefPtr<Gdk::Cursor> get_cursor(Glib::RefPtr<Gdk::Window> window, std::string filename); + void use_tool_cursor(); + + void enableGrDrag(bool enable = true); + bool deleteSelectedDrag(bool just_one); + bool hasGradientDrag() const; + GrDrag *get_drag() { return _grdrag; } + +protected: + bool sp_event_context_knot_mouseover() const; + + void set_high_motion_precision(bool high_precision = true); + + int gobble_key_events(guint keyval, guint mask) const; + void gobble_motion_events(guint mask) const; + + SPDesktop *_desktop = nullptr; + +private: + + bool _keyboardMove(GdkEventKey const &event, Geom::Point const &dir); +}; + +void sp_event_context_read(ToolBase *ec, gchar const *key); + + +void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event); + +void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, GdkEvent *event, + gchar const *ctrl_tip, gchar const *shift_tip, gchar const *alt_tip); + +void init_latin_keys_group(); +guint get_latin_keyval(GdkEventKey const *event, guint *consumed_modifiers = nullptr); + +SPItem *sp_event_context_find_item (SPDesktop *desktop, Geom::Point const &p, bool select_under, bool into_groups); +SPItem *sp_event_context_over_item (SPDesktop *desktop, SPItem *item, Geom::Point const &p); + +void sp_toggle_dropper(SPDesktop *dt); + +bool sp_event_context_knot_mouseover(ToolBase *ec); + +} // namespace Tools +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_SP_EVENT_CONTEXT_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/tools/tweak-tool.cpp b/src/ui/tools/tweak-tool.cpp new file mode 100644 index 0000000..d87d3f2 --- /dev/null +++ b/src/ui/tools/tweak-tool.cpp @@ -0,0 +1,1489 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * tweaking paths without node editing + * + * Authors: + * bulia byak + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tweak-tool.h" + +#include <numeric> + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/circle.h> + +#include "context-fns.h" +#include "desktop-events.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "gradient-chemistry.h" +#include "inkscape.h" +#include "include/macros.h" +#include "message-context.h" +#include "path-chemistry.h" +#include "selection.h" +#include "style.h" + +#include "display/curve.h" +#include "display/control/canvas-item-bpath.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "object/box3d.h" +#include "object/filters/gaussian-blur.h" +#include "object/sp-flowtext.h" +#include "object/sp-item-transform.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-path.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-stop.h" +#include "object/sp-text.h" + +#include "path/path-util.h" + +#include "svg/svg.h" + +#include "ui/icon-names.h" +#include "ui/toolbar/tweak-toolbar.h" + + +using Inkscape::DocumentUndo; + +#define DDC_RED_RGBA 0xff0000ff + +#define DYNA_MIN_WIDTH 1.0e-6 + +namespace Inkscape { +namespace UI { +namespace Tools { + +TweakTool::TweakTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/tweak", "tweak-push.svg") + , pressure(TC_DEFAULT_PRESSURE) + , dragging(false) + , usepressure(false) + , usetilt(false) + , width(0.2) + , force(0.2) + , fidelity(0) + , mode(0) + , is_drawing(false) + , is_dilating(false) + , has_dilated(false) + , dilate_area(nullptr) + , do_h(true) + , do_s(true) + , do_l(true) + , do_o(false) +{ + dilate_area = new Inkscape::CanvasItemBpath(desktop->getCanvasSketch()); + dilate_area->set_stroke(0xff9900ff); + dilate_area->set_fill(0x0, SP_WIND_RULE_EVENODD); + dilate_area->hide(); + + this->is_drawing = false; + + sp_event_context_read(this, "width"); + sp_event_context_read(this, "mode"); + sp_event_context_read(this, "fidelity"); + sp_event_context_read(this, "force"); + sp_event_context_read(this, "usepressure"); + sp_event_context_read(this, "doh"); + sp_event_context_read(this, "dol"); + sp_event_context_read(this, "dos"); + sp_event_context_read(this, "doo"); + + this->style_set_connection = desktop->connectSetStyle( // catch style-setting signal in this tool + //sigc::bind(sigc::ptr_fun(&sp_tweak_context_style_set), this) + sigc::mem_fun(this, &TweakTool::set_style) + ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/tweak/selcue")) { + this->enableSelectionCue(); + } + if (prefs->getBool("/tools/tweak/gradientdrag")) { + this->enableGrDrag(); + } +} + +TweakTool::~TweakTool() { + this->enableGrDrag(false); + + this->style_set_connection.disconnect(); + + if (this->dilate_area) { + delete this->dilate_area; + this->dilate_area = nullptr; + } +} + +static bool is_transform_mode (gint mode) +{ + return (mode == TWEAK_MODE_MOVE || + mode == TWEAK_MODE_MOVE_IN_OUT || + mode == TWEAK_MODE_MOVE_JITTER || + mode == TWEAK_MODE_SCALE || + mode == TWEAK_MODE_ROTATE || + mode == TWEAK_MODE_MORELESS); +} + +static bool is_color_mode (gint mode) +{ + return (mode == TWEAK_MODE_COLORPAINT || mode == TWEAK_MODE_COLORJITTER || mode == TWEAK_MODE_BLUR); +} + +void TweakTool::update_cursor (bool with_shift) { + guint num = 0; + gchar *sel_message = nullptr; + + if (!_desktop->selection->isEmpty()) { + num = (guint)boost::distance(_desktop->selection->items()); + sel_message = g_strdup_printf(ngettext("<b>%i</b> object selected","<b>%i</b> objects selected",num), num); + } else { + sel_message = g_strdup_printf("%s", _("<b>Nothing</b> selected")); + } + + switch (this->mode) { + case TWEAK_MODE_MOVE: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag to <b>move</b>."), sel_message); + this->set_cursor("tweak-move.svg"); + break; + case TWEAK_MODE_MOVE_IN_OUT: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>move in</b>; with Shift to <b>move out</b>."), sel_message); + if (with_shift) { + this->set_cursor("tweak-move-out.svg"); + } else { + this->set_cursor("tweak-move-in.svg"); + } + break; + case TWEAK_MODE_MOVE_JITTER: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>move randomly</b>."), sel_message); + this->set_cursor("tweak-move-jitter.svg"); + break; + case TWEAK_MODE_SCALE: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>scale down</b>; with Shift to <b>scale up</b>."), sel_message); + if (with_shift) { + this->set_cursor("tweak-scale-up.svg"); + } else { + this->set_cursor("tweak-scale-down.svg"); + } + break; + case TWEAK_MODE_ROTATE: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>rotate clockwise</b>; with Shift, <b>counterclockwise</b>."), sel_message); + if (with_shift) { + this->set_cursor("tweak-rotate-counterclockwise.svg"); + } else { + this->set_cursor("tweak-rotate-clockwise.svg"); + } + break; + case TWEAK_MODE_MORELESS: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>duplicate</b>; with Shift, <b>delete</b>."), sel_message); + if (with_shift) { + this->set_cursor("tweak-less.svg"); + } else { + this->set_cursor("tweak-more.svg"); + } + break; + case TWEAK_MODE_PUSH: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag to <b>push paths</b>."), sel_message); + this->set_cursor("tweak-push.svg"); + break; + case TWEAK_MODE_SHRINK_GROW: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>inset paths</b>; with Shift to <b>outset</b>."), sel_message); + if (with_shift) { + this->set_cursor("tweak-outset.svg"); + } else { + this->set_cursor("tweak-inset.svg"); + } + break; + case TWEAK_MODE_ATTRACT_REPEL: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>attract paths</b>; with Shift to <b>repel</b>."), sel_message); + if (with_shift) { + this->set_cursor("tweak-repel.svg"); + } else { + this->set_cursor("tweak-attract.svg"); + } + break; + case TWEAK_MODE_ROUGHEN: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>roughen paths</b>."), sel_message); + this->set_cursor("tweak-roughen.svg"); + break; + case TWEAK_MODE_COLORPAINT: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>paint objects</b> with color."), sel_message); + this->set_cursor("tweak-color.svg"); + break; + case TWEAK_MODE_COLORJITTER: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>randomize colors</b>."), sel_message); + this->set_cursor("tweak-color.svg"); + break; + case TWEAK_MODE_BLUR: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>increase blur</b>; with Shift to <b>decrease</b>."), sel_message); + this->set_cursor("tweak-color.svg"); + break; + } + g_free(sel_message); +} + +bool TweakTool::set_style(const SPCSSAttr* css) { + if (this->mode == TWEAK_MODE_COLORPAINT) { // intercept color setting only in this mode + // we cannot store properties with uris + css = sp_css_attr_unset_uris(const_cast<SPCSSAttr *>(css)); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setStyle("/tools/tweak/style", const_cast<SPCSSAttr *>(css)); + return true; + } + + return false; +} + +void TweakTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "width") { + this->width = CLAMP(val.getDouble(0.1), -1000.0, 1000.0); + } else if (path == "mode") { + this->mode = val.getInt(); + this->update_cursor(false); + } else if (path == "fidelity") { + this->fidelity = CLAMP(val.getDouble(), 0.0, 1.0); + } else if (path == "force") { + this->force = CLAMP(val.getDouble(1.0), 0, 1.0); + } else if (path == "usepressure") { + this->usepressure = val.getBool(); + } else if (path == "doh") { + this->do_h = val.getBool(); + } else if (path == "dos") { + this->do_s = val.getBool(); + } else if (path == "dol") { + this->do_l = val.getBool(); + } else if (path == "doo") { + this->do_o = val.getBool(); + } +} + +static void +sp_tweak_extinput(TweakTool *tc, GdkEvent *event) +{ + if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &tc->pressure)) { + tc->pressure = CLAMP (tc->pressure, TC_MIN_PRESSURE, TC_MAX_PRESSURE); + } else { + tc->pressure = TC_DEFAULT_PRESSURE; + } +} + +static double +get_dilate_radius (TweakTool *tc) +{ + // 10 times the pen width: + return 500 * tc->width/tc->getDesktop()->current_zoom(); +} + +static double +get_path_force (TweakTool *tc) +{ + double force = 8 * (tc->usepressure? tc->pressure : TC_DEFAULT_PRESSURE) + /sqrt(tc->getDesktop()->current_zoom()); + if (force > 3) { + force += 4 * (force - 3); + } + return force * tc->force; +} + +static double +get_move_force (TweakTool *tc) +{ + double force = (tc->usepressure? tc->pressure : TC_DEFAULT_PRESSURE); + return force * tc->force; +} + +static bool +sp_tweak_dilate_recursive (Inkscape::Selection *selection, SPItem *item, Geom::Point p, Geom::Point vector, gint mode, double radius, double force, double fidelity, bool reverse) +{ + bool did = false; + + { + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box && !is_transform_mode(mode) && !is_color_mode(mode)) { + // convert 3D boxes to ordinary groups before tweaking their shapes + item = box->convert_to_group(); + selection->add(item); + } + } + + if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) { + std::vector<SPItem*> items; + items.push_back(item); + std::vector<SPItem*> selected; + std::vector<Inkscape::XML::Node*> to_select; + SPDocument *doc = item->document; + sp_item_list_to_curves (items, selected, to_select); + SPObject* newObj = doc->getObjectByRepr(to_select[0]); + item = dynamic_cast<SPItem *>(newObj); + g_assert(item != nullptr); + selection->add(item); + } + + if (dynamic_cast<SPGroup *>(item) && !dynamic_cast<SPBox3D *>(item)) { + std::vector<SPItem *> children; + for (auto& child: item->children) { + if (dynamic_cast<SPItem *>(&child)) { + children.push_back(dynamic_cast<SPItem *>(&child)); + } + } + + for (auto i = children.rbegin(); i!= children.rend(); ++i) { + SPItem *child = *i; + g_assert(child != nullptr); + if (sp_tweak_dilate_recursive (selection, child, p, vector, mode, radius, force, fidelity, reverse)) { + did = true; + } + } + } else { + if (mode == TWEAK_MODE_MOVE) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) * vector; + item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation())); + did = true; + } + } + + } else if (mode == TWEAK_MODE_MOVE_IN_OUT) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) * + (reverse? (a->midpoint() - p) : (p - a->midpoint())); + item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation())); + did = true; + } + } + + } else if (mode == TWEAK_MODE_MOVE_JITTER) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double dp = g_random_double_range(0, M_PI*2); + double dr = g_random_double_range(0, radius); + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) * Geom::Point(cos(dp)*dr, sin(dp)*dr); + item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation())); + did = true; + } + } + + } else if (mode == TWEAK_MODE_SCALE) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + double scale = 1 + (reverse? force : -force) * 0.05 * (cos(M_PI * x) + 1); + item->scale_rel(Geom::Scale(scale, scale)); + did = true; + } + } + + } else if (mode == TWEAK_MODE_ROTATE) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + double angle = (reverse? force : -force) * 0.05 * (cos(M_PI * x) + 1) * M_PI; + angle *= -selection->desktop()->yaxisdir(); + item->rotate_rel(Geom::Rotate(angle)); + did = true; + } + } + + } else if (mode == TWEAK_MODE_MORELESS) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + double prob = force * 0.5 * (cos(M_PI * x) + 1); + double chance = g_random_double_range(0, 1); + if (chance <= prob) { + if (reverse) { // delete + item->deleteObject(true, true); + } else { // duplicate + SPDocument *doc = item->document; + Inkscape::XML::Document* xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *old_repr = item->getRepr(); + SPObject *old_obj = doc->getObjectByRepr(old_repr); + Inkscape::XML::Node *parent = old_repr->parent(); + Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc); + parent->appendChild(copy); + SPObject *new_obj = doc->getObjectByRepr(copy); + if (selection->includes(old_obj)) { + selection->add(new_obj); + } + Inkscape::GC::release(copy); + } + did = true; + } + } + } + + } else if (dynamic_cast<SPPath *>(item) || dynamic_cast<SPShape *>(item)) { + + Inkscape::XML::Node *newrepr = nullptr; + gint pos = 0; + Inkscape::XML::Node *parent = nullptr; + char const *id = nullptr; + if (!dynamic_cast<SPPath *>(item)) { + newrepr = sp_selected_item_to_curved_repr(item, 0); + if (!newrepr) { + return false; + } + + // remember the position of the item + pos = item->getRepr()->position(); + // remember parent + parent = item->getRepr()->parent(); + // remember id + id = item->getRepr()->attribute("id"); + } + + // skip those paths whose bboxes are entirely out of reach with our radius + Geom::OptRect bbox = item->documentVisualBounds(); + if (bbox) { + bbox->expandBy(radius); + if (!bbox->contains(p)) { + return false; + } + } + + Path *orig = Path_for_item(item, false); + if (orig == nullptr) { + return false; + } + + Path *res = new Path; + res->SetBackData(false); + + Shape *theShape = new Shape; + Shape *theRes = new Shape; + Geom::Affine i2doc(item->i2doc_affine()); + + orig->ConvertWithBackData((0.08 - (0.07 * fidelity)) / i2doc.descrim()); // default 0.059 + orig->Fill(theShape, 0); + + SPCSSAttr *css = sp_repr_css_attr(item->getRepr(), "style"); + gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr); + if (val && strcmp(val, "nonzero") == 0) { + theRes->ConvertToShape(theShape, fill_nonZero); + } else if (val && strcmp(val, "evenodd") == 0) { + theRes->ConvertToShape(theShape, fill_oddEven); + } else { + theRes->ConvertToShape(theShape, fill_nonZero); + } + + if (Geom::L2(vector) != 0) { + vector = 1/Geom::L2(vector) * vector; + } + + bool did_this = false; + if (mode == TWEAK_MODE_SHRINK_GROW) { + if (theShape->MakeTweak(tweak_mode_grow, theRes, + reverse? force : -force, + join_straight, 4.0, + true, p, Geom::Point(0,0), radius, &i2doc) == 0) // 0 means the shape was actually changed + did_this = true; + } else if (mode == TWEAK_MODE_ATTRACT_REPEL) { + if (theShape->MakeTweak(tweak_mode_repel, theRes, + reverse? force : -force, + join_straight, 4.0, + true, p, Geom::Point(0,0), radius, &i2doc) == 0) + did_this = true; + } else if (mode == TWEAK_MODE_PUSH) { + if (theShape->MakeTweak(tweak_mode_push, theRes, + 1.0, + join_straight, 4.0, + true, p, force*2*vector, radius, &i2doc) == 0) + did_this = true; + } else if (mode == TWEAK_MODE_ROUGHEN) { + if (theShape->MakeTweak(tweak_mode_roughen, theRes, + force, + join_straight, 4.0, + true, p, Geom::Point(0,0), radius, &i2doc) == 0) + did_this = true; + } + + // the rest only makes sense if we actually changed the path + if (did_this) { + theRes->ConvertToShape(theShape, fill_positive); + + res->Reset(); + theRes->ConvertToForme(res); + + double th_max = (0.6 - 0.59*sqrt(fidelity)) / i2doc.descrim(); + double threshold = MAX(th_max, th_max*force); + res->ConvertEvenLines(threshold); + res->Simplify(threshold / (selection->desktop()->current_zoom())); + + if (newrepr) { // converting to path, need to replace the repr + bool is_selected = selection->includes(item); + if (is_selected) { + selection->remove(item); + } + + // It's going to resurrect, so we delete without notifying listeners. + item->deleteObject(false); + + // restore id + newrepr->setAttribute("id", id); + // add the new repr to the parent + // move to the saved position + parent->addChildAtPos(newrepr, pos); + + if (is_selected) + selection->add(newrepr); + } + + if (res->descr_cmd.size() > 1) { + gchar *str = res->svg_dump_path(); + if (newrepr) { + newrepr->setAttribute("d", str); + } else { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem && lpeitem->hasPathEffectRecursive()) { + item->setAttribute("inkscape:original-d", str); + } else { + item->setAttribute("d", str); + } + } + g_free(str); + } else { + // TODO: if there's 0 or 1 node left, delete this path altogether + } + + if (newrepr) { + Inkscape::GC::release(newrepr); + newrepr = nullptr; + } + } + + delete theShape; + delete theRes; + delete orig; + delete res; + + if (did_this) { + did = true; + } + } + + } + + return did; +} + + static void +tweak_colorpaint (float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l) +{ + float rgb_g[3]; + + if (!do_h || !do_s || !do_l) { + float hsl_g[3]; + SPColor::rgb_to_hsl_floatv (hsl_g, SP_RGBA32_R_F(goal), SP_RGBA32_G_F(goal), SP_RGBA32_B_F(goal)); + float hsl_c[3]; + SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]); + if (!do_h) { + hsl_g[0] = hsl_c[0]; + } + if (!do_s) { + hsl_g[1] = hsl_c[1]; + } + if (!do_l) { + hsl_g[2] = hsl_c[2]; + } + SPColor::hsl_to_rgb_floatv (rgb_g, hsl_g[0], hsl_g[1], hsl_g[2]); + } else { + rgb_g[0] = SP_RGBA32_R_F(goal); + rgb_g[1] = SP_RGBA32_G_F(goal); + rgb_g[2] = SP_RGBA32_B_F(goal); + } + + for (int i = 0; i < 3; i++) { + double d = rgb_g[i] - color[i]; + color[i] += d * force; + } +} + + static void +tweak_colorjitter (float *color, double force, bool do_h, bool do_s, bool do_l) +{ + float hsl_c[3]; + SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]); + + if (do_h) { + hsl_c[0] += g_random_double_range(-0.5, 0.5) * force; + if (hsl_c[0] > 1) { + hsl_c[0] -= 1; + } + if (hsl_c[0] < 0) { + hsl_c[0] += 1; + } + } + if (do_s) { + hsl_c[1] += g_random_double_range(-hsl_c[1], 1 - hsl_c[1]) * force; + } + if (do_l) { + hsl_c[2] += g_random_double_range(-hsl_c[2], 1 - hsl_c[2]) * force; + } + + SPColor::hsl_to_rgb_floatv (color, hsl_c[0], hsl_c[1], hsl_c[2]); +} + + static void +tweak_color (guint mode, float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l) +{ + if (mode == TWEAK_MODE_COLORPAINT) { + tweak_colorpaint (color, goal, force, do_h, do_s, do_l); + } else if (mode == TWEAK_MODE_COLORJITTER) { + tweak_colorjitter (color, force, do_h, do_s, do_l); + } +} + + static void +tweak_opacity (guint mode, SPIScale24 *style_opacity, double opacity_goal, double force) +{ + double opacity = SP_SCALE24_TO_FLOAT (style_opacity->value); + + if (mode == TWEAK_MODE_COLORPAINT) { + double d = opacity_goal - opacity; + opacity += d * force; + } else if (mode == TWEAK_MODE_COLORJITTER) { + opacity += g_random_double_range(-opacity, 1 - opacity) * force; + } + + style_opacity->value = SP_SCALE24_FROM_FLOAT(opacity); +} + + + static double +tweak_profile (double dist, double radius) +{ + if (radius == 0) { + return 0; + } + double x = dist / radius; + double alpha = 1; + if (x >= 1) { + return 0; + } else if (x <= 0) { + return 1; + } else { + return (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5); + } +} + +static void tweak_colors_in_gradient(SPItem *item, Inkscape::PaintTarget fill_or_stroke, + guint32 const rgb_goal, Geom::Point p_w, double radius, double force, guint mode, + bool do_h, bool do_s, bool do_l, bool /*do_o*/) +{ + SPGradient *gradient = getGradient(item, fill_or_stroke); + + if (!gradient || !dynamic_cast<SPGradient *>(gradient)) { + return; + } + + Geom::Affine i2d (item->i2doc_affine ()); + Geom::Point p = p_w * i2d.inverse(); + p *= (gradient->gradientTransform).inverse(); + // now p is in gradient's original coordinates + + SPLinearGradient *lg = dynamic_cast<SPLinearGradient *>(gradient); + SPRadialGradient *rg = dynamic_cast<SPRadialGradient *>(gradient); + if (lg || rg) { + + double pos = 0; + double r = 0; + + if (lg) { + Geom::Point p1(lg->x1.computed, lg->y1.computed); + Geom::Point p2(lg->x2.computed, lg->y2.computed); + Geom::Point pdiff(p2 - p1); + double vl = Geom::L2(pdiff); + + // This is the matrix which moves and rotates the gradient line + // so it's oriented along the X axis: + Geom::Affine norm = Geom::Affine(Geom::Translate(-p1)) * + Geom::Affine(Geom::Rotate(-atan2(pdiff[Geom::Y], pdiff[Geom::X]))); + + // Transform the mouse point by it to find out its projection onto the gradient line: + Geom::Point pnorm = p * norm; + + // Scale its X coordinate to match the length of the gradient line: + pos = pnorm[Geom::X] / vl; + // Calculate radius in length-of-gradient-line units + r = radius / vl; + + } + if (rg) { + Geom::Point c (rg->cx.computed, rg->cy.computed); + pos = Geom::L2(p - c) / rg->r.computed; + r = radius / rg->r.computed; + } + + // Normalize pos to 0..1, taking into account gradient spread: + double pos_e = pos; + if (gradient->getSpread() == SP_GRADIENT_SPREAD_PAD) { + if (pos > 1) { + pos_e = 1; + } + if (pos < 0) { + pos_e = 0; + } + } else if (gradient->getSpread() == SP_GRADIENT_SPREAD_REPEAT) { + if (pos > 1 || pos < 0) { + pos_e = pos - floor(pos); + } + } else if (gradient->getSpread() == SP_GRADIENT_SPREAD_REFLECT) { + if (pos > 1 || pos < 0) { + bool odd = ((int)(floor(pos)) % 2 == 1); + pos_e = pos - floor(pos); + if (odd) { + pos_e = 1 - pos_e; + } + } + } + + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary(gradient, false); + + double offset_l = 0; + double offset_h = 0; + SPObject *child_prev = nullptr; + for (auto& child: vector->children) { + SPStop *stop = dynamic_cast<SPStop *>(&child); + if (!stop) { + continue; + } + + offset_h = stop->offset; + + if (child_prev) { + SPStop *prevStop = dynamic_cast<SPStop *>(child_prev); + g_assert(prevStop != nullptr); + + if (offset_h - offset_l > r && pos_e >= offset_l && pos_e <= offset_h) { + // the summit falls in this interstop, and the radius is small, + // so it only affects the ends of this interstop; + // distribute the force between the two endstops so that they + // get all the painting even if they are not touched by the brush + tweak_color (mode, stop->getColor().v.c, rgb_goal, + force * (pos_e - offset_l) / (offset_h - offset_l), + do_h, do_s, do_l); + tweak_color(mode, prevStop->getColor().v.c, rgb_goal, + force * (offset_h - pos_e) / (offset_h - offset_l), + do_h, do_s, do_l); + stop->updateRepr(); + child_prev->updateRepr(); + break; + } else { + // wide brush, may affect more than 2 stops, + // paint each stop by the force from the profile curve + if (offset_l <= pos_e && offset_l > pos_e - r) { + tweak_color(mode, prevStop->getColor().v.c, rgb_goal, + force * tweak_profile (fabs (pos_e - offset_l), r), + do_h, do_s, do_l); + child_prev->updateRepr(); + } + + if (offset_h >= pos_e && offset_h < pos_e + r) { + tweak_color (mode, stop->getColor().v.c, rgb_goal, + force * tweak_profile (fabs (pos_e - offset_h), r), + do_h, do_s, do_l); + stop->updateRepr(); + } + } + } + + offset_l = offset_h; + child_prev = &child; + } + } else { + // Mesh + SPMeshGradient *mg = dynamic_cast<SPMeshGradient *>(gradient); + if (mg) { + SPMeshGradient *mg_array = dynamic_cast<SPMeshGradient *>(mg->getArray()); + SPMeshNodeArray *array = &(mg_array->array); + // Every third node is a corner node + for( unsigned i=0; i < array->nodes.size(); i+=3 ) { + for( unsigned j=0; j < array->nodes[i].size(); j+=3 ) { + SPStop *stop = array->nodes[i][j]->stop; + double distance = Geom::L2(Geom::Point(p - array->nodes[i][j]->p)); + tweak_color (mode, stop->getColor().v.c, rgb_goal, + force * tweak_profile (distance, radius), do_h, do_s, do_l); + stop->updateRepr(); + } + } + } + } +} + + static bool +sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point, + guint32 fill_goal, bool do_fill, + guint32 stroke_goal, bool do_stroke, + float opacity_goal, bool do_opacity, + bool do_blur, bool reverse, + Geom::Point p, double radius, double force, + bool do_h, bool do_s, bool do_l, bool do_o) +{ + bool did = false; + + if (dynamic_cast<SPGroup *>(item)) { + for (auto& child: item->children) { + SPItem *childItem = dynamic_cast<SPItem *>(&child); + if (childItem) { + if (sp_tweak_color_recursive (mode, childItem, item_at_point, + fill_goal, do_fill, + stroke_goal, do_stroke, + opacity_goal, do_opacity, + do_blur, reverse, + p, radius, force, do_h, do_s, do_l, do_o)) { + did = true; + } + } + } + + } else { + SPStyle *style = item->style; + if (!style) { + return false; + } + Geom::OptRect bbox = item->documentGeometricBounds(); + if (!bbox) { + return false; + } + + Geom::Rect brush(p - Geom::Point(radius, radius), p + Geom::Point(radius, radius)); + + Geom::Point center = bbox->midpoint(); + double this_force; + + // if item == item_at_point, use max force + if (item == item_at_point) { + this_force = force; + // else if no overlap of bbox and brush box, skip: + } else if (!bbox->intersects(brush)) { + return false; + //TODO: + // else if object > 1.5 brush: test 4/8/16 points in the brush on hitting the object, choose max + //} else if (bbox->maxExtent() > 3 * radius) { + //} + // else if object > 0.5 brush: test 4 corners of bbox and center on being in the brush, choose max + // else if still smaller, then check only the object center: + } else { + this_force = force * tweak_profile (Geom::L2 (p - center), radius); + } + + if (this_force > 0.002) { + + if (do_blur) { + Geom::OptRect bbox = item->documentGeometricBounds(); + if (!bbox) { + return did; + } + + double blur_now = 0; + Geom::Affine i2dt = item->i2dt_affine (); + if (style->filter.set && style->getFilter()) { + //cycle through filter primitives + for (auto& primitive_obj: style->getFilter()->children) { + SPFilterPrimitive *primitive = dynamic_cast<SPFilterPrimitive *>(&primitive_obj); + if (primitive) { + //if primitive is gaussianblur + SPGaussianBlur * spblur = dynamic_cast<SPGaussianBlur *>(primitive); + if (spblur) { + float num = spblur->stdDeviation.getNumber(); + blur_now += num * i2dt.descrim(); // sum all blurs in the filter + } + } + } + } + double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; + blur_now = blur_now / perimeter; + + double blur_new; + if (reverse) { + blur_new = blur_now - 0.06 * force; + } else { + blur_new = blur_now + 0.06 * force; + } + if (blur_new < 0.0005 && blur_new < blur_now) { + blur_new = 0; + } + if (blur_new == 0) { + remove_filter(item, false); + } else { + double radius = blur_new * perimeter; + SPFilter *filter = modify_filter_gaussian_blur_from_item(item->document, item, radius); + sp_style_set_property_url(item, "filter", filter, false); + } + return true; // do not do colors, blur is a separate mode + } + + if (do_fill) { + if (style->fill.isPaintserver()) { + tweak_colors_in_gradient(item, Inkscape::FOR_FILL, fill_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o); + did = true; + } else if (style->fill.isColor()) { + tweak_color (mode, style->fill.value.color.v.c, fill_goal, this_force, do_h, do_s, do_l); + item->updateRepr(); + did = true; + } + } + if (do_stroke) { + if (style->stroke.isPaintserver()) { + tweak_colors_in_gradient(item, Inkscape::FOR_STROKE, stroke_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o); + did = true; + } else if (style->stroke.isColor()) { + tweak_color (mode, style->stroke.value.color.v.c, stroke_goal, this_force, do_h, do_s, do_l); + item->updateRepr(); + did = true; + } + } + if (do_opacity && do_o) { + tweak_opacity (mode, &style->opacity, opacity_goal, this_force); + } + } +} + +return did; +} + + + static bool +sp_tweak_dilate (TweakTool *tc, Geom::Point event_p, Geom::Point p, Geom::Point vector, bool reverse) +{ + SPDesktop *desktop = tc->getDesktop(); + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + return false; + } + + bool did = false; + double radius = get_dilate_radius(tc); + + SPItem *item_at_point = tc->getDesktop()->getItemAtPoint(event_p, TRUE); + + bool do_fill = false, do_stroke = false, do_opacity = false; + guint32 fill_goal = sp_desktop_get_color_tool(desktop, "/tools/tweak", true, &do_fill); + guint32 stroke_goal = sp_desktop_get_color_tool(desktop, "/tools/tweak", false, &do_stroke); + double opacity_goal = sp_desktop_get_master_opacity_tool(desktop, "/tools/tweak", &do_opacity); + if (reverse) { +#if 0 + // HSL inversion + float hsv[3]; + float rgb[3]; + SPColor::rgb_to_hsv_floatv (hsv, + SP_RGBA32_R_F(fill_goal), + SP_RGBA32_G_F(fill_goal), + SP_RGBA32_B_F(fill_goal)); + SPColor::hsv_to_rgb_floatv (rgb, hsv[0]<.5? hsv[0]+.5 : hsv[0]-.5, 1 - hsv[1], 1 - hsv[2]); + fill_goal = SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1); + SPColor::rgb_to_hsv_floatv (hsv, + SP_RGBA32_R_F(stroke_goal), + SP_RGBA32_G_F(stroke_goal), + SP_RGBA32_B_F(stroke_goal)); + SPColor::hsv_to_rgb_floatv (rgb, hsv[0]<.5? hsv[0]+.5 : hsv[0]-.5, 1 - hsv[1], 1 - hsv[2]); + stroke_goal = SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1); +#else + // RGB inversion + fill_goal = SP_RGBA32_U_COMPOSE( + (255 - SP_RGBA32_R_U(fill_goal)), + (255 - SP_RGBA32_G_U(fill_goal)), + (255 - SP_RGBA32_B_U(fill_goal)), + (255 - SP_RGBA32_A_U(fill_goal))); + stroke_goal = SP_RGBA32_U_COMPOSE( + (255 - SP_RGBA32_R_U(stroke_goal)), + (255 - SP_RGBA32_G_U(stroke_goal)), + (255 - SP_RGBA32_B_U(stroke_goal)), + (255 - SP_RGBA32_A_U(stroke_goal))); +#endif + opacity_goal = 1 - opacity_goal; + } + + double path_force = get_path_force(tc); + if (radius == 0 || path_force == 0) { + return false; + } + double move_force = get_move_force(tc); + double color_force = MIN(sqrt(path_force)/20.0, 1); + + // auto items= selection->items(); + std::vector<SPItem*> items(selection->items().begin(), selection->items().end()); + for(auto item : items){ + if (is_color_mode (tc->mode)) { + if (do_fill || do_stroke || do_opacity) { + if (sp_tweak_color_recursive (tc->mode, item, item_at_point, + fill_goal, do_fill, + stroke_goal, do_stroke, + opacity_goal, do_opacity, + tc->mode == TWEAK_MODE_BLUR, reverse, + p, radius, color_force, tc->do_h, tc->do_s, tc->do_l, tc->do_o)) { + did = true; + } + } + } else if (is_transform_mode(tc->mode)) { + if (sp_tweak_dilate_recursive (selection, item, p, vector, tc->mode, radius, move_force, tc->fidelity, reverse)) { + did = true; + } + } else { + if (sp_tweak_dilate_recursive (selection, item, p, vector, tc->mode, radius, path_force, tc->fidelity, reverse)) { + did = true; + } + } + } + + return did; +} + + static void +sp_tweak_update_area (TweakTool *tc) +{ + double radius = get_dilate_radius(tc); + Geom::Affine const sm (Geom::Scale(radius, radius) * Geom::Translate(tc->getDesktop()->point())); + + Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); // Unit circle centered at origin. + path *= sm; + tc->dilate_area->set_bpath(path); + tc->dilate_area->show(); +} + + static void +sp_tweak_switch_mode (TweakTool *tc, gint mode, bool with_shift) +{ + auto tb = dynamic_cast<UI::Toolbar::TweakToolbar*>(tc->getDesktop()->get_toolbar_by_name("TweakToolbar")); + + if(tb) { + tb->set_mode(mode); + } else { + std::cerr << "Could not access Tweak toolbar" << std::endl; + } + + // need to set explicitly, because the prefs may not have changed by the previous + tc->mode = mode; + tc->update_cursor(with_shift); +} + + static void +sp_tweak_switch_mode_temporarily (TweakTool *tc, gint mode, bool with_shift) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // Juggling about so that prefs have the old value but tc->mode and the button show new mode: + gint now_mode = prefs->getInt("/tools/tweak/mode", 0); + + auto tb = dynamic_cast<UI::Toolbar::TweakToolbar*>(tc->getDesktop()->get_toolbar_by_name("TweakToolbar")); + + if(tb) { + tb->set_mode(mode); + } else { + std::cerr << "Could not access Tweak toolbar" << std::endl; + } + + // button has changed prefs, restore + prefs->setInt("/tools/tweak/mode", now_mode); + // changing prefs changed tc->mode, restore back : + tc->mode = mode; + tc->update_cursor(with_shift); +} + +bool TweakTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_ENTER_NOTIFY: + dilate_area->show(); + break; + case GDK_LEAVE_NOTIFY: + dilate_area->hide(); + break; + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { + return TRUE; + } + + Geom::Point const button_w(event->button.x, + event->button.y); + Geom::Point const button_dt(_desktop->w2d(button_w)); + this->last_push = _desktop->dt2doc(button_dt); + + sp_tweak_extinput(this, event); + + this->is_drawing = true; + this->is_dilating = true; + this->has_dilated = false; + + ret = TRUE; + } + break; + case GDK_MOTION_NOTIFY: + { + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + Geom::Point motion_doc(_desktop->dt2doc(motion_dt)); + sp_tweak_extinput(this, event); + + // draw the dilating cursor + double radius = get_dilate_radius(this); + Geom::Affine const sm(Geom::Scale(radius, radius) * Geom::Translate(_desktop->w2d(motion_w))); + Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); // Unit circle centered at origin. + path *= sm; + dilate_area->set_bpath(path); + dilate_area->show(); + + guint num = 0; + if (!_desktop->selection->isEmpty()) { + num = (guint)boost::distance(_desktop->selection->items()); + } + if (num == 0) { + this->message_context->flash(Inkscape::ERROR_MESSAGE, _("<b>Nothing selected!</b> Select objects to tweak.")); + } + + // dilating: + if (this->is_drawing && ( event->motion.state & GDK_BUTTON1_MASK )) { + sp_tweak_dilate (this, motion_w, motion_doc, motion_doc - this->last_push, event->button.state & GDK_SHIFT_MASK? true : false); + //this->last_push = motion_doc; + this->has_dilated = true; + // it's slow, so prevent clogging up with events + gobble_motion_events(GDK_BUTTON1_MASK); + return TRUE; + } + + } + break; + case GDK_BUTTON_RELEASE: + { + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + + this->is_drawing = false; + + if (this->is_dilating && event->button.button == 1) { + if (!this->has_dilated) { + // if we did not rub, do a light tap + this->pressure = 0.03; + sp_tweak_dilate(this, motion_w, _desktop->dt2doc(motion_dt), Geom::Point(0, 0), MOD__SHIFT(event)); + } + this->is_dilating = false; + this->has_dilated = false; + Glib::ustring text; + switch (this->mode) { + case TWEAK_MODE_MOVE: + text = _("Move tweak"); + break; + case TWEAK_MODE_MOVE_IN_OUT: + text = _("Move in/out tweak"); + break; + case TWEAK_MODE_MOVE_JITTER: + text = _("Move jitter tweak"); + break; + case TWEAK_MODE_SCALE: + text = _("Scale tweak"); + break; + case TWEAK_MODE_ROTATE: + text = _("Rotate tweak"); + break; + case TWEAK_MODE_MORELESS: + text = _("Duplicate/delete tweak"); + break; + case TWEAK_MODE_PUSH: + text = _("Push path tweak"); + break; + case TWEAK_MODE_SHRINK_GROW: + text = _("Shrink/grow path tweak"); + break; + case TWEAK_MODE_ATTRACT_REPEL: + text = _("Attract/repel path tweak"); + break; + case TWEAK_MODE_ROUGHEN: + text = _("Roughen path tweak"); + break; + case TWEAK_MODE_COLORPAINT: + text = _("Color paint tweak"); + break; + case TWEAK_MODE_COLORJITTER: + text = _("Color jitter tweak"); + break; + case TWEAK_MODE_BLUR: + text = _("Blur tweak"); + break; + } + DocumentUndo::done(_desktop->getDocument(), text.c_str(), INKSCAPE_ICON("tool-tweak")); + } + break; + } + case GDK_KEY_PRESS: + { + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_m: + case GDK_KEY_M: + case GDK_KEY_0: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_MOVE, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_i: + case GDK_KEY_I: + case GDK_KEY_1: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_MOVE_IN_OUT, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_z: + case GDK_KEY_Z: + case GDK_KEY_2: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_MOVE_JITTER, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_less: + case GDK_KEY_comma: + case GDK_KEY_greater: + case GDK_KEY_period: + case GDK_KEY_3: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_SCALE, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_bracketright: + case GDK_KEY_bracketleft: + case GDK_KEY_4: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_ROTATE, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_d: + case GDK_KEY_D: + case GDK_KEY_5: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_MORELESS, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_p: + case GDK_KEY_P: + case GDK_KEY_6: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_PUSH, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_s: + case GDK_KEY_S: + case GDK_KEY_7: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_SHRINK_GROW, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_a: + case GDK_KEY_A: + case GDK_KEY_8: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_ATTRACT_REPEL, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_r: + case GDK_KEY_R: + case GDK_KEY_9: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_ROUGHEN, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_c: + case GDK_KEY_C: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_COLORPAINT, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_j: + case GDK_KEY_J: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_COLORJITTER, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_b: + case GDK_KEY_B: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_BLUR, MOD__SHIFT(event)); + ret = TRUE; + } + break; + + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + if (!MOD__CTRL_ONLY(event)) { + this->force += 0.05; + if (this->force > 1.0) { + this->force = 1.0; + } + _desktop->setToolboxAdjustmentValue("tweak-force", this->force * 100); + ret = TRUE; + } + break; + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + if (!MOD__CTRL_ONLY(event)) { + this->force -= 0.05; + if (this->force < 0.0) { + this->force = 0.0; + } + _desktop->setToolboxAdjustmentValue("tweak-force", this->force * 100); + ret = TRUE; + } + break; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (!MOD__CTRL_ONLY(event)) { + this->width += 0.01; + if (this->width > 1.0) { + this->width = 1.0; + } + _desktop->setToolboxAdjustmentValue ("tweak-width", this->width * 100); // the same spinbutton is for alt+x + sp_tweak_update_area(this); + ret = TRUE; + } + break; + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (!MOD__CTRL_ONLY(event)) { + this->width -= 0.01; + if (this->width < 0.01) { + this->width = 0.01; + } + _desktop->setToolboxAdjustmentValue("tweak-width", this->width * 100); + sp_tweak_update_area(this); + ret = TRUE; + } + break; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + this->width = 0.01; + _desktop->setToolboxAdjustmentValue("tweak-width", this->width * 100); + sp_tweak_update_area(this); + ret = TRUE; + break; + case GDK_KEY_End: + case GDK_KEY_KP_End: + this->width = 1.0; + _desktop->setToolboxAdjustmentValue("tweak-width", this->width * 100); + sp_tweak_update_area(this); + ret = TRUE; + break; + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + _desktop->setToolboxFocusTo("tweak-width"); + ret = TRUE; + } + break; + + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->update_cursor(true); + break; + + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + sp_tweak_switch_mode_temporarily(this, TWEAK_MODE_SHRINK_GROW, MOD__SHIFT(event)); + break; + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + } + case GDK_KEY_RELEASE: { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->update_cursor(false); + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + sp_tweak_switch_mode (this, prefs->getInt("/tools/tweak/mode"), MOD__SHIFT(event)); + this->message_context->clear(); + break; + default: + sp_tweak_switch_mode (this, prefs->getInt("/tools/tweak/mode"), MOD__SHIFT(event)); + break; + } + } + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + +/* + 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/tools/tweak-tool.h b/src/ui/tools/tweak-tool.h new file mode 100644 index 0000000..d185666 --- /dev/null +++ b/src/ui/tools/tweak-tool.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_TWEAK_CONTEXT_H__ +#define __SP_TWEAK_CONTEXT_H__ + +/* + * tweaking paths without node editing + * + * Authors: + * bulia byak + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/tool-base.h" +#include <2geom/point.h> + +#define SAMPLING_SIZE 8 /* fixme: ?? */ + +#define TC_MIN_PRESSURE 0.0 +#define TC_MAX_PRESSURE 1.0 +#define TC_DEFAULT_PRESSURE 0.35 + +namespace Inkscape { + +class CanvasItemBpath; + +namespace UI { +namespace Tools { + +enum { + TWEAK_MODE_MOVE, + TWEAK_MODE_MOVE_IN_OUT, + TWEAK_MODE_MOVE_JITTER, + TWEAK_MODE_SCALE, + TWEAK_MODE_ROTATE, + TWEAK_MODE_MORELESS, + TWEAK_MODE_PUSH, + TWEAK_MODE_SHRINK_GROW, + TWEAK_MODE_ATTRACT_REPEL, + TWEAK_MODE_ROUGHEN, + TWEAK_MODE_COLORPAINT, + TWEAK_MODE_COLORJITTER, + TWEAK_MODE_BLUR +}; + +class TweakTool : public ToolBase { +public: + TweakTool(SPDesktop *desktop); + ~TweakTool() override; + + /* extended input data */ + gdouble pressure; + + /* attributes */ + bool dragging; /* mouse state: mouse is dragging */ + bool usepressure; + bool usetilt; + + double width; + double force; + double fidelity; + + gint mode; + + bool is_drawing; + + bool is_dilating; + bool has_dilated; + Geom::Point last_push; + Inkscape::CanvasItemBpath *dilate_area; + + bool do_h; + bool do_s; + bool do_l; + bool do_o; + + sigc::connection style_set_connection; + + void set(const Inkscape::Preferences::Entry &val) override; + bool root_handler(GdkEvent *event) override; + void update_cursor(bool with_shift); + +private: + bool set_style(const SPCSSAttr *css); +}; + +} +} +} + +#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/tools/zoom-tool.cpp b/src/ui/tools/zoom-tool.cpp new file mode 100644 index 0000000..dec3a52 --- /dev/null +++ b/src/ui/tools/zoom-tool.cpp @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Handy zooming tool + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 1999-2002 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include <gdk/gdkkeysyms.h> + +#include "zoom-tool.h" + +#include "desktop.h" +#include "rubberband.h" +#include "selection-chemistry.h" + +#include "include/macros.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +ZoomTool::ZoomTool(SPDesktop *desktop) + : ToolBase(desktop, "/tools/zoom", "zoom-in.svg") + , escaped(false) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/zoom/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/zoom/gradientdrag")) { + this->enableGrDrag(); + } +} + +ZoomTool::~ZoomTool() +{ + this->enableGrDrag(false); + ungrabCanvasEvents(); +} + +bool ZoomTool::root_handler(GdkEvent* event) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + double const zoom_inc = prefs->getDoubleLimited("/options/zoomincrement/value", M_SQRT2, 1.01, 10); + + bool ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + { + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point const button_dt(_desktop->w2d(button_w)); + + if (event->button.button == 1) { + // save drag origin + xp = (gint) event->button.x; + yp = (gint) event->button.y; + within_tolerance = true; + + Inkscape::Rubberband::get(_desktop)->start(_desktop, button_dt); + + escaped = false; + + ret = true; + } else if (event->button.button == 3) { + double const zoom_rel( (event->button.state & GDK_SHIFT_MASK) + ? zoom_inc + : 1 / zoom_inc ); + + _desktop->zoom_relative(button_dt, zoom_rel); + ret = true; + } + + grabCanvasEvents(Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK ); + break; + } + + case GDK_MOTION_NOTIFY: + if ((event->motion.state & GDK_BUTTON1_MASK)) { + ret = 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; + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(_desktop->w2d(motion_w)); + Inkscape::Rubberband::get(_desktop)->move(motion_dt); + gobble_motion_events(GDK_BUTTON1_MASK); + } + break; + + case GDK_BUTTON_RELEASE: + { + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point const button_dt(_desktop->w2d(button_w)); + + if ( event->button.button == 1) { + Geom::OptRect const b = Inkscape::Rubberband::get(_desktop)->getRectangle(); + + if (b && !within_tolerance && !(GDK_SHIFT_MASK & event->button.state) ) { + _desktop->set_display_area(*b, 10); + } else if (!escaped) { + double const zoom_rel( (event->button.state & GDK_SHIFT_MASK) + ? 1 / zoom_inc + : zoom_inc ); + + _desktop->zoom_relative(button_dt, zoom_rel); + } + + ret = true; + } + + Inkscape::Rubberband::get(_desktop)->stop(); + + ungrabCanvasEvents(); + + xp = yp = 0; + escaped = false; + break; + } + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Escape: + if (!Inkscape::Rubberband::get(_desktop)->is_started()) { + Inkscape::SelectionHelper::selectNone(_desktop); + } + + Inkscape::Rubberband::get(_desktop)->stop(); + xp = yp = 0; + escaped = true; + ret = true; + break; + + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // prevent the zoom field from activation + if (!MOD__CTRL_ONLY(event)) + ret = true; + break; + + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->set_cursor("zoom-out.svg"); + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + case GDK_KEY_RELEASE: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->set_cursor("zoom-in.svg"); + break; + default: + break; + } + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + +/* + 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/tools/zoom-tool.h b/src/ui/tools/zoom-tool.h new file mode 100644 index 0000000..d7b97ad --- /dev/null +++ b/src/ui/tools/zoom-tool.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_ZOOM_CONTEXT_H__ +#define __SP_ZOOM_CONTEXT_H__ + +/* + * Handy zooming tool + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * + * Copyright (C) 1999-2002 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/tool-base.h" + +#define SP_ZOOM_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ZoomTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_ZOOM_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::ZoomTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ZoomTool : public ToolBase { +public: + ZoomTool(SPDesktop *desktop); + ~ZoomTool() override; + + bool root_handler(GdkEvent *event) override; + +private: + bool escaped; +}; + +} +} +} + +#endif |