summaryrefslogtreecommitdiffstats
path: root/src/ui/tools
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/ui/tools
parentInitial commit. (diff)
downloadinkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz
inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/ui/tools')
-rw-r--r--src/ui/tools/arc-tool.cpp454
-rw-r--r--src/ui/tools/arc-tool.h76
-rw-r--r--src/ui/tools/booleans-builder.cpp271
-rw-r--r--src/ui/tools/booleans-builder.h90
-rw-r--r--src/ui/tools/booleans-subitems.cpp356
-rw-r--r--src/ui/tools/booleans-subitems.h71
-rw-r--r--src/ui/tools/booleans-tool.cpp255
-rw-r--r--src/ui/tools/booleans-tool.h70
-rw-r--r--src/ui/tools/box3d-tool.cpp570
-rw-r--r--src/ui/tools/box3d-tool.h100
-rw-r--r--src/ui/tools/calligraphic-tool.cpp1162
-rw-r--r--src/ui/tools/calligraphic-tool.h101
-rw-r--r--src/ui/tools/connector-tool.cpp1324
-rw-r--r--src/ui/tools/connector-tool.h191
-rw-r--r--src/ui/tools/dropper-tool.cpp394
-rw-r--r--src/ui/tools/dropper-tool.h94
-rw-r--r--src/ui/tools/dynamic-base.cpp141
-rw-r--r--src/ui/tools/dynamic-base.h136
-rw-r--r--src/ui/tools/eraser-tool.cpp1413
-rw-r--r--src/ui/tools/eraser-tool.h155
-rw-r--r--src/ui/tools/flood-tool.cpp1230
-rw-r--r--src/ui/tools/flood-tool.h67
-rw-r--r--src/ui/tools/freehand-base.cpp1007
-rw-r--r--src/ui/tools/freehand-base.h161
-rw-r--r--src/ui/tools/gradient-tool.cpp822
-rw-r--r--src/ui/tools/gradient-tool.h77
-rw-r--r--src/ui/tools/lpe-tool.cpp460
-rw-r--r--src/ui/tools/lpe-tool.h98
-rw-r--r--src/ui/tools/marker-tool.cpp302
-rw-r--r--src/ui/tools/marker-tool.h50
-rw-r--r--src/ui/tools/measure-tool.cpp1445
-rw-r--r--src/ui/tools/measure-tool.h127
-rw-r--r--src/ui/tools/mesh-tool.cpp970
-rw-r--r--src/ui/tools/mesh-tool.h86
-rw-r--r--src/ui/tools/node-tool.cpp861
-rw-r--r--src/ui/tools/node-tool.h115
-rw-r--r--src/ui/tools/pages-tool.cpp668
-rw-r--r--src/ui/tools/pages-tool.h99
-rw-r--r--src/ui/tools/pen-tool.cpp2043
-rw-r--r--src/ui/tools/pen-tool.h175
-rw-r--r--src/ui/tools/pencil-tool.cpp1177
-rw-r--r--src/ui/tools/pencil-tool.h101
-rw-r--r--src/ui/tools/rect-tool.cpp464
-rw-r--r--src/ui/tools/rect-tool.h60
-rw-r--r--src/ui/tools/select-tool.cpp1148
-rw-r--r--src/ui/tools/select-tool.h81
-rw-r--r--src/ui/tools/spiral-tool.cpp409
-rw-r--r--src/ui/tools/spiral-tool.h61
-rw-r--r--src/ui/tools/spray-tool.cpp1528
-rw-r--r--src/ui/tools/spray-tool.h148
-rw-r--r--src/ui/tools/star-tool.cpp428
-rw-r--r--src/ui/tools/star-tool.h72
-rw-r--r--src/ui/tools/text-tool.cpp1905
-rw-r--r--src/ui/tools/text-tool.h122
-rw-r--r--src/ui/tools/tool-base.cpp1712
-rw-r--r--src/ui/tools/tool-base.h262
-rw-r--r--src/ui/tools/tweak-tool.cpp1482
-rw-r--r--src/ui/tools/tweak-tool.h107
-rw-r--r--src/ui/tools/zoom-tool.cpp214
-rw-r--r--src/ui/tools/zoom-tool.h41
60 files changed, 29809 insertions, 0 deletions
diff --git a/src/ui/tools/arc-tool.cpp b/src/ui/tools/arc-tool.cpp
new file mode 100644
index 0000000..cd1c036
--- /dev/null
+++ b/src/ui/tools/arc-tool.cpp
@@ -0,0 +1,454 @@
+// 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"
+
+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 (arc) {
+ // 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 if (!selection->includes(this->item_to_select)) {
+ 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 = cast<SPGenericEllipse>(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 &#215; %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 &#215; %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 &#215; %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 &#215; %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/booleans-builder.cpp b/src/ui/tools/booleans-builder.cpp
new file mode 100644
index 0000000..8fd027a
--- /dev/null
+++ b/src/ui/tools/booleans-builder.cpp
@@ -0,0 +1,271 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Boolean tool shape builder.
+ *
+ *//*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "booleans-builder.h"
+
+#include "actions/actions-undo-document.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/canvas-item-bpath.h"
+#include "object/object-set.h"
+#include "object/sp-item.h"
+#include "object/sp-namedview.h"
+#include "style.h"
+#include "ui/widget/canvas.h"
+#include "svg/svg.h"
+
+namespace Inkscape {
+
+static constexpr std::array<uint32_t, 6> fill_lite = {0x00000055, 0x0291ffff, 0x8eceffff, 0x0291ffff, 0xf299d6ff, 0xff0db3ff};
+static constexpr std::array<uint32_t, 6> fill_dark = {0xffffff55, 0x0291ffff, 0x8eceffff, 0x0291ffff, 0xf299d6ff, 0xff0db3ff};
+
+BooleanBuilder::BooleanBuilder(ObjectSet *set, bool flatten)
+ : _set(set)
+{
+ // Current state of all the items
+ _work_items = (flatten ? SubItem::build_flatten : SubItem::build_mosaic)(set->items_vector());
+
+ auto root = _set->desktop()->getCanvas()->get_canvas_item_root();
+ _group = make_canvasitem<CanvasItemGroup>(root);
+
+ auto nv = _set->desktop()->getNamedView();
+ desk_modified_connection = nv->connectModified([=](SPObject *obj, guint flags) {
+ redraw_items();
+ });
+ redraw_items();
+}
+
+BooleanBuilder::~BooleanBuilder() = default;
+
+/**
+ * Control the visual appearence of this particular bpath
+ */
+void BooleanBuilder::redraw_item(CanvasItemBpath &bpath, bool selected, TaskType task)
+{
+ int i = (int)task * 2 + (int)selected;
+ bpath.set_fill(_dark ? fill_dark[i] : fill_lite[i], SP_WIND_RULE_POSITIVE);
+ bpath.set_stroke(task == TaskType::NONE ? 0x000000dd : 0xffffffff);
+ bpath.set_stroke_width(task == TaskType::NONE ? 1.0 : 3.0);
+}
+
+/**
+ * Update to visuals with the latest subitem list.
+ */
+void BooleanBuilder::redraw_items()
+{
+ auto nv = _set->desktop()->getNamedView();
+ _dark = SP_RGBA32_LUMINANCE(nv->desk_color) < 100;
+
+ _screen_items.clear();
+
+ for (auto &subitem : _work_items) {
+ // Construct BPath from each subitem!
+ auto bpath = make_canvasitem<Inkscape::CanvasItemBpath>(_group.get(), subitem->get_pathv(), false);
+ redraw_item(*bpath, subitem->getSelected(), TaskType::NONE);
+ _screen_items.push_back({ subitem, std::move(bpath), true });
+ }
+
+ // Selectively handle the undo actions being enabled / disabled
+ enable_undo_actions(_set->document(), _undo.size(), _redo.size());
+}
+
+ItemPair *BooleanBuilder::get_item(const Geom::Point &point)
+{
+ for (auto &pair : _screen_items) {
+ if (pair.vis->contains(point, 2.0))
+ return &pair;
+ }
+ return nullptr;
+}
+
+/**
+ * Highlight any shape under the mouse at this point.
+ */
+bool BooleanBuilder::highlight(const Geom::Point &point, bool add)
+{
+ if (has_task())
+ return true;
+
+ bool done = false;
+ for (auto &si : _screen_items) {
+ bool hover = !done && si.vis->contains(point, 2.0);
+ redraw_item(*si.vis, si.work->getSelected(), hover ? (add ? TaskType::ADD : TaskType::DELETE) : TaskType::NONE);
+ if (hover)
+ si.vis->raise_to_top();
+ done = done || hover;
+ }
+ return done;
+}
+
+/**
+ * Select the shape under the cursor
+ */
+bool BooleanBuilder::task_select(const Geom::Point &point, bool add_task)
+{
+ if (has_task())
+ task_cancel();
+ if (auto si = get_item(point)) {
+ _add_task = add_task;
+ _work_task = std::make_shared<SubItem>(*si->work);
+ _work_task->setSelected(true);
+ _screen_task = make_canvasitem<Inkscape::CanvasItemBpath>(_group.get(), _work_task->get_pathv(), false);
+ redraw_item(*_screen_task, true, add_task ? TaskType::ADD : TaskType::DELETE);
+ si->vis->hide();
+ si->visible = false;
+ redraw_item(*si->vis, false, TaskType::NONE);
+ return true;
+ }
+ return false;
+}
+
+bool BooleanBuilder::task_add(const Geom::Point &point)
+{
+ if (!has_task())
+ return false;
+ if (auto si = get_item(point)) {
+ // Invisible items are already processed.
+ if (si->visible) {
+ si->vis->hide();
+ si->visible = false;
+ *_work_task += *si->work;
+ _screen_task->set_bpath(_work_task->get_pathv(), false);
+ return true;
+ }
+ }
+ return false;
+}
+
+void BooleanBuilder::task_cancel()
+{
+ _work_task.reset();
+ _screen_task.reset();
+ for (auto &si : _screen_items) {
+ si.vis->show();
+ si.visible = true;
+ }
+}
+
+void BooleanBuilder::task_commit()
+{
+ if (!has_task())
+ return;
+
+ // Manage undo/redo
+ _undo.emplace_back(std::move(_work_items));
+ _redo.clear();
+
+ // A. Delete all items from _work_items that aren't visible
+ _work_items.clear();
+ for (auto &si : _screen_items) {
+ if (si.visible) {
+ _work_items.emplace_back(si.work);
+ }
+ }
+ if (_add_task) {
+ // B. Add _work_task to _work_items for union tasks
+ _work_items.emplace_back(std::move(_work_task));
+ }
+
+ // C. Reset everything
+ redraw_items();
+ _work_task.reset();
+ _screen_task.reset();
+}
+
+/**
+ * Commit the changes to the document (finish)
+ */
+std::vector<SPObject *> BooleanBuilder::shape_commit(bool all)
+{
+ std::vector<SPObject *> ret;
+ auto doc = _set->document();
+ auto items = _set->items_vector();
+
+ // Only commit anything if we have changes, return selection.
+ if (!has_changes() && !all) {
+ ret.insert(ret.begin(), items.begin(), items.end());
+ return ret;
+ }
+
+ // Count number of selected items.
+ int selected = 0;
+ for (auto const &subitem : _work_items) {
+ selected += (int)subitem->getSelected();
+ }
+
+ for (auto const &subitem : _work_items) {
+ // Either this object is selected, or no objects are selected at all.
+ if (!subitem->getSelected() && selected)
+ continue;
+ auto item = subitem->get_item();
+ auto style = subitem->getStyle();
+ // For the rare occasion the user generates from a hole (no item)
+ if (!item) {
+ item = *items.begin();
+ style = item->style;
+ }
+ if (!item) {
+ g_warning("Can't generate itemless object in boolean-builder.");
+ continue;
+ }
+ auto parent = cast<SPItem>(item->parent);
+
+ Inkscape::XML::Node *repr = doc->getReprDoc()->createElement("svg:path");
+ repr->setAttribute("d", sp_svg_write_path(subitem->get_pathv() * parent->dt2i_affine()));
+ repr->setAttribute("style", style->writeIfDiff(parent->style));
+ parent->getRepr()->addChild(repr, item->getRepr());
+ ret.emplace_back(doc->getObjectByRepr(repr));
+ }
+ _work_items.clear();
+
+ for (auto item : items) {
+ sp_object_ref(item, nullptr);
+ item->deleteObject(true, true);
+ sp_object_unref(item, nullptr);
+ }
+ return ret;
+}
+
+void BooleanBuilder::undo()
+{
+ if (_undo.empty())
+ return;
+
+ // Cancel any task;
+ task_cancel();
+
+ // Shuffle the undo stack
+ _redo.emplace_back(std::move(_work_items));
+ _work_items = std::move(_undo.back());
+ _undo.pop_back();
+
+ // Redraw the screen items
+ redraw_items();
+}
+
+void BooleanBuilder::redo()
+{
+ if (_redo.empty())
+ return;
+
+ // Cancel any task;
+ task_cancel();
+
+ // Shuffle the undo stack
+ _undo.emplace_back(std::move(_work_items));
+ _work_items = std::move(_redo.back());
+ _redo.pop_back();
+
+ // Redraw the screen items
+ redraw_items();
+}
+
+} // namespace Inkscape
diff --git a/src/ui/tools/booleans-builder.h b/src/ui/tools/booleans-builder.h
new file mode 100644
index 0000000..33f4404
--- /dev/null
+++ b/src/ui/tools/booleans-builder.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ *//*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_TOOLS_BOOLEANS_BUILDER_H
+#define INKSCAPE_UI_TOOLS_BOOLEANS_BUILDER_H
+
+#include <vector>
+#include <optional>
+#include "helper/auto-connection.h"
+
+#include "booleans-subitems.h"
+#include "helper/auto-connection.h"
+#include "display/control/canvas-item-ptr.h"
+
+class SPDesktop;
+class SPDocument;
+class SPObject;
+
+namespace Inkscape {
+
+class CanvasItemGroup;
+class CanvasItemBpath;
+class ObjectSet;
+
+using VisualItem = CanvasItemPtr<CanvasItemBpath>;
+struct ItemPair
+{
+ WorkItem work;
+ VisualItem vis;
+ bool visible;
+};
+
+enum class TaskType
+{
+ NONE,
+ ADD,
+ DELETE
+};
+
+class BooleanBuilder
+{
+public:
+ BooleanBuilder(ObjectSet *obj, bool flatten = false);
+ ~BooleanBuilder();
+
+ void undo();
+ void redo();
+
+ std::vector<SPObject *> shape_commit(bool all = false);
+ ItemPair *get_item(const Geom::Point &point);
+ bool task_select(const Geom::Point &point, bool add_task = true);
+ bool task_add(const Geom::Point &point);
+ void task_cancel();
+ void task_commit();
+ bool has_items() const { return !_work_items.empty(); }
+ bool has_task() const { return (bool)_work_task; }
+ bool has_changes() const { return !_undo.empty(); }
+ bool highlight(const Geom::Point &point, bool add_task = true);
+
+private:
+ ObjectSet *_set;
+ CanvasItemPtr<CanvasItemGroup> _group;
+
+ std::vector<WorkItem> _work_items;
+ std::vector<ItemPair> _screen_items;
+ WorkItem _work_task;
+ VisualItem _screen_task;
+ bool _add_task;
+ bool _dark = false;
+
+ // Lists of _work_items which can be brought back.
+ std::vector<std::vector<WorkItem>> _undo;
+ std::vector<std::vector<WorkItem>> _redo;
+
+ auto_connection desk_modified_connection;
+
+ void redraw_item(CanvasItemBpath &bpath, bool selected, TaskType task);
+ void redraw_items();
+};
+
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_TOOLS_BOOLEANS_BUILDER_H
diff --git a/src/ui/tools/booleans-subitems.cpp b/src/ui/tools/booleans-subitems.cpp
new file mode 100644
index 0000000..29309f8
--- /dev/null
+++ b/src/ui/tools/booleans-subitems.cpp
@@ -0,0 +1,356 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * SubItem controls each fractured piece and links it to its original items.
+ *
+ *//*
+ * Authors:
+ * Martin Owens
+ * PBS
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <numeric>
+#include <utility>
+#include <random>
+
+#include <boost/range/adaptor/reversed.hpp>
+
+#include "booleans-subitems.h"
+#include "helper/geom-pathstroke.h"
+#include "livarot/LivarotDefs.h"
+#include "livarot/Shape.h"
+#include "object/sp-shape.h"
+#include "object/sp-text.h"
+#include "object/sp-use.h"
+#include "object/sp-image.h"
+#include "path/path-boolop.h"
+#include "style.h"
+
+namespace Inkscape {
+
+// Todo: (Wishlist) Remove this function when no longer necessary to remove boolops artifacts.
+static Geom::PathVector clean_pathvector(Geom::PathVector &&pathv)
+{
+ Geom::PathVector result;
+
+ for (auto &path : pathv) {
+ if (path.closed() && !is_path_empty(path)) {
+ result.push_back(std::move(path));
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Union operator, merges two subitems when requested by the user
+ * The left hand side will retain priority for the resulting style
+ * so you should be mindful of how you merge these shapes.
+ */
+SubItem &SubItem::operator+=(SubItem const &other)
+{
+ _paths = sp_pathvector_boolop(_paths, other._paths, bool_op_union, fill_nonZero, fill_nonZero, true);
+ sp_flatten(_paths, fill_nonZero);
+ _paths = clean_pathvector(std::move(_paths));
+ return *this;
+}
+
+using ExtractPathvectorsResult = std::vector<std::pair<Geom::PathVector, SPStyle*>>;
+
+static void extract_pathvectors_recursive(SPItem *item, ExtractPathvectorsResult &result, Geom::Affine const &transform)
+{
+ if (is<SPGroup>(item)) {
+ for (auto &child : item->children | boost::adaptors::reversed) {
+ if (auto child_item = cast<SPItem>(&child)) {
+ extract_pathvectors_recursive(child_item, result, child_item->transform * transform);
+ }
+ }
+ } else if (auto img = cast<SPImage>(item)) {
+ result.emplace_back(img->get_curve()->get_pathvector() * transform, item->style);
+ } else if (auto shape = cast<SPShape>(item)) {
+ if (auto curve = shape->curve()) {
+ result.emplace_back(curve->get_pathvector() * transform, item->style);
+ }
+ } else if (auto text = cast<SPText>(item)) {
+ result.emplace_back(text->getNormalizedBpath().get_pathvector() * transform, item->style);
+ } else if (auto use = cast<SPUse>(item)) {
+ if (use->child) {
+ extract_pathvectors_recursive(use->child, result, use->child->transform * Geom::Translate(use->x.computed, use->y.computed) * transform);
+ }
+ }
+}
+
+// Return all pathvectors found within an item, along with their styles, sorted top-to-bottom.
+static ExtractPathvectorsResult extract_pathvectors(SPItem *item)
+{
+ ExtractPathvectorsResult result;
+ extract_pathvectors_recursive(item, result, item->i2dt_affine());
+ return result;
+}
+
+static FillRule sp_to_livarot(SPWindRule fillrule)
+{
+ return fillrule == SP_WIND_RULE_NONZERO ? fill_nonZero : fill_oddEven;
+}
+
+static double diameter(Geom::PathVector const &path)
+{
+ auto rect = path.boundsExact();
+ if (!rect) {
+ return 1;
+ }
+ return std::hypot(rect->width(), rect->height());
+}
+
+// Cut the given pathvector along the lines into several smaller pathvectors.
+static std::vector<Geom::PathVector> improved_cut(Geom::PathVector const &pathv, Geom::PathVector const &lines)
+{
+ Path patha;
+ patha.LoadPathVector(pathv);
+ patha.ConvertWithBackData(diameter(pathv) * 1e-3);
+
+ Path pathb;
+ pathb.LoadPathVector(lines);
+ pathb.ConvertWithBackData(diameter(lines) * 1e-3);
+
+ Shape shapea;
+ {
+ Shape tmp;
+ patha.Fill(&tmp, 0);
+ shapea.ConvertToShape(&tmp);
+ }
+
+ Shape shapeb;
+ {
+ Shape tmp;
+ bool isline = pathb.pts.size() == 2 && pathb.pts[0].isMoveTo && !pathb.pts[1].isMoveTo;
+ pathb.Fill(&tmp, 1, false, isline);
+ shapeb.ConvertToShape(&tmp, fill_justDont);
+ }
+
+ Shape shape;
+ shape.Booleen(&shapeb, &shapea, bool_op_cut, 1);
+
+ Path path;
+ int num_nesting = 0;
+ int *nesting = nullptr;
+ int *conts = nullptr;
+ {
+ path.SetBackData(false);
+ Path *paths[2] = { &patha, &pathb };
+ shape.ConvertToFormeNested(&path, 2, paths, 1, num_nesting, nesting, conts);
+ }
+
+ int num_paths;
+ auto paths = path.SubPathsWithNesting(num_paths, false, num_nesting, nesting, conts);
+
+ std::vector<Geom::PathVector> result;
+
+ for (int i = 0; i < num_paths; i++) {
+ result.emplace_back(paths[i]->MakePathVector());
+ }
+
+ g_free(paths);
+ g_free(conts);
+ g_free(nesting);
+
+ return result;
+}
+
+/**
+ * Take a list of items and fracture into a list of SubItems ready for
+ * use inside the booleans interactive tool.
+ */
+WorkItems SubItem::build_mosaic(std::vector<SPItem*> &&items)
+{
+ // Sort so that topmost items come first.
+ std::sort(items.begin(), items.end(), [] (auto a, auto b) {
+ return sp_object_compare_position_bool(b, a);
+ });
+
+ // Extract all individual pathvectors within the collection of items,
+ // keeping track of their associated item and style, again sorted topmost-first.
+ using AugmentedItem = std::tuple<Geom::PathVector, SPItem*, SPStyle*>;
+ std::vector<AugmentedItem> augmented;
+
+ for (auto item : items) {
+ // Get the correctly-transformed pathvectors, together with their corresponding styles.
+ auto extracted = extract_pathvectors(item);
+
+ // Append to the list of augmented items.
+ for (auto &[pathv, style] : extracted) {
+ augmented.emplace_back(std::move(pathv), item, style);
+ }
+ }
+
+ // Compute a slightly expanded bounding box, collect together all lines, and cut the former by the latter.
+ Geom::OptRect bounds;
+ Geom::PathVector lines;
+
+ for (auto &[pathv, item, style] : augmented) {
+ bounds |= pathv.boundsExact();
+ for (auto &path : pathv) {
+ lines.push_back(path);
+ }
+ }
+
+ if (!bounds) {
+ return {};
+ }
+
+ constexpr double expansion = 10.0;
+ bounds->expandBy(expansion);
+
+ auto bounds_pathv = Geom::PathVector(Geom::Path(*bounds));
+ auto pieces = improved_cut(bounds_pathv, lines);
+
+ // Construct the SubItems, attempting to guess the corresponding augmented item for each piece.
+ WorkItems result;
+
+ auto gen = std::default_random_engine(std::random_device()());
+ auto ranf = [&] { return std::uniform_real_distribution()(gen); };
+ auto randpt = [&] { return Geom::Point(ranf(), ranf()); };
+
+ for (auto &piece : pieces) {
+ // Skip the big enclosing piece that is touching the outer boundary.
+ if (auto rect = piece.boundsExact()) {
+ if ( Geom::are_near(rect->top(), bounds->top(), expansion / 2)
+ || Geom::are_near(rect->bottom(), bounds->bottom(), expansion / 2)
+ || Geom::are_near(rect->left(), bounds->left(), expansion / 2)
+ || Geom::are_near(rect->right(), bounds->right(), expansion / 2))
+ {
+ continue;
+ }
+ }
+
+ // Remove junk paths that are open and/or tiny.
+ for (auto it = piece.begin(); it != piece.end(); ) {
+ if (!it->closed() || is_path_empty(*it)) {
+ it = piece.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ // Skip empty pathvectors.
+ if (piece.empty()) {
+ continue;
+ }
+
+ // Determine the corresponding augmented item.
+ // Fixme: (Wishlist) This is done unreliably and hackily, but livarot/2geom seemingly offer no alternative.
+ std::unordered_map<AugmentedItem*, int> hits;
+
+ auto rect = piece.boundsExact();
+
+ auto add_hit = [&] (Geom::Point const &pt) {
+ // Find an augmented item containing the point.
+ for (auto &aug : augmented) {
+ auto &[pathv, item, style] = aug;
+ auto fill_rule = style->fill_rule.computed;
+ auto winding = pathv.winding(pt);
+ if (fill_rule == SP_WIND_RULE_NONZERO ? winding : winding % 2) {
+ hits[&aug]++;
+ return;
+ }
+ }
+
+ // If none exists, register a background hit.
+ hits[nullptr]++;
+ };
+
+ for (int total_hits = 0, patience = 1000; total_hits < 20 && patience > 0; patience--) {
+ // Attempt to generate a point strictly inside the piece.
+ auto pt = rect->min() + randpt() * rect->dimensions();
+ if (piece.winding(pt)) {
+ add_hit(pt);
+ total_hits++;
+ }
+ }
+
+ // Pick the augmented item with the most hits.
+ AugmentedItem *found = nullptr;
+ int max_hits = 0;
+
+ for (auto &[a, h] : hits) {
+ if (h > max_hits) {
+ max_hits = h;
+ found = a;
+ }
+ }
+
+ // Add the SubItem.
+ auto item = found ? std::get<1>(*found) : nullptr;
+ auto style = found ? std::get<2>(*found) : nullptr;
+ result.emplace_back(std::make_shared<SubItem>(std::move(piece), item, style));
+ }
+
+ return result;
+}
+
+/**
+ * Take a list of items and flatten into a list of SubItems.
+ */
+WorkItems SubItem::build_flatten(std::vector<SPItem*> &&items)
+{
+ // Sort so that topmost items come first.
+ std::sort(items.begin(), items.end(), [] (auto a, auto b) {
+ return sp_object_compare_position_bool(b, a);
+ });
+
+ WorkItems result;
+ Geom::PathVector unioned;
+
+ for (auto item : items) {
+ // Get the correctly-transformed pathvectors, together with their corresponding styles.
+ auto extracted = extract_pathvectors(item);
+
+ for (auto &[pathv, style] : extracted) {
+ // Remove lines.
+ for (auto it = pathv.begin(); it != pathv.end(); ) {
+ if (!it->closed()) {
+ it = pathv.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ // Skip pathvectors that are just lines.
+ if (pathv.empty()) {
+ continue;
+ }
+
+ // Flatten the remaining pathvector according to its fill rule.
+ auto fillrule = style->fill_rule.computed;
+ sp_flatten(pathv, sp_to_livarot(fillrule));
+
+ // Remove the union so far from the shape, then add the shape to the union so far.
+ Geom::PathVector uniq;
+
+ if (unioned.empty()) {
+ uniq = pathv;
+ unioned = std::move(pathv);
+ } else {
+ uniq = sp_pathvector_boolop(unioned, pathv, bool_op_diff, fill_nonZero, fill_nonZero, true);
+ unioned = sp_pathvector_boolop(unioned, pathv, bool_op_union, fill_nonZero, fill_nonZero, true);
+ }
+
+ // Add the new SubItem.
+ result.emplace_back(std::make_shared<SubItem>(std::move(uniq), item, style));
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Return true if this subitem contains the give point.
+ */
+bool SubItem::contains(Geom::Point const &pt) const
+{
+ return _paths.winding(pt) % 2 != 0;
+}
+
+} // namespace Inkscape
diff --git a/src/ui/tools/booleans-subitems.h b/src/ui/tools/booleans-subitems.h
new file mode 100644
index 0000000..bbb12f2
--- /dev/null
+++ b/src/ui/tools/booleans-subitems.h
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ *
+ *//*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_TOOLS_BOOLEANS_SUBITEMS_H
+#define INKSCAPE_UI_TOOLS_BOOLEANS_SUBITEMS_H
+
+#include <2geom/pathvector.h>
+#include <vector>
+#include <functional>
+
+class SPItem;
+class SPStyle;
+
+namespace Inkscape {
+
+class SubItem;
+using WorkItem = std::shared_ptr<SubItem>;
+using WorkItems = std::vector<WorkItem>;
+
+/**
+ * When an item is broken, each broken part is represented by
+ * the SubItem class. This class hold information such as the
+ * original items it originated from and the paths that it
+ * consists of.
+ **/
+class SubItem
+{
+public:
+
+ SubItem(Geom::PathVector paths, SPItem *item, SPStyle *style)
+ : _paths(std::move(paths))
+ , _item(item)
+ , _style(style)
+ {}
+
+ SubItem(const SubItem &copy)
+ : SubItem(copy._paths, copy._item, copy._style)
+ {}
+
+ SubItem &operator+=(const SubItem &other);
+
+ bool contains(const Geom::Point &pt) const;
+
+ const Geom::PathVector &get_pathv() const { return _paths; }
+ SPItem *get_item() const { return _item; }
+ SPStyle *getStyle() const { return _style; }
+
+ static WorkItems build_mosaic(std::vector<SPItem*> &&items);
+ static WorkItems build_flatten(std::vector<SPItem*> &&items);
+
+ bool getSelected() const { return _selected; }
+ void setSelected(bool selected) { _selected = selected; }
+
+private:
+ Geom::PathVector _paths;
+ SPItem *_item;
+ SPStyle *_style;
+ bool _selected = false;
+};
+
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_TOOLS_BOOLEANS_SUBITEMS_H
diff --git a/src/ui/tools/booleans-tool.cpp b/src/ui/tools/booleans-tool.cpp
new file mode 100644
index 0000000..2b3a82d
--- /dev/null
+++ b/src/ui/tools/booleans-tool.cpp
@@ -0,0 +1,255 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+
+#include "actions/actions-tools.h" // set_active_tool()
+#include "ui/tools/booleans-tool.h"
+#include "ui/tools/booleans-builder.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/canvas-item-drawing.h"
+
+#include "desktop.h"
+#include "document.h"
+#include "document-undo.h"
+#include "event-log.h"
+#include "include/macros.h"
+#include "selection.h"
+#include "ui/icon-names.h"
+#include "ui/modifiers.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Modifiers::Modifier;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+InteractiveBooleansTool::InteractiveBooleansTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/booleans", "select.svg")
+{
+ to_commit = false;
+ change_mode(true);
+ update_status();
+ if (auto selection = desktop->getSelection()) {
+ desktop->setWaitingCursor();
+ boolean_builder = std::make_unique<BooleanBuilder>(selection);
+ desktop->clearWaitingCursor();
+
+ // Any changes to the selection cancel the shape building process
+ _sel_modified = selection->connectModified([=](Selection *sel, int) { shape_cancel(); });
+ _sel_changed = selection->connectChanged([=](Selection *sel) { shape_cancel(); });
+ }
+}
+
+InteractiveBooleansTool::~InteractiveBooleansTool()
+{
+ change_mode(false);
+ _sel_modified.disconnect();
+ _sel_changed.disconnect();
+}
+
+void InteractiveBooleansTool::change_mode(bool setup)
+{
+ _desktop->doc()->get_event_log()->updateUndoVerbs();
+ _desktop->getCanvasPagesBg()->set_visible(!setup);
+ _desktop->getCanvasPagesFg()->set_visible(!setup);
+ _desktop->getCanvasDrawing()->set_visible(!setup);
+}
+
+void InteractiveBooleansTool::switching_away(const std::string &new_tool)
+{
+ if (!new_tool.empty() && boolean_builder && new_tool == "/tools/select" || new_tool == "/tool/nodes") {
+ // Only forcefully commit if we have the user's explicit instruction to do so.
+ if (boolean_builder->has_changes() || to_commit) {
+ _desktop->getSelection()->setList(boolean_builder->shape_commit(true));
+ DocumentUndo::done(_desktop->doc(), "Built Shapes", INKSCAPE_ICON("draw-booleans"));
+ }
+ }
+}
+
+bool InteractiveBooleansTool::is_ready() const {
+ if (!boolean_builder || !boolean_builder->has_items()) {
+ if (_desktop->getSelection()->isEmpty()) {
+ _desktop->showNotice(_("You must select some objects to use the Shape Builder tool."), 5000);
+ } else {
+ _desktop->showNotice(_("The Shape Builder requires regular shapes to be selected."), 5000);
+ }
+ return false;
+ }
+ return true;
+}
+
+void InteractiveBooleansTool::set(const Inkscape::Preferences::Entry& val)
+{
+ Glib::ustring path = val.getEntryName();
+ if (path == "/tools/booleans/mode") {
+ update_status();
+ boolean_builder->task_cancel();
+ }
+}
+
+void InteractiveBooleansTool::shape_commit()
+{
+ to_commit = true;
+ // disconnect so we don't get canceled by accident.
+ _sel_modified.disconnect();
+ _sel_changed.disconnect();
+ set_active_tool(_desktop, "Select");
+}
+
+void InteractiveBooleansTool::shape_cancel()
+{
+ boolean_builder.reset();
+ set_active_tool(_desktop, "Select");
+}
+
+bool InteractiveBooleansTool::root_handler(GdkEvent* event)
+{
+ if (!boolean_builder)
+ return false;
+
+ bool ret = false;
+ bool add = should_add(event->button.state);
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ ret = event_button_press_handler(event);
+ break;
+ case GDK_BUTTON_RELEASE:
+ ret = event_button_release_handler(event);
+ break;
+ case GDK_KEY_PRESS:
+ ret = event_key_press_handler(event);
+ // no-break;
+ case GDK_KEY_RELEASE:
+ add = should_add(Modifiers::add_keyval(event->key.state, event->key.keyval, event->type == GDK_KEY_RELEASE));
+ break;
+ case GDK_MOTION_NOTIFY:
+ ret = event_motion_handler(event, add);
+ break;
+ }
+ if (!ret) {
+ set_cursor(add ? "cursor-union.svg" : "cursor-delete.svg");
+ update_status();
+ }
+ return ret || ToolBase::root_handler(event);
+}
+
+/**
+ * Returns true if the shape builder should add items,
+ * false if shape builder should delete items
+ */
+bool InteractiveBooleansTool::should_add(int state) const
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool pref = prefs->getInt("/tools/booleans/mode", 0) != 0;
+ auto modifier = Modifier::get(Modifiers::Type::BOOL_SHIFT);
+ return pref == modifier->active(state);
+}
+
+void InteractiveBooleansTool::update_status()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool pref = prefs->getInt("/tools/booleans/mode", 0) == 0;
+ auto modifier = Modifier::get(Modifiers::Type::BOOL_SHIFT);
+ message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ (pref ? "<b>Drag</b> over fragments to unite them. <b>Click</b> to create a segment. Hold <b>%s</b> to Subtract."
+ : "<b>Drag</b> over fragments to delete them. <b>Click</b> to delete a segment. Hold <b>%s</b> to Unite."),
+ modifier->get_label().c_str());
+}
+
+bool InteractiveBooleansTool::event_button_press_handler(GdkEvent *event)
+{
+ if (event->button.button == 1) {
+ Geom::Point const button_pt(event->button.x, event->button.y);
+ boolean_builder->task_select(button_pt, should_add(event->button.state));
+ return true;
+
+ } else if (event->button.button == 3) {
+ // right click; do not eat it so that right-click menu can appear, but cancel dragging
+ boolean_builder->task_cancel();
+ }
+
+ return false;
+}
+
+bool InteractiveBooleansTool::event_motion_handler(GdkEvent *event, bool add)
+{
+ Geom::Point const motion_pt(event->motion.x, event->motion.y);
+
+ if ((event->motion.state & GDK_BUTTON1_MASK)) {
+ if (boolean_builder->has_task()) {
+ return boolean_builder->task_add(motion_pt);
+ } else {
+ return boolean_builder->task_select(motion_pt, add);
+ }
+ } else {
+ return boolean_builder->highlight(motion_pt, add);
+ }
+
+ return false;
+}
+
+bool InteractiveBooleansTool::event_button_release_handler(GdkEvent *event)
+{
+ if (event->button.button == 1) {
+ boolean_builder->task_commit();
+ }
+ return true;
+}
+
+bool InteractiveBooleansTool::catch_undo(bool redo) {
+ if (redo) {
+ boolean_builder->redo();
+ } else {
+ boolean_builder->undo();
+ }
+ return true;
+}
+
+bool InteractiveBooleansTool::event_key_press_handler(GdkEvent *event)
+{
+ bool ret = false;
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Escape:
+ if (boolean_builder->has_task()) {
+ boolean_builder->task_cancel();
+ } else {
+ shape_cancel();
+ }
+ ret = true;
+ break;
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (boolean_builder->has_task()) {
+ boolean_builder->task_commit();
+ } else {
+ shape_commit();
+ }
+ ret = true;
+ break;
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (event->key.state & INK_GDK_PRIMARY_MASK) {
+ ret = catch_undo(event->key.state & GDK_SHIFT_MASK);
+ }
+ break;
+
+
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+} // namespace Tools
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/tools/booleans-tool.h b/src/ui/tools/booleans-tool.h
new file mode 100644
index 0000000..9eafb88
--- /dev/null
+++ b/src/ui/tools/booleans-tool.h
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * A tool for building shapes.
+ */
+/* Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_TOOLS_BOOLEANS_TOOL
+#define INKSCAPE_UI_TOOLS_BOOLEANS_TOOL
+
+#include "ui/tools/tool-base.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+class BooleanBuilder;
+
+namespace UI {
+namespace Tools {
+
+class InteractiveBooleansTool : public ToolBase {
+public:
+
+ InteractiveBooleansTool(SPDesktop *desktop);
+ ~InteractiveBooleansTool() override;
+
+ void switching_away(const std::string &new_tool) override;
+
+ // Preferences set
+ void set(const Inkscape::Preferences::Entry& val) override;
+
+ // Undo/redo catching
+ bool catch_undo(bool redo) override;
+
+ // Catch empty selections
+ bool is_ready() const override;
+
+ // Event functions
+ bool root_handler(GdkEvent* event) override;
+
+ void shape_commit();
+ void shape_cancel();
+private:
+ void update_status();
+ void change_mode(bool setup);
+ bool should_add(int state) const;
+
+ bool event_button_press_handler(GdkEvent* event);
+ bool event_button_release_handler(GdkEvent* event);
+ bool event_motion_handler(GdkEvent* event, bool add);
+ bool event_key_press_handler(GdkEvent* event);
+
+ std::unique_ptr<BooleanBuilder> boolean_builder;
+
+ sigc::connection _sel_modified;
+ sigc::connection _sel_changed;
+
+ bool to_commit = false;
+};
+
+} // namespace Tools
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_TOOLS_BOOLEANS_TOOL
diff --git a/src/ui/tools/box3d-tool.cpp b/src/ui/tools/box3d-tool.cpp
new file mode 100644
index 0000000..a9d972c
--- /dev/null
+++ b/src/ui/tools/box3d-tool.cpp
@@ -0,0 +1,570 @@
+// 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"
+
+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)
+{
+ auto defs = document->getDefs();
+
+ bool has_persp = false;
+ for (auto &child : defs->children) {
+ if (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 (!cur_persp) {
+ // Can happen if perspective is deleted while dragging, e.g. on document closure.
+ ret = true;
+ break;
+ }
+ 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->getSelection()->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..a75c2db
--- /dev/null
+++ b/src/ui/tools/box3d-tool.h
@@ -0,0 +1,100 @@
+// 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;
+}
+
+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..ed23158
--- /dev/null
+++ b/src/ui/tools/calligraphic-tool.cpp
@@ -0,0 +1,1162 @@
+// 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 "ui/widget/canvas.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;
+
+ currentshape = make_canvasitem<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 = make_canvasitem<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() = default;
+
+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();
+
+ // Get average color.
+ double R, G, B, A;
+ drawing->averageColor(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 */
+ segments.clear();
+
+ /* reset accumulated curve */
+ accumulated.reset();
+ clear_current();
+
+ 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;
+ }
+
+ accumulated.reset();
+
+ repr = nullptr;
+
+ /* initialize first point */
+ 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 && (is<SPShape>(selected) || is<SPText>(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(std::move(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(std::move(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(std::move(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(std::move(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 */
+ 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 */
+ accumulated.reset();
+
+ clear_current();
+ 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 */
+ currentshape->set_bpath(nullptr);
+
+ /* reset curve */
+ currentcurve.reset();
+ cal1.reset();
+ cal2.reset();
+
+ /* reset points */
+ npoints = 0;
+}
+
+void CalligraphicTool::set_to_accumulated(bool unionize, bool subtract) {
+ if (!accumulated.is_empty()) {
+ if (!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();
+ auto item = cast<SPItem>(layer->appendChildRepr(this->repr));
+ Inkscape::GC::release(this->repr);
+ item->transform = layer->i2doc_affine().inverse();
+ item->updateRepr();
+ }
+
+ Geom::PathVector pathv = accumulated.get_pathvector() * _desktop->dt2doc();
+ 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.
+ auto result = cast<SPItem>(_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 (
+ cal1.is_empty() ||
+ cal2.is_empty() ||
+ (cal1.get_segment_count() <= 0) ||
+ cal1.first_path()->closed()
+ ) {
+
+ cal1.reset();
+ cal2.reset();
+
+ return false; // failure
+ }
+
+ auto rev_cal2 = cal2.reversed();
+
+ if ((rev_cal2.get_segment_count() <= 0) || rev_cal2.first_path()->closed()) {
+ cal1.reset();
+ cal2.reset();
+
+ return false; // failure
+ }
+
+ Geom::Curve const * dc_cal1_firstseg = cal1.first_segment();
+ Geom::Curve const * rev_cal2_firstseg = rev_cal2.first_segment();
+ Geom::Curve const * dc_cal1_lastseg = cal1.last_segment();
+ Geom::Curve const * rev_cal2_lastseg = rev_cal2.last_segment();
+
+ accumulated.reset(); /* Is this required ?? */
+
+ accumulated.append(cal1);
+
+ add_cap(accumulated, dc_cal1_lastseg->finalPoint(), rev_cal2_firstseg->initialPoint(), cap_rounding);
+
+ accumulated.append(rev_cal2, true);
+
+ add_cap(accumulated, rev_cal2_lastseg->finalPoint(), dc_cal1_firstseg->initialPoint(), cap_rounding);
+
+ accumulated.closepath();
+
+ cal1.reset();
+ 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 ( cal1.is_empty() || cal2.is_empty() ) {
+ /* dc->npoints > 0 */
+ /* g_print("calligraphics(1|2) reset\n"); */
+ cal1.reset();
+ cal2.reset();
+
+ cal1.moveto(this->point1[0]);
+ 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) {
+ 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: dc->segments is always NULL at this point??
+ if (this->segments.empty()) { // first segment
+ add_cap(currentcurve, b2[0], b1[0], cap_rounding);
+ }
+ currentcurve.closepath();
+ currentshape->set_bpath(&currentcurve, true);
+ }
+
+ /* Current calligraphic */
+ 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]);
+ }
+ } 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++) {
+ cal1.lineto(this->point1[i]);
+ }
+ for (gint i = 1; i < this->npoints; i++) {
+ 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(!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_pathvector(), 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));
+
+ segments.emplace_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() {
+ currentcurve.reset();
+
+ currentcurve.moveto(this->point2[this->npoints-1]);
+
+ for (gint i = this->npoints-2; i >= 0; i--) {
+ currentcurve.lineto(this->point2[i]);
+ }
+
+ for (gint i = 0; i < this->npoints; i++) {
+ currentcurve.lineto(this->point1[i]);
+ }
+
+ if (this->npoints >= 2) {
+ add_cap(currentcurve, point1[npoints - 1], point2[npoints - 1], cap_rounding);
+ }
+
+ currentcurve.closepath();
+ currentshape->set_bpath(&currentcurve, 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..75b2a4a
--- /dev/null
+++ b/src/ui/tools/calligraphic-tool.h
@@ -0,0 +1,101 @@
+// 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 "display/control/canvas-item-ptr.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;
+ CanvasItemPtr<Inkscape::CanvasItemBpath> hatch_area;
+ 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..8b6f64a
--- /dev/null
+++ b/src/ui/tools/connector-tool.cpp
@@ -0,0 +1,1324 @@
+// 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 "xml/node.h"
+
+#include "svg/svg.h"
+
+namespace Inkscape::UI::Tools {
+
+void CCToolShapeNodeObserver::notifyAttributeChanged(Inkscape::XML::Node &repr, GQuark name_, Util::ptr_shared, Util::ptr_shared)
+{
+ auto tool = static_cast<ConnectorTool*>(this);
+
+ auto const name = g_quark_to_string(name_);
+ // 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 == tool->active_shape_repr) {
+ // Active shape has moved. Clear active shape.
+ tool->cc_clear_active_shape();
+ } else if (&repr == tool->active_conn_repr) {
+ // The active conn has been moved.
+ // Set it again, which just sets new handle positions.
+ tool->cc_set_active_conn(tool->active_conn);
+ }
+ }
+}
+
+void CCToolLayerNodeObserver::notifyChildRemoved(Inkscape::XML::Node&, Inkscape::XML::Node &child, Inkscape::XML::Node*)
+{
+ auto tool = static_cast<ConnectorTool*>(this);
+
+ if (&child == tool->active_shape_repr) {
+ // The active shape has been deleted. Clear active shape.
+ tool->cc_clear_active_shape();
+ }
+}
+
+using Inkscape::DocumentUndo;
+
+static void cc_clear_active_knots(SPKnotList k);
+
+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;*/
+
+ConnectorTool::ConnectorTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/connector", "connector.svg")
+ , state {SP_CONNECTOR_CONTEXT_IDLE}
+{
+ 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 */
+ red_curve.emplace();
+
+ /* Create green curve */
+ green_curve.emplace();
+
+ // 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) {
+ this->active_shape_repr->removeObserver(shapeNodeObserver());
+ Inkscape::GC::release(this->active_shape_repr);
+ this->active_shape_repr = nullptr;
+
+ this->active_shape_layer_repr->removeObserver(layerNodeObserver());
+ 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) {
+ this->active_conn_repr->removeObserver(shapeNodeObserver());
+ 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(is<SPPath>(clickeditem));
+
+ m.setup(_desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.unSetup();
+
+ // Update the hidden path
+ auto i2d = clickeditem->i2dt_affine();
+ auto d2i = i2d.inverse();
+ auto path = cast<SPPath>(clickeditem);
+ auto curve = *path->curve();
+ if (clickedhandle == endpt_handle[0]) {
+ auto o = endpt_handle[1]->pos;
+ curve.stretch_endpoints(p * d2i, o * d2i);
+ } else {
+ auto o = endpt_handle[0]->pos;
+ curve.stretch_endpoints(o * d2i, p * d2i);
+ }
+ path->setCurve(std::move(curve));
+ sp_conn_reroute_path_immediate(path);
+
+ // Copy this to the temporary visible path
+ red_curve = path->curveForEdit()->transformed(i2d);
+ red_bpath->set_bpath(&*red_curve);
+
+ 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(cast<SPPath>(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.
+ red_curve = SPConnEndPair::createCurve(newConnRef, curvature);
+ red_curve->transform(_desktop->doc2dt());
+ red_bpath->set_bpath(&*red_curve, 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_optional<SPCurve>();
+ std::swap(c, green_curve);
+
+ red_curve->reset();
+ red_bpath->set_bpath(nullptr);
+
+ if (c->is_empty()) {
+ return;
+ }
+
+ _flushWhite(&*c);
+}
+
+
+/*
+ * 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->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 = cast<SPItem>(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(cast<SPPath>(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 = path->curveForEdit()->transformed(cc->clickeditem->i2dt_affine());
+ cc->red_bpath->set_bpath(&*cc->red_curve, 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) {
+ auto use = 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) {
+ this->active_shape_repr->removeObserver(shapeNodeObserver());
+ Inkscape::GC::release(this->active_shape_repr);
+
+ this->active_shape_layer_repr->removeObserver(layerNodeObserver());
+ 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);
+ this->active_shape_repr->addObserver(shapeNodeObserver());
+
+ this->active_shape_layer_repr = this->active_shape_repr->parent();
+ Inkscape::GC::anchor(this->active_shape_layer_repr);
+ this->active_shape_layer_repr->addObserver(layerNodeObserver());
+ }
+
+ 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
+ if (auto use = cast<SPUse>(item)) {
+ 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( is<SPPath>(item) );
+
+ const SPCurve *curve = cast<SPPath>(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) {
+ this->active_conn_repr->removeObserver(shapeNodeObserver());
+ 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);
+ this->active_conn_repr->addObserver(shapeNodeObserver());
+ }
+
+ 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 = cast<SPPath>(item)) {
+ SPCurve const *curve = path->curve();
+ if ( curve && !(curve->is_closed()) ) {
+ // Open paths are connectors.
+ return false;
+ }
+ } else if (is<SPText>(item) || is<SPFlowtext>(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 (auto path = cast<SPPath>(item)) {
+ bool closed = path->curveForEdit()->is_closed();
+ if (path->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);
+ }
+}
+
+} // 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/connector-tool.h b/src/ui/tools/connector-tool.h
new file mode 100644
index 0000000..f2271aa
--- /dev/null
+++ b/src/ui/tools/connector-tool.h
@@ -0,0 +1,191 @@
+// 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 <optional>
+#include <string>
+
+#include <2geom/point.h>
+#include <sigc++/connection.h>
+
+#include "display/curve.h"
+
+#include "ui/tools/tool-base.h"
+
+#include "xml/node-observer.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
+};
+
+using SPKnotList = std::map<SPKnot *, int>;
+
+namespace Inkscape::UI::Tools {
+
+class ConnectorTool;
+
+class CCToolShapeNodeObserver : public Inkscape::XML::NodeObserver
+{
+ friend class ConnectorTool;
+ ~CCToolShapeNodeObserver() override = default; // can only exist as a direct base of ConnectorTool
+
+ void notifyAttributeChanged(Inkscape::XML::Node &, GQuark, Util::ptr_shared, Util::ptr_shared) final;
+};
+
+class CCToolLayerNodeObserver : public Inkscape::XML::NodeObserver
+{
+ friend class ConnectorTool;
+ ~CCToolLayerNodeObserver() override = default; // can only exist as a direct base of ConnectorTool
+
+ void notifyChildRemoved(Inkscape::XML::Node &, Inkscape::XML::Node &, Inkscape::XML::Node *) final;
+};
+
+class ConnectorTool
+ : public ToolBase
+ , private CCToolShapeNodeObserver
+ , private CCToolLayerNodeObserver
+{
+public:
+ ConnectorTool(SPDesktop *desktop);
+ ~ConnectorTool() override;
+
+ Inkscape::Selection *selection{nullptr};
+ Geom::Point p[5];
+
+ /** \invar npoints in {0, 2}. */
+ gint npoints{0};
+ unsigned int state : 4;
+
+ // Red curve
+ Inkscape::CanvasItemBpath *red_bpath{nullptr};
+ std::optional<SPCurve> red_curve;
+ guint32 red_color{0xff00007f};
+
+ // Green curve
+ std::optional<SPCurve> green_curve;
+
+ // The new connector
+ SPItem *newconn{nullptr};
+ Avoid::ConnRef *newConnRef{nullptr};
+ gdouble curvature{0.0};
+ bool isOrthogonal{false};
+
+ // The active shape
+ SPItem *active_shape{nullptr};
+ Inkscape::XML::Node *active_shape_repr{nullptr};
+ Inkscape::XML::Node *active_shape_layer_repr{nullptr};
+
+ // Same as above, but for the active connector
+ SPItem *active_conn{nullptr};
+ Inkscape::XML::Node *active_conn_repr{nullptr};
+ sigc::connection sel_changed_connection;
+
+ // The activehandle
+ SPKnot *active_handle{nullptr};
+
+ // The selected handle, used in editing mode
+ SPKnot *selected_handle{nullptr};
+
+ SPItem *clickeditem{nullptr};
+ SPKnot *clickedhandle{nullptr};
+
+ SPKnotList knots;
+ SPKnot *endpt_handle[2]{};
+ sigc::connection endpt_handler_connection[2];
+ gchar *shref{nullptr};
+ gchar *sub_shref{nullptr};
+ gchar *ehref {nullptr};
+ gchar *sub_ehref{nullptr};
+
+ 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);
+
+ CCToolShapeNodeObserver &shapeNodeObserver() { return *this; }
+ CCToolLayerNodeObserver &layerNodeObserver() { return *this; }
+ friend CCToolShapeNodeObserver;
+ friend CCToolLayerNodeObserver;
+};
+
+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);
+
+} // namespace Inkscape::UI::Tools
+
+#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..a909df7
--- /dev/null
+++ b/src/ui/tools/dropper-tool.cpp
@@ -0,0 +1,394 @@
+// 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 = make_canvasitem<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();
+}
+
+/**
+ * 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(std::move(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();
+
+ // Get average color.
+ double R, G, B, A;
+ drawing->averageColor(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..5222ca3
--- /dev/null
+++ b/src/ui/tools/dropper-tool.h
@@ -0,0 +1,94 @@
+// 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 "display/control/canvas-item-ptr.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
+ CanvasItemPtr<CanvasItemBpath> area; ///< 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..5f9de6a
--- /dev/null
+++ b/src/ui/tools/dynamic-base.cpp
@@ -0,0 +1,141 @@
+// 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)
+ , 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() = default;
+
+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..46fa5fd
--- /dev/null
+++ b/src/ui/tools/dynamic-base.h
@@ -0,0 +1,136 @@
+// 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 "display/curve.h"
+#include "display/control/canvas-item-ptr.h"
+
+#include <optional>
+
+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 */
+ SPCurve accumulated;
+
+ /** canvas items for "committed" segments */
+ std::vector<CanvasItemPtr<CanvasItemBpath>> segments;
+
+ /** canvas item for red "leading" segment */
+ CanvasItemPtr<CanvasItemBpath> currentshape;
+
+ /** shape of red "leading" segment */
+ SPCurve currentcurve;
+
+ /** left edge of the stroke; combined to get accumulated */
+ SPCurve cal1;
+
+ /** right edge of the stroke; combined to get accumulated */
+ 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..e111533
--- /dev/null
+++ b/src/ui/tools/eraser-tool.cpp
@@ -0,0 +1,1413 @@
+// 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 "preferences.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 {
+
+EraserTool::EraserTool(SPDesktop *desktop)
+ : DynamicBase(desktop, "/tools/eraser", "eraser.svg")
+ , _break_apart{"/tools/eraser/break_apart", false}
+ , _mode_int{"/tools/eraser/mode", 1} // Cut mode is default
+{
+ currentshape = make_canvasitem<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
+
+ _mode_int.min = 0;
+ _mode_int.max = 2;
+ _updateMode();
+ _mode_int.action = [this]() { _updateMode(); };
+
+ enableSelectionCue();
+}
+
+EraserTool::~EraserTool() = default;
+
+/** Reads the current Eraser mode from Preferences and sets `mode` accordingly. */
+void EraserTool::_updateMode()
+{
+ int const mode_idx = _mode_int;
+ // 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();
+
+ segments.clear();
+
+ /* reset accumulated curve */
+ accumulated.reset();
+ _clearCurrent();
+ repr = nullptr;
+}
+
+bool EraserTool::root_handler(GdkEvent* event)
+{
+ bool ret = false;
+ 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);
+ segments.clear();
+
+ // Create eraser stroke shape
+ _fitAndSplit(true);
+ _accumulate();
+
+ // Perform the actual erase operation
+ SPDocument *document = _desktop->getDocument();
+ if (_doWork()) {
+ DocumentUndo::done(document, _("Draw eraser stroke"), INKSCAPE_ICON("draw-eraser"));
+ } else {
+ DocumentUndo::cancel(document);
+ }
+
+ /* 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;
+}
+
+/** Inserts the temporary red shape of the eraser stroke (the "acid") into the document.
+ * @return a pointer to the inserted item
+ */
+SPItem *EraserTool::_insertAcidIntoDocument(SPDocument *document)
+{
+ auto *top_layer = _desktop->layerManager().currentRoot();
+ auto *eraser_item = cast<SPItem>(top_layer->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+ eraser_item->updateRepr();
+ Geom::PathVector pathv = accumulated.get_pathvector() * _desktop->dt2doc();
+ pathv *= eraser_item->i2doc_affine().inverse();
+ repr->setAttribute("d", sp_svg_write_path(pathv));
+ return cast<SPItem>(document->getObjectByRepr(repr));
+}
+
+void EraserTool::_clearCurrent()
+{
+ // reset bpath
+ currentshape->set_bpath(nullptr);
+
+ // reset curve
+ currentcurve.reset();
+ cal1.reset();
+ cal2.reset();
+
+ // reset points
+ npoints = 0;
+}
+
+/**
+ * @brief Performs the actual erase operation against the current document
+ * @return whether actual erasing took place (and undo history should be updated).
+ */
+bool EraserTool::_doWork()
+{
+ if (accumulated.is_empty()) {
+ if (repr) {
+ sp_repr_unparent(repr);
+ repr = nullptr;
+ }
+ return false;
+ }
+
+ SPDocument *document = _desktop->getDocument();
+ if (!repr) {
+ // Create eraser repr
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *eraser_repr = xml_doc->createElement("svg:path");
+
+ sp_desktop_apply_style_tool(_desktop, eraser_repr, "/tools/eraser", false);
+ repr = eraser_repr;
+ }
+ if (!repr) {
+ return false;
+ }
+
+ Selection *selection = _desktop->getSelection();
+ if (!selection) {
+ return false;
+ }
+ bool was_selection = !selection->isEmpty();
+
+ // Find items to work on as well as items that will be needed to restore the selection afterwards.
+ _survivers.clear();
+ _clearStatusBar();
+
+ std::vector<EraseTarget> to_erase = _findItemsToErase();
+
+ bool work_done = false;
+ if (!to_erase.empty()) {
+ selection->clear();
+ work_done = _performEraseOperation(to_erase, true);
+ if (was_selection && !_survivers.empty()) {
+ selection->add(_survivers.begin(), _survivers.end());
+ }
+ }
+ // Clean up the eraser stroke repr:
+ sp_repr_unparent(repr);
+ repr = nullptr;
+ _acid = nullptr;
+ return work_done;
+}
+
+/**
+ * @brief Erases from a shape by cutting (boolean difference or cut operation).
+ * @param target - the item to be erased
+ * @param store_survivers - whether the surviving selected items and their remains should be stored.
+ * @return whether the target was successfully processed.
+ */
+bool EraserTool::_cutErase(EraseTarget target, bool store_survivers)
+{
+ // If the item is a clone, we check if the original is cuttable before unlinking it
+ if (auto use = cast<SPUse>(target.item)) {
+ auto original = use->trueOriginal();
+ if (_uncuttableItemType(original)) {
+ if (store_survivers && target.was_selected) {
+ _survivers.push_back(target.item);
+ }
+ return false;
+ } else if (auto *group = cast<SPGroup>(original)) {
+ return _probeUnlinkCutClonedGroup(target, use, group, store_survivers);
+ }
+ // A simple clone of a cuttable item: unlink and erase it.
+ target.item = use->unlink();
+ if (target.was_selected && store_survivers) { // Reselect the freshly unlinked item
+ _survivers.push_back(target.item);
+ }
+ }
+ return _booleanErase(target, store_survivers);
+}
+
+/**
+ * @brief Analyses a cloned group and decides if the CUT mode should unlink the clone.
+ * The decision to unlink the clone is based on collision detection between the eraser stroke
+ * and any of the eraseable contents of the cloned group, in the clone's coordinates.
+ * Unlinking only happens if there's an overlap between the eraser stroke and something that
+ * can be erased in CUT mode (via boolean operations).
+ * If the decision is made to unlink the clone, a copy of the clone is inserted into the document,
+ * and the function then erases all elements of the newly inserted group.
+ * @param original_target - the original erase target which turned out to be a clone.
+ * @param clone - the pointer to the SPUse object representing the clone (assument non-null).
+ * @param cloned_group - the original group that is cloned (at the origin of the USE chain).
+ * @param store_survivers - whether the surviving selected items and their remains should be stored.
+ * @return whether the clone was unlinked and something was erased from the resulting new group.
+ */
+bool EraserTool::_probeUnlinkCutClonedGroup(EraseTarget &original_target, SPUse *clone, SPGroup *cloned_group,
+ bool store_survivers)
+{
+ std::vector<EraseTarget> children;
+ children.reserve(cloned_group->getItemCount());
+
+ for (auto *child : cloned_group->childList(false)) {
+ children.emplace_back(cast<SPItem>(child), false);
+ }
+ auto const filtered_children = _filterCutEraseables(children, true);
+
+ // We must now check if any of the eraseable items in the original group, after transforming
+ // to the coordinates of the clone, actually intersect the eraser stroke.
+ Geom::Affine parent_inverse_transform;
+ if (auto *parent_item = cast<SPItem>(cloned_group->parent)) {
+ parent_inverse_transform = parent_item->i2doc_affine().inverse();
+ }
+ auto const relative_transform = parent_inverse_transform * clone->i2doc_affine();
+ auto const eraser_bounds = _acid->documentExactBounds();
+ if (!eraser_bounds) {
+ return false;
+ }
+ auto const eraser_in_group_coordinates = *eraser_bounds * relative_transform.inverse();
+ bool found_collision = false;
+ for (auto const &orig_child : filtered_children) {
+ if (orig_child.item->collidesWith(eraser_in_group_coordinates)) {
+ found_collision = true;
+ break;
+ }
+ }
+ if (found_collision) {
+ auto *unlinked = cast<SPGroup>(clone->unlink());
+ if (!unlinked) {
+ return false;
+ }
+ std::vector<EraseTarget> unlinked_children;
+ unlinked_children.reserve(filtered_children.size());
+
+ for (auto *child : unlinked->childList(false)) {
+ unlinked_children.emplace_back(cast<SPItem>(child), false);
+ }
+ auto overlapping = _filterCutEraseables(_filterByCollision(unlinked_children, _acid));
+
+ // If the clone was selected, the newly unlinked group should stay selected
+ if (original_target.was_selected && store_survivers) {
+ _survivers.push_back(unlinked);
+ }
+
+ return _performEraseOperation(overlapping, false);
+ } else {
+ if (original_target.was_selected && store_survivers) {
+ _survivers.push_back(original_target.item); // If the clone was selected, it should stay so
+ }
+ if (filtered_children.size() < children.size()) {
+ auto non_eraseable_touched = [&](EraseTarget const &t) -> bool {
+ if (!t.item || !_uncuttableItemType(t.item)) {
+ return false;
+ }
+ return t.item->collidesWith(eraser_in_group_coordinates);
+ };
+ if (std::any_of(children.begin(), children.end(), non_eraseable_touched)) {
+ _setStatusBarMessage(_("Some objects could not be cut."));
+ }
+ }
+ return false;
+ }
+}
+
+/** 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 (is<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 erasure.
+ * @param target - the item to be erased.
+ * @param store_survivers - whether the surviving selected items and their remains should be stored.
+ * @return true on success, false on failure
+ */
+bool EraserTool::_booleanErase(EraseTarget target, bool store_survivers)
+{
+ if (!target.item) {
+ return false;
+ }
+ XML::Document *xml_doc = _desktop->doc()->getReprDoc();
+ XML::Node *duplicate_stroke = repr->duplicate(xml_doc);
+ repr->parent()->appendChild(duplicate_stroke);
+ Glib::ustring duplicate_id = duplicate_stroke->attribute("id");
+ GC::release(duplicate_stroke); // parent takes over
+ ObjectSet operands(_desktop);
+ operands.set(duplicate_stroke);
+ if (!nowidth) {
+ operands.pathUnion(true, true);
+ }
+ operands.add(target.item);
+ operands.removeLPESRecursive(true);
+
+ _handleStrokeStyle(target.item);
+
+ if (nowidth) {
+ operands.pathCut(true, true);
+ } else {
+ operands.pathDiff(true, true);
+ }
+ if (auto *spill = _desktop->doc()->getObjectById(duplicate_id)) {
+ operands.remove(spill);
+ spill->deleteObject(false);
+ return false;
+ }
+ if (!_break_apart) {
+ operands.combine(true, true);
+ } else if (!nowidth) {
+ operands.breakApart(true, false, true);
+ }
+ if (store_survivers && target.was_selected) {
+ _survivers.insert(_survivers.end(), operands.items().begin(), operands.items().end());
+ }
+ return true;
+}
+
+/**
+ * @brief Performs the actual erasing on a collection of erase targets.
+ * In CUT mode, the optional survivers vector will be populated with leftover pieces of
+ * partially erased shapes that used to be selected.
+ * @param items_to_erase - a non-empty vector of erase targets.
+ * @param store_survivers - whether the surviving selected items and their remains should be stored.
+ * @return whether something was actually erased.
+ */
+bool EraserTool::_performEraseOperation(std::vector<EraseTarget> const &items_to_erase, bool store_survivers)
+{
+ if (mode == EraserToolMode::CUT) {
+ bool erased_something = false;
+ for (auto const &target : items_to_erase) {
+ erased_something = _cutErase(target, store_survivers) || erased_something;
+ }
+ return erased_something;
+ } else if (mode == EraserToolMode::CLIP) {
+ if (nowidth) {
+ return false;
+ }
+ for (auto const &target : items_to_erase) {
+ _clipErase(target.item);
+ }
+ return true;
+ } else { // mode == EraserToolMode::DELETE
+ for (auto const &target : items_to_erase) {
+ if (target.item) {
+ target.item->deleteObject(true);
+ }
+ }
+ return true;
+ }
+}
+
+/** Handles the "evenodd" stroke style */
+void EraserTool::_handleStrokeStyle(SPItem *item) const
+{
+ auto *style = item->style;
+ if (style && 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) const
+{
+ 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;
+ SPClipPath *clip_path = item->getClipObject();
+ if (clip_path) {
+ std::vector<SPItem *> selected;
+ selected.push_back(cast<SPItem>(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 = cast<SPItem>(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) {
+ auto dup_clip_obj = cast<SPItem>(item->parent->appendChildRepr(dup_clip));
+ Inkscape::GC::release(dup_clip);
+ if (dup_clip_obj) {
+ dup_clip_obj->transform *= item->getRelativeTransform(cast<SPItem>(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);
+ auto rect = cast<SPRect>(item->parent->appendChildRepr(rect_repr));
+ Inkscape::GC::release(rect_repr);
+ rect->setPosition(bbox->left(), bbox->top(), bbox->width(), bbox->height());
+ rect->transform = cast<SPItem>(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);
+ }
+}
+
+/** 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)
+{
+ auto as_path = 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.reversed();
+
+ 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();
+}
+
+/**
+ * @brief Filters out elements that can be erased in CUT mode (by boolean operations) from the given
+ * vector of potential erase targets. For items that cannot be erased in the CUT mode, a
+ * warning message can be flashed in the status bar.
+ * @param items - a vector containing EraseTarget structs
+ * @param silent - if set to true, the status bar messages will not be shown.
+ * @return a filtered vector whose elements can be erased in CUT mode
+*/
+std::vector<EraseTarget> EraserTool::_filterCutEraseables(std::vector<EraseTarget> const &items, bool silent)
+{
+ std::vector<EraseTarget> result;
+ result.reserve(items.size());
+
+ for (auto &target : items) {
+ if (Error e = _uncuttableItemType(target.item)) {
+ if (!silent) {
+ if (e & RASTER_IMAGE) {
+ _setStatusBarMessage(_("Cannot cut out from a bitmap, use <b>Clip</b> mode "
+ "instead."));
+ } else if (e & NO_AREA_PATH) {
+ _setStatusBarMessage(_("Cannot cut out from a path with zero area, use "
+ "<b>Clip</b> mode instead."));
+ }
+ }
+ } else {
+ result.push_back(target);
+ }
+ }
+ return result;
+}
+
+/**
+ * @brief Filters a list of potential erase targets by collision with a given item
+ * @param items - a vector of EraseTarget elements to be filtered
+ * @param with - a pointer to an SPItem to check collisions with
+ * @return a new vector containing those elements of `items` that have a collision with `with`.
+ */
+std::vector<EraseTarget> EraserTool::_filterByCollision(std::vector<EraseTarget> const &items, SPItem *with) const
+{
+ std::vector<EraseTarget> result;
+ if (!with) {
+ return result;
+ }
+ result.reserve(items.size());
+
+ if (auto const collision_shape = with->documentExactBounds()) {
+ for (auto const &target : items) {
+ if (target.item && target.item->collidesWith(*collision_shape)) {
+ result.push_back(target);
+ }
+ }
+ }
+ return result;
+}
+
+/**
+ * @brief Prepares a list of items in the current document containing the items which qualify
+ * for the erase operation (based on selection & collision detection).
+ * Additionally, the selected items which are going to survive the erase operation (and
+ * should be used to restore the selection afterwards) will be added to the _survivers member.
+ * If the user attempts to erase an illegal item, a warning message is shown in the status bar.
+ * @return items that should undergo the erase operation
+ */
+std::vector<EraseTarget> EraserTool::_findItemsToErase()
+{
+ std::vector<EraseTarget> result;
+
+ auto *document = _desktop->getDocument();
+ auto *selection = _desktop->getSelection();
+ if (!document || !selection) {
+ return result;
+ }
+
+ if (mode == EraserToolMode::DELETE) {
+ // In DELETE mode, the classification is based on having been touched by the mouse cursor:
+ // * result should contain touched items;
+ // * _survivers should contain selected but untouched items.
+ auto *r = Rubberband::get(_desktop);
+ std::vector<SPItem *> touched = document->getItemsAtPoints(_desktop->dkey, r->getPoints());
+ if (selection->isEmpty()) {
+ for (auto *item : touched) {
+ result.emplace_back(item, false);
+ }
+ } else {
+ for (auto *item : selection->items()) {
+ if (std::find(touched.begin(), touched.end(), item) == touched.end()) {
+ _survivers.push_back(item);
+ } else {
+ result.emplace_back(item, true);
+ }
+ }
+ }
+ } else {
+ // In the other modes, we start with a crude filtering step based on bounding boxes
+ _acid = _insertAcidIntoDocument(document);
+ if (!_acid) {
+ return result;
+ }
+ Geom::OptRect eraser_bbox = _acid->documentVisualBounds();
+ if (!eraser_bbox) {
+ return result;
+ }
+ std::vector<SPItem *> candidates = document->getItemsPartiallyInBox(_desktop->dkey, *eraser_bbox,
+ false, false, false, true);
+ std::vector<EraseTarget> allowed; ///< Items we're allowed to erase based on selection
+ allowed.reserve(candidates.size());
+
+ // If selection is empty, we're allowed to erase all items except the eraser stroke itself.
+ if (selection->isEmpty()) {
+ for (auto *candidate : candidates) {
+ if (candidate != _acid) {
+ allowed.emplace_back(candidate, false);
+ }
+ }
+ } // How we handle non-empty selection further depends on the mode.
+
+ if (mode == EraserToolMode::CUT) {
+ // In CUT mode, we must unpack groups, since the boolean difference/cut operation
+ // doesn't make sense for a group.
+ for (auto *selected : selection->items()) {
+ bool included_for_erase = false;
+ for (auto *candidate : candidates) {
+ if (selected == candidate || selected->isAncestorOf(candidate)) {
+ allowed.emplace_back(candidate, selection->includes(candidate));
+ included_for_erase = (candidate == selected) || included_for_erase;
+ }
+ }
+ if (!included_for_erase) {
+ _survivers.push_back(selected);
+ }
+ }
+ // The filtering is based on a precise collision detection procedure:
+ // * result will contain all eraseable items that overlap with the eraser stroke;
+ // * _survivers will contain all selected items that were rejected during this filtering.
+ auto overlapping = _filterByCollision(allowed, _acid);
+ auto valid = _filterCutEraseables(overlapping); // Sets status bar messages
+
+ for (auto const &element : allowed) {
+ if (element.item && element.was_selected &&
+ std::find(valid.begin(), valid.end(), element) == valid.end())
+ {
+ _survivers.push_back(element.item);
+ }
+ }
+ result.insert(result.end(), valid.begin(), valid.end());
+
+ } else if (mode == EraserToolMode::CLIP) {
+ // In CLIP mode, we don't check descendants, because clip can be set to an entire group.
+ auto const all_selected = selection->items();
+ for (auto *item : all_selected) {
+ allowed.emplace_back(item, true);
+ }
+
+ // The classification is also based on the precise collision detection:
+ // * result will contain all items that overlap with the eraser stroke;
+ // * _survivers will contain all selected items, since CLIP mode is always non-destructive.
+ auto overlapping = _filterByCollision(allowed, _acid);
+ result.insert(result.end(), overlapping.begin(), overlapping.end());
+ _survivers.insert(_survivers.end(), all_selected.begin(), all_selected.end());
+ }
+ }
+ return result;
+}
+
+void EraserTool::_fitAndSplit(bool releasing)
+{
+ double const tolerance_sq = square(_desktop->w2d().descrim() * tolerance);
+ nowidth = (width == 0); // setting width is managed by the base class
+
+#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, 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_pathvector(), 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.emplace_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, 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..5198ebd
--- /dev/null
+++ b/src/ui/tools/eraser-tool.h
@@ -0,0 +1,155 @@
+// 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"
+#include "object/sp-use.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+enum class EraserToolMode
+{
+ DELETE,
+ CUT,
+ CLIP
+};
+static inline constexpr auto DEFAULT_ERASER_MODE = EraserToolMode::CUT;
+
+/** Represents an item to erase */
+struct EraseTarget
+{
+ SPItem *item = nullptr; ///< Pointer to the item to be erased
+ bool was_selected = false; ///< Whether the item was part of selection
+
+ EraseTarget(SPItem *ptr, bool sel)
+ : item{ptr}
+ , was_selected{sel}
+ {}
+ inline bool operator==(EraseTarget const &other) const noexcept { return item == other.item; }
+};
+
+class EraserTool : public DynamicBase {
+
+private:
+ // non-static data:
+ EraserToolMode mode = DEFAULT_ERASER_MODE;
+ bool nowidth = false;
+ std::vector<MessageId> _our_messages;
+ SPItem *_acid = nullptr;
+ std::vector<SPItem *> _survivers;
+ Pref<bool> _break_apart;
+ Pref<int> _mode_int;
+
+ // static data:
+ static constexpr uint32_t trace_color_rgba = 0xff0000ff; // RGBA red
+ static constexpr SPWindRule trace_wind_rule = SP_WIND_RULE_EVENODD;
+
+ static constexpr double tolerance = 0.1;
+
+ static constexpr double epsilon = 0.5e-6;
+ static constexpr double epsilon_start = 0.5e-2;
+ static constexpr double vel_start = 1e-5;
+
+ static constexpr double drag_default = 1.0;
+ static constexpr double drag_min = 0.0;
+ static constexpr double drag_max = 1.0;
+
+ static constexpr double min_pressure = 0.0;
+ static constexpr double max_pressure = 1.0;
+ static constexpr double default_pressure = 1.0;
+
+ static constexpr double min_tilt = -1.0;
+ static constexpr double max_tilt = 1.0;
+ static constexpr double default_tilt = 0.0;
+
+public:
+ // public member functions
+ EraserTool(SPDesktop *desktop);
+ ~EraserTool() override;
+ bool root_handler(GdkEvent *event) final;
+
+ using Error = std::uint64_t;
+ static constexpr Error ALL_GOOD = 0x0;
+ static constexpr Error NON_EXISTENT = 0x1 << 1;
+ static constexpr Error NO_AREA_PATH = 0x1 << 2;
+ static constexpr Error RASTER_IMAGE = 0x1 << 3;
+ static constexpr Error ERROR_GROUP = 0x1 << 4;
+
+private:
+ // private member functions
+ void _accumulate();
+ bool _apply(Geom::Point p);
+ bool _booleanErase(EraseTarget target, bool store_survivers);
+ void _brush();
+ void _cancel();
+ void _clearCurrent();
+ void _clearStatusBar();
+ void _clipErase(SPItem *item) const;
+ void _completeBezier(double tolerance_sq, bool releasing);
+ bool _cutErase(EraseTarget target, bool store_survivers);
+ bool _doWork();
+ void _drawTemporaryBox();
+ void _extinput(GdkEvent *event);
+ void _failedBezierFallback();
+ std::vector<EraseTarget> _filterByCollision(std::vector<EraseTarget> const &items, SPItem *with) const;
+ std::vector<EraseTarget> _filterCutEraseables(std::vector<EraseTarget> const &items, bool silent = false);
+ std::vector<EraseTarget> _findItemsToErase();
+ void _fitAndSplit(bool releasing);
+ void _fitDrawLastPoint();
+ bool _handleKeypress(GdkEventKey const *key);
+ void _handleStrokeStyle(SPItem *item) const;
+ SPItem *_insertAcidIntoDocument(SPDocument *document);
+ bool _performEraseOperation(std::vector<EraseTarget> const &items_to_erase, bool store_survivers);
+ void _reset(Geom::Point p);
+ void _setStatusBarMessage(char *message);
+ void _updateMode();
+
+ 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);
+ bool _probeUnlinkCutClonedGroup(EraseTarget &original_target, SPUse* clone, SPGroup* cloned_group,
+ bool store_survivers = true);
+};
+
+} // 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..3e94f35
--- /dev/null
+++ b/src/ui/tools/flood-tool.cpp
@@ -0,0 +1,1230 @@
+// 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 "async/progress.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
+
+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;
+
+ auto gray_map = Trace::GrayMap(max_x - min_x + 1, max_y - min_y + 1);
+ unsigned gray_map_y = 0;
+ for (unsigned y = min_y; y <= max_y; y++) {
+ auto gray_map_t = gray_map.row(gray_map_y);
+
+ trace_t = get_trace_pixel(trace_px, min_x, y, bci.width);
+ for (unsigned x = min_x; x <= max_x; x++) {
+ *gray_map_t = is_pixel_colored(trace_t) ? Trace::GrayMap::BLACK : Trace::GrayMap::WHITE;
+ gray_map_t++;
+ trace_t++;
+ }
+ gray_map_y++;
+ }
+
+ Trace::Potrace::PotraceTracingEngine pte;
+ auto progress = Async::ProgressAlways<double>();
+ auto results = pte.traceGrayMap(gray_map, progress);
+
+ // XML Tree being used here directly while it shouldn't be...."
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double offset = prefs->getDouble("/tools/paintbucket/offset", 0.0);
+
+ for (auto result : results) {
+
+ Inkscape::XML::Node *pathRepr = xml_doc->createElement("svg:path");
+ /* Set style */
+ sp_desktop_apply_style_tool (desktop, pathRepr, "/tools/paintbucket", false);
+
+ Path *path = new Path;
+ path->LoadPathVector(result.path);
+
+ 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) {
+ cast<SPItem>(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.",
+ cast<SPPath>(reprobj)->nodesInPath()), cast<SPPath>(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.",
+ cast<SPPath>(reprobj)->nodesInPath()), cast<SPPath>(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 &= 0xffffff00; // make color transparent for 'alpha' flood mode to work
+ // bgcolor is 0xrrggbbaa, we need 0xaarrggbb
+ dtc = bgcolor >> 8; // keep color transparent; page color doesn't support transparency anymore
+
+ 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..35cf119
--- /dev/null
+++ b/src/ui/tools/freehand-base.cpp
@@ -0,0 +1,1007 @@
+// 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" // TODO: Remove in the future
+#include "ui/tools/pencil-tool.h" // TODO: Remove in the future
+
+#define MIN_PRESSURE 0.0
+#define MAX_PRESSURE 1.0
+#define DEFAULT_PRESSURE 1.0
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+/**
+ * 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, std::shared_ptr<SPCurve> gc);
+
+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)
+ , red_color(0xff00007f)
+ , blue_color(0x0000ff7f)
+ , green_color(0x00ff007f)
+ , highlight_color(0x0000007f)
+ , green_closed(false)
+ , white_item(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
+ sel_changed_connection = selection->connectChanged([=](Selection *) { _attachSelection(); });
+ sel_modified_connection = selection->connectModified([=](Selection *, guint) { onSelectionModified(); });
+
+ // Create red bpath
+ this->red_bpath = make_canvasitem<CanvasItemBpath>(desktop->getCanvasSketch());
+ this->red_bpath->set_stroke(this->red_color);
+ this->red_bpath->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ // Create blue bpath
+ this->blue_bpath = make_canvasitem<CanvasItemBpath>(desktop->getCanvasSketch());
+ this->blue_bpath->set_stroke(this->blue_color);
+ this->blue_bpath->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ // Create green curve
+ green_curve = std::make_shared<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());
+
+ _attachSelection();
+}
+
+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 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 = cast<SPLPEItem>(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)
+{
+ 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");
+ auto successor = 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 = 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 = cast<SPLPEItem>(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;
+ auto use = cast<SPUse>(item);
+ if ( use ) {
+ return;
+ }
+ SPDesktop *desktop = dc->getDesktop();
+ SPDocument *document = desktop->getDocument();
+ if (!document || !desktop) {
+ return;
+ }
+ if (!is<SPLPEItem>(item)) {
+ return;
+ }
+ if(!cast_unsafe<SPLPEItem>(item)->hasPathEffectOfType(BEND_PATH)){
+ Effect::createAndApply(BEND_PATH, document, item);
+ }
+ Effect* lpe = cast<SPLPEItem>(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 = cast<SPLPEItem>(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 const *curve, bool is_bend)
+{
+ using namespace Inkscape::LivePathEffect;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ auto *desktop = dc->getDesktop();
+
+ if (item && is<SPLPEItem>(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(dc->getPrefsPath() + "/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 || (!is<SPShape>(bend_item) && !is<SPGroup>(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(dc->getPrefsPath() + "/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));
+ tol *= 10000;
+ std::ostringstream ss;
+ ss << tol;
+ spdc_apply_simplify(ss.str(), dc, item);
+ sp_lpe_item_update_patheffect(cast<SPLPEItem>(item), true, false);
+ }
+ if (prefs->getInt(dc->getPrefsPath() + "/freehand-mode", 0) == 1) {
+ Effect::createAndApply(SPIRO, dc->getDesktop()->getDocument(), item);
+ }
+
+ if (prefs->getInt(dc->getPrefsPath() + "/freehand-mode", 0) == 2) {
+ Effect::createAndApply(BSPLINE, dc->getDesktop()->getDocument(), item);
+ }
+ if (auto sp_shape = cast<SPShape>(item)) {
+ curve = sp_shape->curve();
+ }
+ 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);
+ guint curve_length = curve->get_segment_count();
+ 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"
+ 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"
+ SPCurve c;
+ constexpr 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)){
+ dc->selection->toCurves(true);
+ if (auto pasted_clipboard = dc->selection->singleItem()){
+ 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 && (is<SPShape>(bend_item) || is<SPGroup>(bend_item))){
+ // If item is a SPRect, convert it to path first:
+ if (is<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
+ */
+
+/* fixme: We have to ensure this is not delayed (Lauris) */
+void FreehandBase::onSelectionModified()
+{
+ _attachSelection();
+}
+
+void FreehandBase::_attachSelection()
+{
+ // We reset white and forget white/start/end anchors
+ white_curves.clear();
+ white_anchors.clear();
+ white_item = nullptr;
+ sa = nullptr;
+ ea = nullptr;
+
+ SPItem *item = selection ? selection->singleItem() : nullptr;
+
+ if ( item && is<SPPath>(item) ) {
+ // Create new white data
+ // Item
+ white_item = item;
+
+ // Curve list
+ // We keep it in desktop coordinates to eliminate calculation errors
+ auto path = static_cast<SPPath *>(item);
+ if (!path->curveForEdit()) {
+ return;
+ }
+
+ auto tmp = path->curveForEdit()->transformed(white_item->i2dt_affine()).split();
+ white_curves.clear();
+ white_curves.reserve(tmp.size());
+ for (auto &t : tmp) {
+ white_curves.emplace_back(std::make_shared<SPCurve>(std::move(t)));
+ }
+
+ // Anchor list
+ for (auto const &c : white_curves) {
+ g_return_if_fail( c->get_segment_count() > 0 );
+ if ( !c->is_closed() ) {
+ white_anchors.emplace_back(std::make_unique<SPDrawAnchor>(this, c, true , *c->first_point()));
+ white_anchors.emplace_back(std::make_unique<SPDrawAnchor>(this, c, false, *c->last_point()));
+ }
+ }
+ // 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_shared<SPCurve>();
+ std::swap(c, dc->green_curve);
+ dc->green_bpaths.clear();
+
+ // Blue
+ c->append_continuous(std::move(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, std::move(c));
+ 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 = std::make_shared<SPCurve>(e->reversed());
+ }
+ if(prefs->getInt(dc->getPrefsPath() + "/freehand-mode", 0) == 1 ||
+ prefs->getInt(dc->getPrefsPath() + "/freehand-mode", 0) == 2)
+ {
+ e = std::make_shared<SPCurve>(e->reversed());
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*e->last_segment());
+ if(cubic){
+ auto lastSeg = std::make_shared<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 = std::make_shared<SPCurve>(e->reversed());
+ }
+ c->append_continuous(*e);
+ }
+ if (forceclosed)
+ {
+ dc->getDesktop()->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed."));
+ c->closepath_current();
+ }
+ spdc_flush_white(dc, std::move(c));
+}
+
+static void spdc_flush_white(FreehandBase *dc, std::shared_ptr<SPCurve> gc)
+{
+ std::shared_ptr<SPCurve> c;
+
+ if (! dc->white_curves.empty()) {
+ g_assert(dc->white_item);
+
+ c = std::make_shared<SPCurve>();
+ for (auto const &wc : dc->white_curves) {
+ c->append(*wc);
+ }
+
+ dc->white_curves.clear();
+ if (gc) {
+ c->append(*gc);
+ }
+ } else if (gc) {
+ c = std::move(gc);
+ } 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->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 = cast<SPLPEItem>(dc->white_item)->hasPathEffectRecursive();
+ } else {
+ repr = xml_doc->createElement("svg:path");
+ // Set style
+ sp_desktop_apply_style_tool(desktop, repr, dc->getPrefsPath(), 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 = cast<SPItem>(layer->appendChildRepr(repr));
+ }
+ spdc_check_for_and_apply_waiting_LPE(dc, dc->white_item, c.get(), false);
+ }
+ if (!dc->white_item) {
+ // Attach repr
+ auto item = 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);
+ }
+ }
+ auto lpeitem = 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.
+ dc->onSelectionModified();
+ }
+
+ // 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_free_colors(FreehandBase *dc)
+{
+ // Red
+ dc->red_bpath.reset();
+
+ // Blue
+ dc->blue_bpath.reset();
+ dc->blue_curve.reset();
+
+ // Overwrite start anchor curve
+ dc->sa_overwrited.reset();
+ // Green
+ 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();
+ auto item = cast<SPItem>(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..a803a74
--- /dev/null
+++ b/src/ui/tools/freehand-base.h
@@ -0,0 +1,161 @@
+// 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"
+#include "display/curve.h"
+#include "display/control/canvas-item-ptr.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;
+
+protected:
+ guint32 red_color;
+ guint32 blue_color;
+ guint32 green_color;
+ guint32 highlight_color;
+
+public:
+ // Red - Last segment as it's drawn.
+ CanvasItemPtr<CanvasItemBpath> red_bpath;
+ SPCurve red_curve;
+ std::optional<Geom::Point> red_curve_get_last_point();
+
+ // Blue - New path after LPE as it's drawn.
+ CanvasItemPtr<CanvasItemBpath> blue_bpath;
+ SPCurve blue_curve;
+
+ // Green - New path as it's drawn.
+ std::vector<CanvasItemPtr<CanvasItemBpath>> green_bpaths;
+ std::shared_ptr<SPCurve> green_curve;
+ std::unique_ptr<SPDrawAnchor> green_anchor;
+ bool green_closed; // a flag meaning we hit the green anchor, so close the path on itself
+
+ // White
+ SPItem *white_item;
+ std::vector<std::shared_ptr<SPCurve>> white_curves;
+ std::vector<std::unique_ptr<SPDrawAnchor>> white_anchors;
+
+ // Temporary modified curve when start anchor
+ std::shared_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;
+
+ void onSelectionModified();
+
+protected:
+ bool root_handler(GdkEvent* event) override;
+ void _attachSelection();
+};
+
+/**
+ * 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..04acf4b
--- /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)
+// 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);
+}
+
+void GradientTool::select_prev()
+{
+ g_assert(_grdrag);
+ GrDragger *d = _grdrag->select_prev();
+ _desktop->scroll_to_point(d->point);
+}
+
+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 &it : _grdrag->item_curves) {
+ if (it.curve->contains(event_p, tolerance)) {
+ return it.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 (is<SPGradient>(parent)) {
+ doc = parent->document;
+ SPStop *new_stop = sp_vector_add_stop (cast<SPGradient>(parent), this_stop, next_stop, offset);
+ new_stops.push_back(new_stop);
+ cast<SPGradient>(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);
+ }
+ 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 && !(event->button.state & GDK_CONTROL_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 && !(event->button.state & GDK_SHIFT_MASK)) {
+ ret = TRUE;
+ Inkscape::Rubberband::get(_desktop)->stop();
+ 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..6098a46
--- /dev/null
+++ b/src/ui/tools/gradient-tool.h
@@ -0,0 +1,77 @@
+// 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;
+
+ 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..5149afc
--- /dev/null
+++ b/src/ui/tools/lpe-tool.cpp
@@ -0,0 +1,460 @@
+// 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));
+
+ shape_editor = std::make_unique<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()
+{
+ shape_editor.reset();
+ canvas_bbox.reset();
+ measuring_items.clear();
+
+ 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 (!is<SPLPEItem>(item)) {
+ return -1;
+ }
+
+ Inkscape::LivePathEffect::Effect* lpe = cast<SPLPEItem>(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 && is<SPLPEItem>(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)
+{
+ lc->canvas_bbox.reset();
+
+ 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 = make_canvasitem<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::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) {
+ auto path = 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;
+
+ auto canvas_text = make_canvasitem<CanvasItemText>(tmpgrp, Geom::Point(0,0), arc_length);
+ set_pos_and_anchor(canvas_text.get(), pwd2, 0.5, 10);
+ if (!show) {
+ canvas_text->hide();
+ }
+
+ lc->measuring_items[path] = std::move(canvas_text);
+ }
+ }
+}
+
+void lpetool_delete_measuring_items(LpeTool *lc)
+{
+ 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(std::move(arc_length));
+ set_pos_and_anchor(i.second.get(), 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..498031e
--- /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;
+
+ std::unique_ptr<ShapeEditor> shape_editor;
+ CanvasItemPtr<CanvasItemRect> canvas_bbox;
+ Inkscape::LivePathEffect::EffectType mode;
+
+ std::map<SPPath*, CanvasItemPtr<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..5633871
--- /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) {
+ auto shape = cast<SPShape>(item);
+
+ if(shape && shape->hasMarkers() && (editMarkerMode != -1)) {
+ SPObject *obj = shape->_marker[editMarkerMode];
+
+ if(obj) {
+
+ auto sp_marker = 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(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..beee75c
--- /dev/null
+++ b/src/ui/tools/measure-tool.cpp
@@ -0,0 +1,1445 @@
+// 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 "page-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 &center, Geom::Point const &end, Geom::Point const &anchor,
+ double angle, bool to_phantom,
+ 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->lower_to_bottom();
+ 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);
+
+ measure_tmp_items.clear();
+ measure_item.clear();
+ measure_phantom_items.clear();
+}
+
+static char const *endpoint_to_pref(bool is_start)
+{
+ return is_start ? "/tools/measure/measure-start" : "/tools/measure/measure-end";
+}
+
+Geom::Point MeasureTool::readMeasurePoint(bool is_start)
+{
+ return Preferences::get()->getPoint(endpoint_to_pref(is_start), Geom::Point(Geom::infinity(), Geom::infinity()));
+}
+
+void MeasureTool::writeMeasurePoint(Geom::Point point, bool is_start)
+{
+ Preferences::get()->setPoint(endpoint_to_pref(is_start), 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,
+ 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();
+ 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;");
+ auto marker = cast<SPItem>(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)");
+ auto path = cast<SPItem>(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();
+
+ measure_phantom_items.clear();
+ 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);
+ auto measure_item = cast<SPItem>(_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 = 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);
+ auto text_item_box = cast<SPItem>(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();
+
+ 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.emplace_back(canvas_tooltip);
+ } else {
+ measure_tmp_items.emplace_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->lower_to_bottom();
+ 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->lower_to_bottom();
+ 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.emplace_back(canvas_tooltip);
+}
+
+void MeasureTool::showInfoBox(Geom::Point cursor, bool into_groups)
+{
+ using Inkscape::Util::Quantity;
+
+ 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);
+ // Correct for the current page's position.
+ if (prefs->getBool("/options/origincorrection/page", true)) {
+ affine *= _desktop->getDocument()->getPageManager().getSelectedPageAffine().inverse();
+ }
+ 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 = 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);
+ double yaxis_shift = Quantity::convert(fontsize, "px", unit->abbr);
+ Geom::Point rel_position = Geom::Point(origin, origin + yaxis_shift);
+ /* Keeps infobox just above the cursor */
+ Geom::Point pos = _desktop->w2d(cursor);
+ double gap = Quantity::convert(7 + fontsize, "px", unit->abbr);
+ double yaxisdir = _desktop->yaxisdir();
+
+ if (selected) {
+ showItemInfoText(pos - (yaxisdir * Geom::Point(0, rel_position[Geom::Y]) * zoom), _desktop->getSelection()->includes(over) ? _("Selected") : _("Not selected"), fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+ }
+
+ if (is<SPShape>(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 * Geom::Point(0, rel_position[Geom::Y]) * zoom), measure_str, fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+
+ } else if (is<SPGroup>(over)) {
+
+ measure_str = _("Press 'CTRL' to measure into group");
+ showItemInfoText(pos - (yaxisdir * Geom::Point(0, rel_position[Geom::Y]) * 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 * Geom::Point(0, rel_position[Geom::Y]) * 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 * Geom::Point(0, rel_position[Geom::Y]) * 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 * Geom::Point(0, rel_position[Geom::Y]) * 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 * Geom::Point(0, rel_position[Geom::Y]) * 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
+ measure_tmp_items.clear();
+
+ //TODO:Calculate the measure area for current length and origin
+ // and use canvas->redraw_all(). 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 = cast<SPShape>(item)) {
+ calculate_intersections(_desktop, item, lineseg, *shape->curve(), intersection_times);
+ } else {
+ if (is<SPText>(item) || is<SPFlowtext>(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.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_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 {
+ auto item = cast<SPItem>(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..f8f1920
--- /dev/null
+++ b/src/ui/tools/measure-tool.h
@@ -0,0 +1,127 @@
+// 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"
+#include "display/control/canvas-item-ptr.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 &center, Geom::Point const &end, Geom::Point const &anchor,
+ double angle, bool to_phantom,
+ 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<CanvasItemPtr<CanvasItem>> measure_tmp_items;
+ std::vector<CanvasItemPtr<CanvasItem>> measure_phantom_items;
+ std::vector<CanvasItemPtr<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..2521471
--- /dev/null
+++ b/src/ui/tools/mesh-tool.cpp
@@ -0,0 +1,970 @@
+// 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);
+}
+
+void MeshTool::select_prev()
+{
+ g_assert(_grdrag);
+ GrDragger *d = _grdrag->select_prev();
+ _desktop->scroll_to_point(d->point);
+}
+
+/**
+ * Returns vector of control curves mouse is over. Returns only first if 'first' is true.
+ * event_p is in canvas (world) units.
+ */
+std::vector<GrDrag::ItemCurve*> 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<GrDrag::ItemCurve*> selected;
+
+ for (auto &it : _grdrag->item_curves) {
+ if (it.curve->contains(event_p, tolerance)) {
+ selected.emplace_back(&it);
+ 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
+ auto gradient = cast<SPMeshGradient>( 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::cerr << "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::cerr << "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 ( is<SPMeshGradient>(server) ) {
+
+ Geom::OptRect item_bbox = item->geometricBounds();
+ auto gradient = cast<SPMeshGradient>(server);
+ if (gradient->array.fill_box( item_bbox )) {
+ changed = true;
+ }
+ }
+ }
+
+ if (style->stroke.isPaintserver()) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ if ( is<SPMeshGradient>(server) ) {
+
+ Geom::OptRect item_bbox = item->visualBounds();
+ auto gradient = cast<SPMeshGradient>(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 && is<SPMeshGradient>(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) {
+ Inkscape::PaintTarget fill_or_stroke = it->is_fill ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+ GrDragger *dragger0 = _grdrag->getDraggerFor(it->item, POINT_MG_CORNER, it->corner0, fill_or_stroke);
+ GrDragger *dragger1 = _grdrag->getDraggerFor(it->item, POINT_MG_CORNER, it->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 && is<SPMeshGradient>(server))
+ has_mesh = true;
+ }
+ }
+
+ if (has_mesh && !(event->button.state & GDK_CONTROL_MASK)) {
+ 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]->item, mousepoint_doc, 0);
+ ret = TRUE;
+ }
+ } else {
+ dragging = false;
+
+ // unless clicked with Ctrl (to enable Ctrl+doubleclick).
+ if (event->button.state & GDK_CONTROL_MASK && !(event->button.state & GDK_SHIFT_MASK)) {
+ ret = TRUE;
+ Inkscape::Rubberband::get(_desktop)->stop();
+ 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 && is<SPMeshGradient>(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 = is<SPText>(*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..8fcf163
--- /dev/null
+++ b/src/ui/tools/mesh-tool.h
@@ -0,0 +1,86 @@
+// 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 "gradient-drag.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)
+
+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<GrDrag::ItemCurve*> 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..ad82477
--- /dev/null
+++ b/src/ui/tools/node-tool.cpp
@@ -0,0 +1,861 @@
+// 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 "rubberband.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/knot/knot-holder.h"
+#include "ui/modifiers.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/tools/node-tool.h"
+
+using Inkscape::Modifiers::Modifier;
+
+/** @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;
+
+ // 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->_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->get_rubberband()->stop();
+
+ 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;
+
+ _path_data->node_data.node_group->unlink();
+ _path_data->node_data.handle_group->unlink();
+ _path_data->node_data.handle_line_group->unlink();
+ _path_data->outline_group->unlink();
+ _path_data->dragpoint_group->unlink();
+ _transform_handle_group->unlink();
+}
+
+Inkscape::Rubberband *NodeTool::get_rubberband() const
+{
+ return Inkscape::Rubberband::get(_desktop);
+}
+
+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) {
+ auto lpeitem = 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());
+ SPCurve c;
+ 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_pathvector(), 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->getSelection());
+ } else if (entry_name == "edit_masks") {
+ this->edit_masks = value.getBool();
+ this->selection_changed(_desktop->getSelection());
+ } 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 && (is<SPGroup>(obj) || is<SPObjectGroup>(obj))) {
+ for (auto& c: obj->children) {
+ gather_items(nt, base, &c, role, s);
+ }
+ } else if (auto item = cast<SPItem>(obj)) {
+ ShapeRecord r;
+ r.object = obj;
+ r.role = role;
+
+ // TODO add support for objectBoundingBox
+ if (role != SHAPE_ROLE_NORMAL && base) {
+ r.edit_transform = base->i2doc_affine();
+ }
+
+ 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 = 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(cast<SPItem>(r.object)) == this->_shape_editors.end()) {
+ auto si = std::make_unique<ShapeEditor>(_desktop, r.edit_transform);
+ auto item = cast<SPItem>(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->getSelection();
+ static Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ auto rband = get_rubberband();
+
+ if (!rband->is_started()) {
+ if (_multipath->event(this, event) || _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));
+
+ if (event->motion.state & GDK_BUTTON1_MASK) {
+ if (rband->is_started()) {
+ rband->move(motion_dt);
+ }
+
+ auto touch_path = Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->get_label();
+ if (rband->getMode() == RUBBERBAND_MODE_TOUCHPATH) {
+ this->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
+ _("<b>Draw over</b> lines to select their nodes; release <b>%s</b> to switch to rubberband selection"), touch_path.c_str());
+ } else {
+ this->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
+ _("<b>Drag around</b> nodes to select them; press <b>%s</b> to switch to box selection"), touch_path.c_str());
+ }
+ return true;
+ } else if (rband->is_moved()) {
+ // Mouse button is up, but rband is still kicking.
+ rband->stop();
+ }
+
+ 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->getSelection()->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")) {
+ if (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 = cast<SPShape>(over_item);
+ if (!shape) {
+ break; // for now, handle only shapes
+ }
+
+ this->flashed_item = over_item;
+ if (!shape->curveForEdit()) {
+ break; // break out when curve doesn't exist
+ }
+
+ auto c = shape->curveForEdit()->transformed(over_item->i2dt_affine());
+
+ auto flash = new Inkscape::CanvasItemBpath(_desktop->getCanvasTemp(), c.get_pathvector(), 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_PRESS:
+ if (event->button.button == 1) {
+ if(Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->active(event->button.state)) {
+ rband->setMode(RUBBERBAND_MODE_TOUCHPATH);
+ } else {
+ rband->defaultMode();
+ }
+
+ Geom::Point const event_pt(event->button.x, event->button.y);
+ Geom::Point const desktop_pt(_desktop->w2d(event_pt));
+ rband->start(_desktop, desktop_pt, true);
+ return true;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1) {
+ if (rband->is_started() && rband->is_moved()) {
+ select_area(rband->getPath(), &event->button);
+ } else {
+ select_point(&event->button);
+ }
+ rband->stop();
+ return true;
+ }
+ break;
+
+ case GDK_2BUTTON_PRESS:
+ if ( event->button.button == 1 ) {
+ // 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()));
+ return true;
+ }
+ }
+ }
+ 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);
+}
+
+bool NodeTool::item_handler(SPItem *item, GdkEvent *event)
+{
+ bool ret = ToolBase::item_handler(item, event);
+
+ // Node shape editors are handled differently than shape tools
+ if (!ret && event->type == GDK_BUTTON_PRESS && event->button.button == 1) {
+ for (auto &se : _shape_editors) {
+ // This allows users to select an arbitary position in a pattern to edit on canvas.
+ if (auto knotholder = se.second->knotholder) {
+ auto point = Geom::Point(event->button.x, event->button.y);
+
+ // This allows us to dive into groups and find what the real item is
+ if (_desktop->getItemAtPoint(point, true) != knotholder->getItem())
+ continue;
+
+ ret = knotholder->set_item_clickpos(_desktop->w2d(point) * _desktop->dt2doc());
+ }
+ }
+ }
+ return ret;
+}
+
+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"));
+ }
+ }
+}
+
+void NodeTool::select_area(Geom::Path const &path, GdkEventButton *event) {
+ using namespace Inkscape::UI;
+
+ if (this->_multipath->empty()) {
+ // if multipath is empty, select rubberbanded items rather than nodes
+ Inkscape::Selection *selection = _desktop->getSelection();
+ auto sel_doc = _desktop->dt2doc() * *path.boundsFast();
+ 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(path, true);
+ } else {
+ // A/B/C. Adds nodes under box to existing selection.
+ this->_selected_nodes->selectArea(path);
+ if (ctrl) {
+ // C. Selects the inverse of all nodes under the box.
+ this->_selected_nodes->invertSelection();
+ }
+ }
+ }
+}
+
+void NodeTool::select_point(GdkEventButton *event) {
+ using namespace Inkscape::UI; // pull in event helpers
+
+ if (!event) {
+ return;
+ }
+
+ if (event->button != 1) {
+ return;
+ }
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+
+ 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 if (!selection->includes(item_clicked)) {
+ 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..d02481b
--- /dev/null
+++ b/src/ui/tools/node-tool.h
@@ -0,0 +1,115 @@
+// 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;
+ }
+
+ class Rubberband;
+}
+
+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;
+ bool item_handler(SPItem *item, GdkEvent *event) override;
+ void deleteSelected();
+private:
+ Inkscape::Rubberband *get_rubberband() const;
+
+ 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::Path const &path, GdkEventButton *event);
+ void select_point(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..45c5dfe
--- /dev/null
+++ b/src/ui/tools/pages-tool.cpp
@@ -0,0 +1,668 @@
+// 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/modifiers.h"
+#include "ui/widget/canvas.h"
+
+using Inkscape::Modifiers::Modifier;
+
+#define INDEX_OF(v, k) (std::distance(v.begin(), std::find(v.begin(), v.end(), k)));
+
+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->getSelection()->setBackup();
+ desktop->getSelection()->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));
+ resize_knots.push_back(knot);
+
+ auto m_knot = new SPKnot(desktop, _("Set page margin"), Inkscape::CANVAS_ITEM_CTRL_TYPE_MARGIN, "PageTool:Margin");
+ m_knot->setFill(0xffffff00, 0x0000ff00, 0x000000ff, 0x000000ff);
+ m_knot->setStroke(0x1699d791, 0xff99d791, 0x000000ff, 0x000000ff);
+ m_knot->setSize(11);
+ m_knot->setAnchor(SP_ANCHOR_CENTER);
+ m_knot->updateCtrl();
+ m_knot->hide();
+ m_knot->request_signal.connect(sigc::mem_fun(*this, &PagesTool::marginKnotMoved));
+ m_knot->ungrabbed_signal.connect(sigc::mem_fun(*this, &PagesTool::marginKnotFinished));
+ margin_knots.push_back(m_knot);
+
+ 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"));
+ m_knot->setCursor(SP_KNOT_STATE_DRAGGING, this->get_cursor(window, "page-resizing.svg"));
+ m_knot->setCursor(SP_KNOT_STATE_MOUSEOVER, this->get_cursor(window, "page-resize.svg"));
+ }
+ }
+ }
+
+ if (!visual_box) {
+ visual_box = make_canvasitem<CanvasItemRect>(desktop->getCanvasControls());
+ visual_box->set_stroke(0x0000ff7f);
+ visual_box->hide();
+ }
+ if (!drag_group) {
+ drag_group = make_canvasitem<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->getSelection()->restoreBackup();
+
+ visual_box.reset();
+
+ for (auto knot : resize_knots) {
+ delete knot;
+ }
+ resize_knots.clear();
+
+ if (drag_group) {
+ drag_group.reset();
+ 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::marginKnotSet(Geom::Rect margin_rect)
+{
+ for (int i = 0; i < margin_knots.size(); i++) {
+ margin_knots[i]->moveto(middleOfSide(i, margin_rect) * _desktop->doc2dt());
+ margin_knots[i]->show();
+ }
+}
+
+/*
+ * Get the middle of the side of the rectangle.
+ */
+Geom::Point PagesTool::middleOfSide(int side, const Geom::Rect &rect)
+{
+ return Geom::middle_point(rect.corner(side), rect.corner((side + 1) % 4));
+}
+
+void PagesTool::resizeKnotMoved(SPKnot *knot, Geom::Point const &ppointer, guint state)
+{
+ Geom::Rect rect; ///< Page rectangle in desktop coordinates.
+
+ 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()) * document->doc2dt();
+ }
+
+ 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 = 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) {
+ document->getPageManager().fitToRect(*on_screen_rect * document->dt2doc(), page);
+ Inkscape::DocumentUndo::done(document, "Resize page", INKSCAPE_ICON("tool-pages"));
+ on_screen_rect = {};
+ }
+ visual_box->hide();
+ mouse_is_pressed = false;
+}
+
+
+bool PagesTool::marginKnotMoved(SPKnot *knot, Geom::Point *ppointer, guint state)
+{
+ auto document = _desktop->getDocument();
+ auto &pm = document->getPageManager();
+
+ // Editing margins creates a page for the margin to be stored in.
+ pm.enablePages();
+
+ if (auto page = pm.getSelected()) {
+ Geom::Point point = *ppointer * document->dt2doc();
+
+ // Confine knot to edge
+ auto confine = Modifiers::Modifier::get(Modifiers::Type::TRANS_CONFINE)->active(state);
+ if (!Modifiers::Modifier::get(Modifiers::Type::MOVE_SNAPPING)->active(state)) {
+ point = getSnappedResizePoint(point, state, knot->drag_origin, page);
+ }
+
+ // Calculate what we're acting on, clamp it depending on the side.
+ int side = INDEX_OF(margin_knots, knot);
+ auto axis = (side & 1) ? Geom::X : Geom::Y;
+ auto delta = (point - page->getDocumentRect().corner(side))[axis];
+ auto value = std::max(0.0, (side + 1) & 2 ? -delta : delta);
+
+ // Set to page and back to to knot to inform confinement.
+ page->setMarginSide(side, value, confine);
+ knot->setPosition(middleOfSide(side, page->getDocumentMargin()) * document->doc2dt(), state);
+
+ Inkscape::DocumentUndo::maybeDone(document, "page-margin", ("Adjust page margin"), INKSCAPE_ICON("tool-pages"));
+ } else {
+ g_warning("Can't add margin, pages not enabled correctly!");
+ }
+ return true;
+}
+
+void PagesTool::marginKnotFinished(SPKnot *knot, guint state)
+{
+ // Margins are updated in real time.
+}
+
+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);
+ _desktop->getCanvas()->enable_autoscroll();
+ } 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->fitToRect(*rect * affine * document->dt2doc(), 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_EDGE_CORNER, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_PAGE_EDGE_CENTER, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_PAGE_EDGE_CORNER, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_PAGE_EDGE_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.get(), 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) {
+ shape->unlink();
+ }
+ 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();
+ }
+ for (auto knot : margin_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()));
+ marginKnotSet(*(doc->preferredBounds()));
+ });
+ resizeKnotSet(*(doc->preferredBounds()));
+ marginKnotSet(*(doc->preferredBounds()));
+ }
+ }
+}
+
+
+void PagesTool::pageModified(SPObject *object, guint /*flags*/)
+{
+ if (auto page = cast<SPPage>(object)) {
+ resizeKnotSet(page->getDesktopRect());
+ marginKnotSet(page->getDocumentMargin());
+ }
+}
+
+} // 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..30887b1
--- /dev/null
+++ b/src/ui/tools/pages-tool.h
@@ -0,0 +1,99 @@
+// 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"
+#include "display/control/canvas-item-ptr.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 marginKnotSet(Geom::Rect margin_rect);
+ bool marginKnotMoved(SPKnot *knot, Geom::Point *point, guint state);
+ void marginKnotFinished(SPKnot *knot, guint state);
+
+ 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;
+ std::vector<SPKnot *> margin_knots;
+ SPKnot *grabbed_knot = nullptr;
+ SPPage *highlight_item = nullptr;
+ SPPage *dragging_item = nullptr;
+ std::optional<Geom::Rect> on_screen_rect; ///< On-screen rectangle, in desktop coordinates.
+ CanvasItemPtr<CanvasItemRect> visual_box;
+ CanvasItemPtr<CanvasItemGroup> drag_group;
+ std::vector<Inkscape::CanvasItemBpath *> drag_shapes;
+ std::vector<Inkscape::SnapCandidatePoint> _bbox_points;
+
+ static Geom::Point middleOfSide(int side, const Geom::Rect &rect);
+};
+
+} // 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..2c280c6
--- /dev/null
+++ b/src/ui/tools/pen-tool.cpp
@@ -0,0 +1,2043 @@
+// 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"
+
+// Given an optionally-present SPCurve, e.g. a smart/raw pointer or an optional,
+// return a copy of its pathvector if present, or a blank pathvector otherwise.
+template <typename T>
+static Geom::PathVector copy_pathvector_optional(T &p)
+{
+ if (p) {
+ return p->get_pathvector();
+ } else {
+ return {};
+ }
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static Geom::Point pen_drag_origin_w(0, 0);
+static bool pen_within_tolerance = false;
+
+PenTool::PenTool(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename)
+ : FreehandBase(desktop, prefs_path, cursor_filename)
+ , _undo{"doc.undo"}
+ , _redo{"doc.redo"}
+{
+ tablet_enabled = false;
+
+ // Pen indicators (temporary handles shown when adding a new node).
+ auto canvas = desktop->getCanvasControls();
+ for (int i = 0; i < 4; i++) {
+ ctrl[i] = make_canvasitem<CanvasItemCtrl>(canvas, ctrl_types[i]);
+ ctrl[i]->set_fill(0x0);
+ ctrl[i]->hide();
+ }
+
+ cl0 = make_canvasitem<CanvasItemCurve>(canvas);
+ cl1 = make_canvasitem<CanvasItemCurve>(canvas);
+ 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);
+ }
+ }
+
+ for (auto &c : ctrl) {
+ c.reset();
+ }
+ cl0.reset();
+ cl1.reset();
+
+ 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->state = PenTool::STOP;
+ this->_resetColors();
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+ cl0->hide();
+ cl1->hide();
+ this->message_context->clear();
+ this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled"));
+ _redo_stack.clear();
+}
+
+/**
+ * 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
+
+ sa = anchor;
+ if (anchor) {
+ //Put the start overwrite curve always on the same direction
+ if (anchor->start) {
+ sa_overwrited = std::make_shared<SPCurve>(sa->curve->reversed());
+ } else {
+ sa_overwrited = std::make_shared<SPCurve>(*sa->curve);
+ }
+ _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() && is<SPPath>(selection->singleItem())) {
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path"));
+ }
+
+ // Create green anchor
+ p = event_dt;
+ _endpointSnap(p, bevent.state);
+ green_anchor = std::make_unique<SPDrawAnchor>(this, green_curve, 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){
+ ctrl[1]->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){
+ ctrl[1]->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(cast<SPPath>(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
+ this->green_bpaths.clear();
+
+ // one canvas bpath for all of green_curve
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), copy_pathvector_optional(green_curve), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ this->green_bpaths.emplace_back(canvas_shape);
+ }
+ if (this->green_anchor) {
+ this->green_anchor->ctrl->set_position(this->green_anchor->dp);
+ }
+
+ red_curve.reset();
+ red_curve.moveto(p[0]);
+ red_curve.curveto(p[1], p[2], p[3]);
+ red_bpath->set_bpath(&red_curve, true);
+
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+ // handles
+ // hide the handlers in bspline and spiro modes
+ if (this->npoints == 5) {
+ ctrl[0]->set_position(p[0]);
+ ctrl[0]->show();
+ ctrl[3]->set_position(p[3]);
+ ctrl[3]->show();
+ }
+
+ if (this->p[0] != this->p[1] && !this->spiro && !this->bspline) {
+ ctrl[1]->set_position(p[1]);
+ ctrl[1]->show();
+ cl1->set_coords(p[0], p[1]);
+ cl1->show();
+ } else {
+ 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];
+ ctrl[2]->set_position(p2);
+ ctrl[2]->show();
+ cl0->set_coords(p2, p[0]);
+ cl0->show();
+ } else {
+ 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_shared<SPCurve>();
+ previous->moveto(A);
+ previous->curveto(B, C, D);
+ if (green_curve->get_segment_count() == 1) {
+ green_curve = std::move(previous);
+ } else {
+ //we eliminate the last segment
+ 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_shared<SPCurve>();
+ if (auto const cubic = dynamic_cast<Geom::CubicBezier const *>(green_curve->last_segment())) {
+ A = green_curve->last_segment()->initialPoint();
+ B = (*cubic)[1];
+ C = *green_curve->last_point();
+ D = C;
+ } else {
+ //We obtain the last segment 4 points in the previous curve
+ A = green_curve->last_segment()->initialPoint();
+ B = A;
+ C = *green_curve->last_point();
+ D = C;
+ }
+ previous->moveto(A);
+ previous->curveto(B, C, D);
+ if (green_curve->get_segment_count() == 1){
+ green_curve = std::move(previous);
+ }else{
+ //we eliminate the last segment
+ 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 (green_curve->is_unset() && sa && !sa->curve->is_unset()) {
+ _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/redo.
+ if (npoints > 0 && _undo.isTriggeredBy(&event->key)) {
+ return _undoLastPoint(true);
+ } else if (_redo.isTriggeredBy(&event->key)) {
+ return _redoLastPoint();
+ }
+
+ 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->getSelection()->toGuides();
+ ret = true;
+ }
+ break;
+ case GDK_KEY_BackSpace:
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ ret = _undoLastPoint();
+ break;
+ default:
+ break;
+ }
+ return ret;
+}
+
+void PenTool::_resetColors() {
+ // Red
+ this->red_curve.reset();
+ this->red_bpath->set_bpath(nullptr);
+
+ // Blue
+ blue_curve.reset();
+ blue_bpath->set_bpath(nullptr);
+
+ // Green
+ 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&#176;, 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
+ this->green_bpaths.clear();
+
+ // one canvas bpath for all of green_curve
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), copy_pathvector_optional(green_curve), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ green_bpaths.emplace_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()){
+ 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]);
+ }
+}
+
+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 (is<SPLPEItem>(this->white_item) && cast<SPLPEItem>(this->white_item)->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect *thisEffect =
+ cast<SPLPEItem>(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 (is<SPLPEItem>(this->white_item) && cast<SPLPEItem>(this->white_item)->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect *thisEffect =
+ cast<SPLPEItem>(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_shared<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);
+ 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_shared<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;
+ SPCurve tmp_curve;
+ this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]);
+ if (this->green_curve->is_unset() && !this->sa) {
+ this->p[1] = this->p[0] + (1./3)*(this->p[3] - this->p[0]);
+ if (shift) {
+ this->p[2] = this->p[3];
+ }
+ } else if (!this->green_curve->is_unset()){
+ tmp_curve = *green_curve;
+ } else {
+ tmp_curve = *sa_overwrited;
+ }
+ 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))
+ {
+ SPCurve previous_weight_power;
+ previous_weight_power.moveto(tmp_curve.last_segment()->initialPoint());
+ previous_weight_power.lineto(this->p[0]);
+ auto 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 (sa && green_curve->is_unset()) {
+ sa_overwrited = std::make_shared<SPCurve>(tmp_curve);
+ }
+ green_curve = std::make_shared<SPCurve>(std::move(tmp_curve));
+ }
+ if (cubic) {
+ if (this->bspline) {
+ SPCurve weight_power;
+ weight_power.moveto(red_curve.last_segment()->initialPoint());
+ weight_power.lineto(*red_curve.last_point());
+ auto 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] = 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 = *red_curve.last_point();
+ SPCurve red;
+ red.moveto(this->p[0]);
+ red.curveto(this->p[1],this->p[2],this->p[3]);
+ red_bpath->set_bpath(&red, true);
+ }
+
+ if(this->anchor_statusbar && !this->red_curve.is_unset()){
+ if(shift){
+ this->_bsplineSpiroEndAnchorOff();
+ }else{
+ this->_bsplineSpiroEndAnchorOn();
+ }
+ }
+
+ // remove old piecewise green canvasitems
+ green_bpaths.clear();
+
+ // one canvas bpath for all of green_curve
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), copy_pathvector_optional(green_curve), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ green_bpaths.emplace_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]);
+ SPCurve tmp_curve;
+ SPCurve last_segment;
+ Geom::Point point_c(0,0);
+ if( green_anchor && green_anchor->active ){
+ tmp_curve = green_curve->reversed();
+ if (green_curve->get_segment_count() == 0) {
+ return;
+ }
+ } else if(this->sa){
+ tmp_curve = sa_overwrited->reversed();
+ }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());
+ } 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(std::move(last_segment));
+ }
+ tmp_curve.reverse();
+ if (green_anchor && green_anchor->active) {
+ green_curve->reset();
+ green_curve = std::make_shared<SPCurve>(std::move(tmp_curve));
+ } else {
+ sa_overwrited->reset();
+ sa_overwrited = std::make_shared<SPCurve>(std::move(tmp_curve));
+ }
+}
+
+void PenTool::_bsplineSpiroEndAnchorOff()
+{
+ SPCurve tmp_curve;
+ SPCurve last_segment;
+ this->p[2] = this->p[3];
+ if (green_anchor && green_anchor->active) {
+ tmp_curve = green_curve->reversed();
+ if (green_curve->get_segment_count() == 0) {
+ return;
+ }
+ } else if (sa) {
+ tmp_curve = sa_overwrited->reversed();
+ } 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(std::move(last_segment));
+ }
+ tmp_curve.reverse();
+
+ if (green_anchor && green_anchor->active) {
+ green_curve->reset();
+ green_curve = std::make_shared<SPCurve>(std::move(tmp_curve));
+ } else {
+ sa_overwrited->reset();
+ sa_overwrited = std::make_shared<SPCurve>(std::move(tmp_curve));
+ }
+}
+
+//prepares the curves for its transformation into BSpline curve.
+void PenTool::_bsplineSpiroBuild()
+{
+ if (!spiro && !bspline){
+ return;
+ }
+
+ //We create the base curve
+ SPCurve curve;
+ //If we continuate the existing curve we add it at the start
+ if (sa && !sa->curve->is_unset()){
+ curve = *sa_overwrited;
+ }
+
+ if (!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, 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 (bspline) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Geom::PathVector hp;
+ bool uniform = false;
+ Glib::ustring pref_path = "/live_effects/bspline/uniform";
+ if (prefs->getEntry(pref_path).isValid()) {
+ uniform = prefs->getString(pref_path) == "true";
+ }
+ LivePathEffect::sp_bspline_do_effect(curve, 0, hp, uniform);
+ } else {
+ LivePathEffect::sp_spiro_do_effect(curve);
+ }
+
+ blue_bpath->set_bpath(&curve, true);
+ blue_bpath->set_stroke(blue_color);
+ blue_bpath->show();
+
+ blue_curve.reset();
+ //We hide the holders that doesn't contribute anything
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+ if (spiro){
+ ctrl[1]->set_position(p[0]);
+ ctrl[1]->show();
+ }
+ 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, true);
+
+ if (statusbar) {
+ gchar *message;
+ if(this->spiro || this->bspline){
+ message = is_curve ?
+ _("<b>Curve segment</b>: angle %3.2f&#176;; <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&#176;; <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&#176;, 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&#176;, 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.
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+
+ ctrl[1]->show();
+ cl1->show();
+
+ if ( this->npoints == 2 ) {
+ this->p[1] = q;
+ cl0->hide();
+ ctrl[1]->set_position(p[1]);
+ ctrl[1]->show();
+ cl1->set_coords(p[0], p[1]);
+ this->_setAngleDistanceStatusMessage(q, 0, _("<b>Curve handle</b>: angle %3.2f&#176;, length %s; with <b>Ctrl</b> to snap angle"));
+ } else if ( this->npoints == 5 ) {
+ this->p[4] = q;
+ 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, true);
+ }
+ // Avoid conflicting with initial point ctrl
+ if (green_curve->get_segment_count() > 0) {
+ ctrl[0]->set_position(this->p[0]);
+ ctrl[0]->show();
+ }
+ ctrl[3]->set_position(this->p[3]);
+ ctrl[3]->show();
+ ctrl[2]->set_position(this->p[2]);
+ ctrl[2]->show();
+ ctrl[1]->set_position(this->p[4]);
+ ctrl[1]->show();
+
+ cl0->set_coords(this->p[3], this->p[2]);
+ cl1->set_coords(this->p[3], this->p[4]);
+
+ gchar *message = is_symm ?
+ _("<b>Curve handle, symmetric</b>: angle %3.2f&#176;, 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&#176;, 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);
+ }
+
+ 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]))
+ {
+ SPCurve lsegment;
+ 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());
+ green_curve->backspace();
+ green_curve->append_continuous(std::move(lsegment));
+ }
+ }
+ green_curve->append_continuous(red_curve);
+ auto curve = red_curve;
+
+ /// \todo fixme:
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), curve.get_pathvector(), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ green_bpaths.emplace_back(canvas_shape);
+
+ this->p[0] = this->p[3];
+ this->p[1] = this->p[4];
+ this->npoints = 2;
+
+ red_curve.reset();
+ _redo_stack.clear();
+ }
+}
+
+bool PenTool::_undoLastPoint(bool user_undo) {
+ bool ret = false;
+
+ if ( this->green_curve->is_unset() || (this->green_curve->last_segment() == nullptr) ) {
+ if (red_curve.is_unset()) {
+ return ret; // do nothing; this event should be handled upstream
+ }
+ _cancel();
+ ret = true;
+ } else {
+ red_curve.reset();
+ if (user_undo) {
+ if (_did_redo) {
+ _redo_stack.clear();
+ _did_redo = false;
+ }
+ _redo_stack.push_back(green_curve->get_pathvector());
+ }
+ // 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()) {
+ this->green_bpaths.pop_back();
+ }
+ this->green_curve->reset();
+ } else {
+ this->green_curve->backspace();
+ if (this->green_bpaths.size() > 1) {
+ 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];
+ ctrl[1]->set_position(this->p[0]);
+ } else {
+ this->p[1] = this->p[0];
+ }
+ }
+
+ for (auto &c : ctrl) {
+ c->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;
+}
+
+/** Re-add the last undone point to the path being drawn */
+bool PenTool::_redoLastPoint()
+{
+ if (_redo_stack.empty()) {
+ return false;
+ }
+
+ auto old_green = std::move(_redo_stack.back());
+ _redo_stack.pop_back();
+ green_curve->set_pathvector(old_green);
+
+ if (auto const *last_seg = green_curve->last_segment()) {
+ Geom::Path freshly_added;
+ freshly_added.append(*last_seg);
+ green_bpaths.emplace_back(make_canvasitem<CanvasItemBpath>(_desktop->getCanvasSketch(), freshly_added, true));
+ }
+ green_bpaths.back()->set_stroke(green_color);
+ green_bpaths.back()->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ auto const last_point = green_curve->last_point();
+ if (last_point) {
+ p[0] = p[1] = *last_point;
+ }
+ _setSubsequentPoint(p[3], true);
+ _bsplineSpiroBuild();
+
+ _did_redo = true;
+ return true;
+}
+
+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->_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;
+
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+
+ cl0->hide();
+ cl1->hide();
+
+ this->green_anchor.reset();
+ _redo_stack.clear();
+ 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..a7053e8
--- /dev/null
+++ b/src/ui/tools/pen-tool.h
@@ -0,0 +1,175 @@
+// 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 <array>
+#include <sigc++/sigc++.h>
+
+#include "display/control/canvas-item-enums.h"
+#include "live_effects/effect.h"
+#include "ui/tools/freehand-base.h"
+#include "util/action-accel.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?
+
+ 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;
+
+ CanvasItemPtr<CanvasItemCtrl> ctrl[4]; // Origin, Start, Center, End point of path.
+ static constexpr std::array<CanvasItemCtrlType, 4> ctrl_types = {
+ CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH, CANVAS_ITEM_CTRL_TYPE_ROTATE,
+ CANVAS_ITEM_CTRL_TYPE_ROTATE, CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH};
+
+ CanvasItemPtr<CanvasItemCurve> cl0;
+ CanvasItemPtr<CanvasItemCurve> cl1;
+
+ 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(bool user_undo = false);
+ bool _redoLastPoint();
+
+ 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;
+ Util::ActionAccel _undo, _redo; ///< Keep track of Undo and Redo keybindings
+ // NOTE: undoing work in progress always deletes the last added point,
+ // so there's no need for an undo stack.
+ std::vector<Geom::PathVector> _redo_stack; ///< History of undone events
+ bool _did_redo = false;
+};
+
+}
+}
+}
+
+#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..568606d
--- /dev/null
+++ b/src/ui/tools/pencil-tool.cpp
@@ -0,0 +1,1177 @@
+// 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; }
+
+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)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/freehand/pencil/selcue")) {
+ this->enableSelectionCue();
+ }
+ 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) {
+ sa_overwrited = std::make_shared<SPCurve>(anchor->curve->reversed());
+ } else {
+ sa_overwrited = std::make_shared<SPCurve>(*anchor->curve);
+ }
+ _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() && is<SPPath>(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 ( !sa && !green_anchor ) {
+ /* Create green anchor */
+ green_anchor = std::make_unique<SPDrawAnchor>(this, green_curve, 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 (is<SPLPEItem>(item)) {
+ Effect* lpe = cast<SPLPEItem>(item)->getCurrentLPE();
+ if (lpe) {
+ LPEPowerStroke* ps = static_cast<LPEPowerStroke*>(lpe);
+ if (ps) {
+ _desktop->getSelection()->clear();
+ _desktop->getSelection()->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) {
+ _addFreehandPoint(p_end, revent.state, true);
+ _pressure_curve.reset();
+ } else {
+ _endpointSnap(p_end, revent.state);
+ if (p_end != p) {
+ // then we must have snapped!
+ _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);
+
+ 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->getSelection()->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);
+ }
+ }
+}
+
+/**
+ * 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);
+ SPCurve curvepressure;
+ 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]);
+ }
+ }
+ curvepressure.transform(currentLayer()->i2dt_affine().inverse());
+ 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);
+
+ auto powerpreview = cast<SPShape>(currentLayer()->appendChildRepr(pp));
+ auto lpeitem = 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 = _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);
+ }
+ _pressure_curve = SPCurve(std::move(pressure_path));
+ red_bpath->set_bpath(&_pressure_curve);
+ }
+ 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]);
+ Geom::Point point_at2 = b[4 * c + 3] + (1./3) * (b[4 * c + 0] - b[4 * c + 3]);
+ 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]);
+ Geom::Point point_at2 = b[3] + (1./3)*(b[0] - b[3]);
+ 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);
+ }
+ 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);
+
+ /// \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(), red_curve.get_pathvector(), true);
+ cshape->set_stroke(green_color);
+ cshape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ this->green_bpaths.emplace_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..b1e0b2c
--- /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;
+ 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..a7b0e5a
--- /dev/null
+++ b/src/ui/tools/rect-tool.cpp
@@ -0,0 +1,464 @@
+// 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"
+
+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 (rect) {
+ // 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 if (!selection->includes(this->item_to_select)) {
+ 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->getSelection()->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 = cast<SPRect>(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 &#215; %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 &#215; %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 &#215; %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 &#215; %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..6137c94
--- /dev/null
+++ b/src/ui/tools/select-tool.cpp
@@ -0,0 +1,1148 @@
+// 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/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->getSelection(),
+ 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;
+ auto current_group = 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 (is<SPGroup>(clicked_item) && !is<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));
+
+ {
+ auto selGroup = 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->getCanvas()->enable_autoscroll();
+ 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();
+ auto singleGroup = 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;
+
+ 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 (item && 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 (keyval) {
+ case GDK_KEY_Left: // move selection left
+ case GDK_KEY_KP_Left:
+ if (!MOD__CTRL(event)) { // not ctrl
+ gint mul = 1 + gobble_key_events(keyval, 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(keyval, 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(keyval, 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(keyval, 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:
+ case GDK_KEY_c:
+ case GDK_KEY_C:
+ /* stamping mode: show outline mode moving */
+ if (this->dragging && this->grabbed) {
+ _seltrans->stamp(keyval != GDK_KEY_space);
+ 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(keyval, 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(keyval, 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();
+ auto clickedGroup = cast<SPGroup>(clicked_item);
+ if ( (clickedGroup && (clickedGroup->layerMode() != SPGroup::LAYER)) || is<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->getSelection()->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;
+}
+
+/**
+ * Update the toolbar description to this selection.
+ */
+void SelectTool::updateDescriber(Inkscape::Selection *selection)
+{
+ _describer->updateMessage(selection);
+}
+
+}
+}
+}
+
+/*
+ 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..e71a61f
--- /dev/null
+++ b/src/ui/tools/select-tool.h
@@ -0,0 +1,81 @@
+// 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;
+
+ void updateDescriber(Inkscape::Selection *sel);
+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..8ab8efb
--- /dev/null
+++ b/src/ui/tools/spiral-tool.cpp
@@ -0,0 +1,409 @@
+// 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"
+
+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 (spiral) {
+ // 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 = cast<SPSpiral>(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&#176;; 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..c6089b3
--- /dev/null
+++ b/src/ui/tools/spray-tool.cpp
@@ -0,0 +1,1528 @@
+// 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"
+#include "ui/widget/canvas.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)
+ , 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 = make_canvasitem<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();
+}
+
+void SprayTool::update_cursor(bool /*with_shift*/) {
+ guint num = 0;
+ gchar *sel_message = nullptr;
+
+ if (!_desktop->getSelection()->isEmpty()) {
+ num = (guint)boost::distance(_desktop->getSelection()->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();
+
+ // Get average color.
+ double R, G, B, A;
+ drawing->averageColor(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.
+ if (auto box = cast<SPBox3D>(item)) {
+ desktop->getSelection()->remove(item);
+ set->remove(item);
+ item = box->convert_to_group();
+ set->add(item);
+ desktop->getSelection()->add(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 = 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);
+ auto item_copied = 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 (is<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 = 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->getSelection()->isEmpty()) {
+ num = (guint)boost::distance(_desktop->getSelection()->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..f8bda36
--- /dev/null
+++ b/src/ui/tools/spray-tool.h
@@ -0,0 +1,148 @@
+// 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"
+#include "display/control/canvas-item-ptr.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;
+ CanvasItemPtr<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..b211916
--- /dev/null
+++ b/src/ui/tools/star-tool.cpp
@@ -0,0 +1,428 @@
+// 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"
+
+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 (star) {
+ // 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 if (!selection->includes(this->item_to_select)) {
+ 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 = cast<SPStar>(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&#176;; with <b>Ctrl</b> to snap angle") :
+ _("<b>Star</b>: radius %s, angle %.2f&#176;; 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..9aaffa7
--- /dev/null
+++ b/src/ui/tools/text-tool.cpp
@@ -0,0 +1,1905 @@
+// 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/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 = make_canvasitem<CanvasItemCurve>(desktop->getCanvasControls());
+ cursor->set_stroke(0x000000ff);
+ cursor->hide();
+
+ // The rectangle box tightly wrapping text object when selected or under cursor.
+ indicator = make_canvasitem<CanvasItemRect>(desktop->getCanvasControls());
+ indicator->set_stroke(0x0000ff7f);
+ indicator->set_shadow(0xffffff7f, 1);
+ indicator->hide();
+
+ // The shape that the text is flowing into
+ frame = make_canvasitem<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 = make_canvasitem<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 && (is<SPFlowtext>(item) || is<SPText>(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;
+ }
+
+ cursor.reset();
+ indicator.reset();
+ frame.reset();
+ padding_frame.reset();
+ 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 (is<SPText>(item_ungrouped) || is<SPFlowtext>(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);
+ auto text_item = cast<SPItem>(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, "&lt;"); break;
+ case '>': strcpy(utf8, "&gt;"); break;
+ case '&': strcpy(utf8, "&amp;"); 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 &#215; %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 (is<SPText>(item_ungrouped) || is<SPFlowtext>(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 (is<SPText>(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>
+ }
+
+ auto text_element = 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;
+ auto textitem = 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();
+ }
+
+ auto flowtext = 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 (is<SPText>(item) || is<SPFlowtext>(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.
+ auto sptext = 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 ( is<SPString>(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 (is<SPText>(tc->text)) {
+ Geom::OptRect opt_frame = cast<SPText>(tc->text)->get_frame();
+ if (opt_frame && (!opt_frame->contains(p0))) {
+ scroll = false;
+ }
+ } else if (is<SPFlowtext>(tc->text)) {
+ SPItem *frame = cast<SPFlowtext>(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);
+ else
+ desktop->scroll_to_point(d1);
+ }
+ }
+
+ 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;
+ std::unique_ptr<Shape> exclusion_shape;
+ double padding = 0.0;
+
+ // Frame around text
+ if (is<SPFlowtext>(tc->text)) {
+ SPItem *frame = cast<SPFlowtext>(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 = 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 = 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 = cast<SPShape>(shape_item)) {
+ if (shape->curve()) {
+ curve.append(shape->curve()->transformed(shape->transform));
+ }
+ }
+ }
+
+ if (!curve.is_empty()) {
+ bool has_padding = std::fabs(padding) > 1e-12;
+
+ if (has_padding || exclusion_shape) {
+ // 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, exclusion_shape.get(), 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;
+
+ 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.emplace_back(quad);
+ }
+
+ if (tc->shape_editor) {
+ 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 ((is<SPText>(ti) || is<SPFlowtext>(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()->getSelection()->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()->getSelection()->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..c87431e
--- /dev/null
+++ b/src/ui/tools/text-tool.h
@@ -0,0 +1,122 @@
+// 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"
+#include "display/control/canvas-item-ptr.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 ---
+ CanvasItemPtr<CanvasItemCurve> cursor;
+ CanvasItemPtr<CanvasItemRect> indicator;
+ CanvasItemPtr<CanvasItemBpath> frame; // Highlighting flowtext shapes or textpath path
+ CanvasItemPtr<CanvasItemBpath> padding_frame; // Highlighting flowtext padding
+ std::vector<CanvasItemPtr<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..59a6470
--- /dev/null
+++ b/src/ui/tools/tool-base.cpp
@@ -0,0 +1,1712 @@
+// 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/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"
+
+// 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 double scroll_multiply = 1;
+static unsigned scroll_keyval = 0;
+
+// globals for key processing
+static bool latin_keys_group_valid = FALSE;
+static int 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)
+ : _prefs_path(std::move(prefs_path))
+ , _cursor_filename("none")
+ , _cursor_default(std::move(cursor_filename))
+ , _uses_snap(uses_snap)
+ , _desktop(desktop)
+{
+ pref_observer = Inkscape::Preferences::PreferencesObserver::create(_prefs_path, [this] (auto &val) { set(val); });
+ set_cursor(_cursor_default);
+ _desktop->getCanvas()->grab_focus();
+
+ message_context = std::make_unique<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()
+{
+ enableSelectionCue(false);
+ _dse_timeout_conn.disconnect();
+}
+
+/**
+ * Called by our pref_observer if a preference has been changed.
+ */
+void ToolBase::set(Inkscape::Preferences::Entry const &/*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 const &filename) const
+{
+ 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);
+ }
+}
+
+/**
+ * Gobbles next key events on the queue with the same keyval and mask. Returns the number of events consumed.
+ */
+gint gobble_key_events(guint keyval, guint mask) {
+ GdkEvent *event_next;
+ gint i = 0;
+
+ event_next = gdk_event_get();
+ // while the next event is also a key notify with the same keyval and mask,
+ while (event_next && (event_next->type == GDK_KEY_PRESS || event_next->type
+ == GDK_KEY_RELEASE) && event_next->key.keyval == keyval && (!mask
+ || (event_next->key.state & mask))) {
+ if (event_next->type == GDK_KEY_PRESS)
+ i++;
+ // kill it
+ gdk_event_free(event_next);
+ // get next
+ event_next = gdk_event_get();
+ }
+ // otherwise, put it back onto the queue
+ if (event_next)
+ gdk_event_put(event_next);
+
+ return i;
+}
+
+/**
+ * Gobbles next motion notify events on the queue with the same mask. Returns the number of events consumed.
+ */
+void gobble_motion_events(guint mask) {
+ GdkEvent *event_next;
+
+ event_next = gdk_event_get();
+ // while the next event is also a key notify with the same keyval and mask,
+ while (event_next && event_next->type == GDK_MOTION_NOTIFY
+ && (event_next->motion.state & mask)) {
+ // kill it
+ gdk_event_free(event_next);
+ // get next
+ event_next = gdk_event_get();
+ }
+ // otherwise, put it back onto the queue
+ if (event_next)
+ gdk_event_put(event_next);
+}
+
+/**
+ * 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 double accelerate_scroll(GdkEvent *event, double acceleration)
+{
+ auto time_diff = event->key.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 = event->key.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");
+ bool ret = false;
+
+ auto compute_angle = [&] {
+ // Hack: Undo coordinate transformation applied by canvas to get events back to window coordinates.
+ // Real solution: Move all this functionality out of this file to somewhere higher up in the chain.
+ auto cursor = Geom::Point(event->motion.x, event->motion.y) * _desktop->canvas->get_geom_affine().inverse() * _desktop->canvas->get_affine() - _desktop->canvas->get_pos();
+ return Geom::deg_from_rad(Geom::atan2(cursor - Geom::Point(_desktop->canvas->get_dimensions()) / 2.0));
+ };
+
+ 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 = event->button.x;
+ yp = 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 (is_space_panning()) {
+ // When starting panning, make sure there are no snap events pending because these might disable the panning again
+ if (_uses_snap) {
+ 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()) {
+ // Canvas ctrl + middle-click to rotate
+ rotating = true;
+
+ start_angle = current_angle = compute_angle();
+
+ grabCanvasEvents(Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK);
+
+ } 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) {
+ 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) {
+ 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)) {
+ 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 &&
+ std::abs((int)event->motion.x - xp) < tolerance * 3 &&
+ std::abs((int)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);
+ }
+
+ auto const motion_w = Geom::Point(event->motion.x, event->motion.y);
+ auto const moved_w = motion_w - button_w;
+ _desktop->scroll_relative(moved_w);
+ ret = true;
+ }
+ } else if (zoom_rb) {
+ if (within_tolerance &&
+ std::abs((int)event->motion.x - xp) < tolerance &&
+ std::abs((int)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()) {
+ auto const motion_w = Geom::Point(event->motion.x, event->motion.y);
+ auto 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.
+ auto const motion_w = Geom::Point(xp, yp);
+ auto 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);
+ }
+ } else if (rotating) {
+ auto angle = compute_angle();
+
+ double constexpr rotation_snap = 15.0;
+ double delta_angle = angle - start_angle;
+ if (event->motion.state & GDK_SHIFT_MASK &&
+ event->motion.state & GDK_CONTROL_MASK) {
+ delta_angle = 0.0;
+ } else if (event->motion.state & GDK_SHIFT_MASK) {
+ delta_angle = std::round(delta_angle / rotation_snap) * rotation_snap;
+ } else if (event->motion.state & GDK_CONTROL_MASK) {
+ // ?
+ } else if (event->motion.state & GDK_MOD1_MASK) {
+ // Decimal raw angle
+ } else {
+ delta_angle = std::floor(delta_angle);
+ }
+ angle = start_angle + delta_angle;
+
+ _desktop->rotate_relative_keep_point(_desktop->w2d(Geom::Rect(_desktop->canvas->get_area_world()).midpoint()),
+ Geom::rad_from_deg(angle - current_angle));
+ current_angle = angle;
+ ret = true;
+ }
+ 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 (event->button.button == 2 && rotating) {
+ rotating = false;
+ ungrabCanvasEvents();
+ }
+
+ if (middle_mouse_zoom && within_tolerance && (panning || zoom_rb)) {
+ zoom_rb = 0;
+
+ if (panning) {
+ panning = PANNING_NONE;
+ ungrabCanvasEvents();
+ }
+
+ auto const event_w = Geom::Point(event->button.x, event->button.y);
+ auto 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)
+ auto const motion_w = Geom::Point(event->button.x, event->button.y);
+ auto 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;
+
+ // TODO: make these keys customizable
+ case GDK_KEY_F:
+ case GDK_KEY_f:
+ if (!MOD__SHIFT(event) && !MOD__CTRL(event) && !MOD__ALT(event)) {
+ _desktop->quick_preview(true);
+ 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 = std::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 = std::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 = std::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 = std::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:
+ menu_popup(event);
+ ret = true;
+ break;
+
+ case GDK_KEY_F10:
+ if (MOD__SHIFT_ONLY(event)) {
+ menu_popup(event);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_space:
+ within_tolerance = true;
+ xp = yp = 0;
+ if (!allow_panning) break;
+ panning = PANNING_SPACE;
+ 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 (is_space_panning()) {
+ 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;
+
+ // TODO: make these keys customizable
+ case GDK_KEY_F:
+ case GDK_KEY_f:
+ _desktop->quick_preview(false);
+ ret = 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)
+ double delta_x = 0;
+ double 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 = std::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) {
+ auto 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 = std::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) {
+ auto 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:
+ _button1on = true;
+ break;
+ case 2:
+ _button2on = true;
+ break;
+ case 3:
+ _button3on = true;
+ break;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ switch (event->button.button) {
+ case 1:
+ _button1on = false;
+ break;
+ case 2:
+ _button2on = false;
+ break;
+ case 3:
+ _button3on = false;
+ break;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ _button1on = event->motion.state & Gdk::ModifierType::BUTTON1_MASK;
+ _button2on = event->motion.state & Gdk::ModifierType::BUTTON2_MASK;
+ _button3on = event->motion.state & Gdk::ModifierType::BUTTON3_MASK;
+ break;
+ }
+}
+
+bool ToolBase::are_buttons_1_and_3_on() const
+{
+ return _button1on && _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)
+{
+ bool ret = false;
+
+ if (event->type == 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))) {
+ menu_popup(event);
+ ret = true;
+ } else if (event->button.button == 1 && shape_editor && shape_editor->has_knotholder()) {
+ // This allows users to select an arbitary position in a pattern to edit on canvas.
+ auto knotholder = shape_editor->knotholder;
+ auto point = Geom::Point(event->button.x, event->button.y);
+ if (_desktop->getItemAtPoint(point, true) == knotholder->getItem()) {
+ ret = knotholder->set_item_clickpos(_desktop->w2d(point) * _desktop->dt2doc());
+ }
+ }
+ }
+
+ 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 (shape_editor) {
+ return 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); // 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)
+{
+ if (auto window = _desktop->getToplevel()->get_window()) {
+ window->set_event_compression(!high_precision);
+ }
+}
+
+Geom::Point ToolBase::setup_for_drag_start(GdkEvent *ev)
+{
+ xp = ev->button.x;
+ yp = ev->button.y;
+ within_tolerance = true;
+
+ auto const p = Geom::Point(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);
+}
+
+/**
+ * Calls virtual set() function of ToolBase.
+ */
+void sp_event_context_read(ToolBase *ec, char const *key)
+{
+ if (!ec || !key) return;
+ 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 tool_root_handler(event);
+ }
+
+ switch (event->type) {
+ case GDK_MOTION_NOTIFY:
+ snap_delay_handler(nullptr, nullptr, reinterpret_cast<GdkEventMotion*>(event),
+ DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER);
+ break;
+ case GDK_BUTTON_RELEASE:
+ // If we have any pending snapping action, then invoke it now
+ process_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 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 = 0;
+
+ // Just set the on buttons for now. later, behave as intended.
+ 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 (is_panning()) {
+ ret = ToolBase::root_handler(event);
+ } else {
+ ret = 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 virtual_item_handler(item, event);
+ }
+
+ switch (event->type) {
+ case GDK_MOTION_NOTIFY:
+ snap_delay_handler(item, nullptr, reinterpret_cast<GdkEventMotion*>(event),
+ DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER);
+ break;
+ case GDK_BUTTON_RELEASE:
+ // If we have any pending snapping action, then invoke it now
+ process_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.
+ 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 = 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;
+ }
+
+ auto const button_w = Geom::Point(event->button.x, event->button.y);
+ auto 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);
+ }
+ }
+
+ auto 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, char const *ctrl_tip, char const *shift_tip,
+ char 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);
+
+ char *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 (std::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 (int 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), nullptr);
+ 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 /*= nullptr*/)
+{
+ 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;
+ }
+#ifndef __APPLE__
+ // on macOS <option> key inserts special characters and below condition fires all the time
+ 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;
+ }
+#endif
+
+ 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->getSelection()->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) { // 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 timeout).
+ *
+ * @param item Pointer that store a reference to a canvas or to an item.
+ * @param 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 ToolBase::snap_delay_handler(gpointer item, gpointer item2, GdkEventMotion const *event, DelayedSnapEvent::DelayedSnapEventOrigin origin)
+{
+ static guint32 prev_time;
+ static std::optional<Geom::Point> prev_pos;
+
+ if (!_uses_snap || _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*>(this);
+ // 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 = 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
+ discard_delayed_snap_event();
+ } else if (getDesktop() && 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.
+ 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;
+ double 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)
+ _dse.emplace(this, item, item2, event, origin);
+ _schedule_delayed_snap_event(); // 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 (!_dse) { // 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
+ _dse.emplace(this, item, item2, event, origin);
+ _schedule_delayed_snap_event();
+ } // 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(!_dse);
+ _dse.emplace(this, item, item2, event, origin);
+ _schedule_delayed_snap_event();
+ }
+
+ prev_pos = event_pos;
+ prev_time = event_t;
+ }
+}
+
+/**
+ * When the delayed snap event timer expires, this method will be called and will re-inject the last motion
+ * event in an appropriate place, with snapping being turned on again.
+ */
+void ToolBase::process_delayed_snap_event()
+{
+ // Snap NOW! For this the "postponed" flag will be reset and the last motion event will be repeated
+
+ _dse_timeout_conn.disconnect();
+
+ if (!_dse) {
+ // 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;
+ }
+
+ auto dt = getDesktop();
+ if (!dt) {
+ _dse.reset();
+ return;
+ }
+
+ _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 its origin.
+ // The switch below takes care of that and prepares the relevant parameters.
+ switch (_dse->getOrigin()) {
+ case DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER:
+ tool_root_handler(_dse->getEvent());
+ break;
+ case DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER: {
+ auto item = reinterpret_cast<SPItem*>(_dse->getItem());
+ if (item) {
+ virtual_item_handler(item, _dse->getEvent());
+ }
+ break;
+ }
+ case DelayedSnapEvent::KNOT_HANDLER: {
+ auto knot = reinterpret_cast<SPKnot*>(_dse->getItem2());
+ check_if_knot_deleted(knot);
+ if (knot) {
+ bool was_grabbed = knot->is_grabbed();
+ knot->setFlag(SP_KNOT_GRABBED, true); // Must be grabbed for Inkscape::SelTrans::handleRequest() to pass
+ sp_knot_handler_request_position(_dse->getEvent(), knot);
+ knot->setFlag(SP_KNOT_GRABBED, was_grabbed);
+ }
+ break;
+ }
+ case DelayedSnapEvent::CONTROL_POINT_HANDLER: {
+ using Inkscape::UI::ControlPoint;
+ auto point = reinterpret_cast<ControlPoint*>(_dse->getItem2());
+ if (point) {
+ if (point->position().isFinite() && dt == point->_desktop) {
+ point->_eventHandler(this, _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 guideline = reinterpret_cast<CanvasItemGuideLine*>(_dse->getItem());
+ auto guide = reinterpret_cast<SPGuide*> (_dse->getItem2());
+ if (guideline && guide) {
+ sp_dt_guide_event(_dse->getEvent(), guideline, guide);
+ }
+ break;
+ }
+ case DelayedSnapEvent::GUIDE_HRULER:
+ case DelayedSnapEvent::GUIDE_VRULER: {
+ gpointer item = _dse->getItem();
+ auto widget = reinterpret_cast<Gtk::Widget*>(_dse->getItem2());
+ if (item && widget) {
+ g_assert(GTK_IS_WIDGET(item));
+ bool horiz = _dse->getOrigin() == DelayedSnapEvent::GUIDE_HRULER;
+ SPDesktopWidget::ruler_event(GTK_WIDGET(item), _dse->getEvent(), SP_DESKTOP_WIDGET(widget), horiz);
+ }
+ break;
+ }
+ default:
+ g_warning("Origin of snap-delay event has not been defined!");
+ break;
+ }
+
+ _dse_callback_in_process = false;
+ _dse.reset();
+}
+
+/**
+ * If a delayed snap event has been scheduled, this function will cancel it.
+ */
+void ToolBase::discard_delayed_snap_event()
+{
+ _dse_timeout_conn.disconnect();
+ _desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false);
+ _dse.reset();
+}
+
+/**
+ * Internal function used to set process_delayed_snap_event() to occur a given delay in the future
+ * from now. Subsequent calls will reset the timer. Calling process_delayed_snap_event() manually
+ * will cancel the timer.
+ */
+void ToolBase::_schedule_delayed_snap_event()
+{
+ // Get timeout value in seconds.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value = prefs->getDoubleLimited("/options/snapdelay/value", 0, 0, 1000);
+
+ // If the timeout value is too large, we assume it comes from an old preferences file
+ // where it used to be measured in milliseconds, and convert it appropriately.
+ if (value > 1.0) {
+ value /= 1000.0; // convert milliseconds to seconds
+ }
+
+ _dse_timeout_conn.disconnect();
+ _dse_timeout_conn = Glib::signal_timeout().connect([this] {
+ process_delayed_snap_event();
+ return false; // one-shot
+ }, value * 1000.0);
+}
+
+} // 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/tool-base.h b/src/ui/tools/tool-base.h
new file mode 100644
index 0000000..aaf0b9a
--- /dev/null
+++ b/src/ui/tools/tool-base.h
@@ -0,0 +1,262 @@
+// 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 <optional>
+
+#include <boost/noncopyable.hpp>
+#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 UI {
+class ShapeEditor;
+
+namespace Tools {
+class ToolBase;
+
+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 *tool, gpointer item, gpointer item2, GdkEventMotion const *event,
+ DelayedSnapEvent::DelayedSnapEventOrigin origin)
+ : _tool(tool)
+ , _item(item)
+ , _item2(item2)
+ , _origin(origin)
+ {
+ _event = gdk_event_copy(reinterpret_cast<GdkEvent const*>(event));
+ _event->motion.time = GDK_CURRENT_TIME;
+ }
+
+ ~DelayedSnapEvent()
+ {
+ gdk_event_free(_event);
+ }
+
+ ToolBase *getEventContext() const { return _tool; }
+ gpointer getItem() const { return _item; }
+ gpointer getItem2() const { return _item2; }
+ GdkEvent *getEvent() const { return _event; }
+ DelayedSnapEventOrigin getOrigin() const { return _origin; }
+
+private:
+ ToolBase *_tool;
+ gpointer _item;
+ gpointer _item2;
+ GdkEvent *_event;
+ 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
+ , boost::noncopyable
+{
+public:
+ ToolBase(SPDesktop *desktop, std::string prefs_path, std::string cursor_filename, bool uses_snap = true);
+ virtual ~ToolBase();
+
+ 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);
+ virtual bool catch_undo(bool redo = false) { return false; }
+ virtual bool can_undo(bool redo = false) { return false; }
+ virtual bool is_ready() const { return true; }
+
+ 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 const &getPrefsPath() const { return _prefs_path; };
+ void enableSelectionCue(bool enable = true);
+
+ Inkscape::MessageContext *defaultMessageContext() const { return message_context.get(); }
+
+ SPDesktop *getDesktop() const { 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();
+
+ virtual void switching_away(const std::string &new_tool) {}
+private:
+ std::unique_ptr<Inkscape::Preferences::PreferencesObserver> pref_observer;
+ std::string _prefs_path;
+
+protected:
+ Glib::RefPtr<Gdk::Cursor> _cursor;
+ std::string _cursor_filename = "select.svg";
+ std::string _cursor_default = "select.svg";
+
+ int xp = 0; ///< where drag started
+ int yp = 0; ///< where drag started
+ int 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;
+
+ bool rotating = false;
+ double start_angle, current_angle;
+
+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; }
+
+ std::unique_ptr<Inkscape::MessageContext> message_context;
+ Inkscape::SelCue *_selcue = nullptr;
+
+ GrDrag *_grdrag = nullptr;
+
+ ShapeEditor *shape_editor = nullptr;
+
+ void snap_delay_handler(gpointer item, gpointer item2, GdkEventMotion const *event, DelayedSnapEvent::DelayedSnapEventOrigin origin);
+ void process_delayed_snap_event();
+ void discard_delayed_snap_event();
+ bool _uses_snap = false;
+
+ 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 const &filename) const;
+ 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);
+
+ SPDesktop *_desktop = nullptr;
+
+private:
+ bool _keyboardMove(GdkEventKey const &event, Geom::Point const &dir);
+
+ std::optional<DelayedSnapEvent> _dse;
+ void _schedule_delayed_snap_event();
+ sigc::connection _dse_timeout_conn;
+ bool _dse_callback_in_process = false;
+};
+
+void sp_event_context_read(ToolBase *ec, char const *key);
+
+void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event);
+
+gint gobble_key_events(guint keyval, guint mask);
+void gobble_motion_events(guint mask);
+
+void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, GdkEvent *event,
+ char const *ctrl_tip, char const *shift_tip, char const *alt_tip);
+
+void init_latin_keys_group();
+unsigned get_latin_keyval(GdkEventKey const *event, unsigned *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..808a4b4
--- /dev/null
+++ b/src/ui/tools/tweak-tool.cpp
@@ -0,0 +1,1482 @@
+// 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)
+ , do_h(true)
+ , do_s(true)
+ , do_l(true)
+ , do_o(false)
+{
+ dilate_area = make_canvasitem<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");
+
+ 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()
+{
+ enableGrDrag(false);
+}
+
+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->getSelection()->isEmpty()) {
+ num = (guint)boost::distance(_desktop->getSelection()->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;
+
+ {
+ auto box = 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 (is<SPText>(item) || is<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 = cast<SPItem>(newObj);
+ g_assert(item != nullptr);
+ selection->add(item);
+ }
+
+ if (is<SPGroup>(item) && !is<SPBox3D>(item)) {
+ std::vector<SPItem *> children;
+ for (auto& child: item->children) {
+ if (is<SPItem>(&child)) {
+ children.push_back(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 (is<SPPath>(item) || is<SPShape>(item)) {
+
+ Inkscape::XML::Node *newrepr = nullptr;
+ gint pos = 0;
+ Inkscape::XML::Node *parent = nullptr;
+ char const *id = nullptr;
+ if (!is<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 {
+ auto lpeitem = 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) {
+ 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
+
+ auto lg = cast<SPLinearGradient>(gradient);
+ auto rg = 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) {
+ auto stop = cast<SPStop>(&child);
+ if (!stop) {
+ continue;
+ }
+
+ offset_h = stop->offset;
+
+ if (child_prev) {
+ auto prevStop = 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
+ auto mg = cast<SPMeshGradient>(gradient);
+ if (mg) {
+ auto mg_array = 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 (is<SPGroup>(item)) {
+ for (auto& child: item->children) {
+ auto childItem = 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) {
+ auto primitive = cast<SPFilterPrimitive>(&primitive_obj);
+ if (primitive) {
+ //if primitive is gaussianblur
+ auto spblur = cast<SPGaussianBlur>(primitive);
+ if (spblur) {
+ float num = spblur->get_std_deviation().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->getSelection()->isEmpty()) {
+ num = (guint)boost::distance(_desktop->getSelection()->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..77bfb1f
--- /dev/null
+++ b/src/ui/tools/tweak-tool.h
@@ -0,0 +1,107 @@
+// 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 <2geom/point.h>
+#include "ui/tools/tool-base.h"
+#include "display/control/canvas-item-ptr.h"
+#include "helper/auto-connection.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 */
+ double pressure;
+
+ /* attributes */
+ bool dragging; /* mouse state: mouse is dragging */
+ bool usepressure;
+ bool usetilt;
+
+ double width;
+ double force;
+ double fidelity;
+
+ int mode;
+
+ bool is_drawing;
+
+ bool is_dilating;
+ bool has_dilated;
+ Geom::Point last_push;
+ CanvasItemPtr<CanvasItemBpath> dilate_area;
+
+ bool do_h;
+ bool do_s;
+ bool do_l;
+ bool do_o;
+
+ auto_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