diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
commit | c853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch) | |
tree | 7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/ui/tools | |
parent | Initial commit. (diff) | |
download | inkscape-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')
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 × %s (constrained to ratio %d:%d); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str(), ratio_x, ratio_y); + } else { + if (ratio_y == 1) { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Ellipse</b>: %s × %s (constrained to golden ratio 1.618 : 1); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Ellipse</b>: %s × %s (constrained to golden ratio 1 : 1.618); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } + } + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Ellipse</b>: %s × %s; with <b>Ctrl</b> to make circle, integer-ratio, or golden-ratio ellipse; with <b>Shift</b> to draw around the starting point"), xs.c_str(), ys.c_str()); + } +} + +void ArcTool::finishItem() { + this->message_context->clear(); + + if (this->arc != nullptr) { + if (this->arc->rx.computed == 0 || this->arc->ry.computed == 0) { + this->cancel(); // Don't allow the creating of zero sized arc, for example when the start and and point snap to the snap grid point + return; + } + + this->arc->updateRepr(); + this->arc->doWriteTransform(this->arc->transform, nullptr, true); + + _desktop->getSelection()->set(this->arc); + + DocumentUndo::done(_desktop->getDocument(), _("Create ellipse"), INKSCAPE_ICON("draw-ellipse")); + + this->arc = nullptr; + } +} + +void ArcTool::cancel() { + _desktop->getSelection()->clear(); + ungrabCanvasEvents(); + + if (this->arc != nullptr) { + this->arc->deleteObject(); + this->arc = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + DocumentUndo::cancel(_desktop->getDocument()); +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/arc-tool.h b/src/ui/tools/arc-tool.h new file mode 100644 index 0000000..312f943 --- /dev/null +++ b/src/ui/tools/arc-tool.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_ARC_CONTEXT_H +#define SEEN_ARC_CONTEXT_H + +/* + * Ellipse drawing context + * + * Authors: + * Mitsuru Oka + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2000-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> + +#include <2geom/point.h> +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" + +class SPItem; +class SPGenericEllipse; + +namespace Inkscape { + class Selection; +} + +#define SP_ARC_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ArcTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_ARC_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::ArcTool*>(obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ArcTool : public ToolBase { +public: + ArcTool(SPDesktop *desktop); + ~ArcTool() override; + + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; +private: + SPGenericEllipse *arc; + + Geom::Point center; + + sigc::connection sel_changed_connection; + + void selection_changed(Inkscape::Selection* selection); + + void drag(Geom::Point pt, guint state); + void finishItem(); + void cancel(); +}; + +} +} +} + +#endif /* !SEEN_ARC_CONTEXT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/tools/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 ©) + : 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(¤tcurve, 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(¤tcurve, 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(¤tcurve, 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(¤tcurve, 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 ¢er, 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 ¢er, 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°, 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°; <b>Shift+Click</b> creates cusp node, <b>ALT</b> moves previous, <b>Enter</b> or <b>Shift+Enter</b> to finish" ): + _("<b>Line segment</b>: angle %3.2f°; <b>Shift+Click</b> creates cusp node, <b>ALT</b> moves previous, <b>Enter</b> or <b>Shift+Enter</b> to finish"); + this->_setAngleDistanceStatusMessage(p, 0, message); + } else { + message = is_curve ? + _("<b>Curve segment</b>: angle %3.2f°, distance %s; with <b>Ctrl</b> to snap angle, <b>Enter</b> or <b>Shift+Enter</b> to finish the path" ): + _("<b>Line segment</b>: angle %3.2f°, distance %s; with <b>Ctrl</b> to snap angle, <b>Enter</b> or <b>Shift+Enter</b> to finish the path"); + this->_setAngleDistanceStatusMessage(p, 0, message); + } + + } +} + +void PenTool::_setCtrl(Geom::Point const q, guint const state) +{ + // use 'q' as 'p' shadows member variable. + 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°, 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°, length %s; with <b>Ctrl</b> to snap angle, with <b>Shift</b> to move this handle only") : + _("<b>Curve handle</b>: angle %3.2f°, length %s; with <b>Ctrl</b> to snap angle, with <b>Shift</b> to move this handle only"); + this->_setAngleDistanceStatusMessage(q, 3, message); + } else { + g_warning("Something bad happened - npoints is %d", this->npoints); + } +} + +void PenTool::_finishSegment(Geom::Point const q, guint const state) { // use 'q' as 'p' shadows member variable. + if (this->polylines_paraxial) { + this->nextParaxialDirection(q, this->p[0], state); + } + + 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 × %s (constrained to ratio %d:%d); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str(), ratio_x, ratio_y); + } else { + if (ratio_y == 1) { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s (constrained to golden ratio 1.618 : 1); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s (constrained to golden ratio 1 : 1.618); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } + } + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s; with <b>Ctrl</b> to make square, integer-ratio, or golden-ratio rectangle; with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } +} + +void RectTool::finishItem() { + this->message_context->clear(); + + if (this->rect != nullptr) { + if (this->rect->width.computed == 0 || this->rect->height.computed == 0) { + this->cancel(); // Don't allow the creating of zero sized rectangle, for example when the start and and point snap to the snap grid point + return; + } + + this->rect->updateRepr(); + this->rect->doWriteTransform(this->rect->transform, nullptr, true); + + _desktop->getSelection()->set(this->rect); + + DocumentUndo::done(_desktop->getDocument(), _("Create rectangle"), INKSCAPE_ICON("draw-rectangle")); + + this->rect = nullptr; + } +} + +void RectTool::cancel(){ + _desktop->getSelection()->clear(); + ungrabCanvasEvents(); + + if (this->rect != nullptr) { + this->rect->deleteObject(); + this->rect = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + DocumentUndo::cancel(_desktop->getDocument()); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/rect-tool.h b/src/ui/tools/rect-tool.h new file mode 100644 index 0000000..79d1a8a --- /dev/null +++ b/src/ui/tools/rect-tool.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_RECT_CONTEXT_H__ +#define __SP_RECT_CONTEXT_H__ + +/* + * Rectangle drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +class SPRect; + +namespace Inkscape { + +class Selection; + +namespace UI { +namespace Tools { + +class RectTool : public ToolBase { +public: + RectTool(SPDesktop *desktop); + ~RectTool() override; + + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; +private: + SPRect *rect; + Geom::Point center; + + gdouble rx; /* roundness radius (x direction) */ + gdouble ry; /* roundness radius (y direction) */ + + sigc::connection sel_changed_connection; + + void drag(Geom::Point const pt, guint state); + void finishItem(); + void cancel(); + void selection_changed(Inkscape::Selection* selection); +}; + +} +} +} + +#endif diff --git a/src/ui/tools/select-tool.cpp b/src/ui/tools/select-tool.cpp new file mode 100644 index 0000000..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°; 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°; with <b>Ctrl</b> to snap angle") : + _("<b>Star</b>: radius %s, angle %.2f°; with <b>Ctrl</b> to snap angle") ), + rads.c_str(), arg1 * 180 / M_PI); +} + +void StarTool::finishItem() { + this->message_context->clear(); + + if (this->star != nullptr) { + if (this->star->r[1] == 0) { + // Don't allow the creating of zero sized arc, for example + // when the start and and point snap to the snap grid point + this->cancel(); + return; + } + + // Set transform center, so that odd stars rotate correctly + // LP #462157 + this->star->setCenter(this->center); + this->star->set_shape(); + this->star->updateRepr(SP_OBJECT_WRITE_EXT); + // compensate stroke scaling couldn't be done in doWriteTransform + double const expansion = this->star->transform.descrim(); + this->star->doWriteTransform(this->star->transform, nullptr, true); + this->star->adjust_stroke_width_recursive(expansion); + + _desktop->getSelection()->set(this->star); + DocumentUndo::done(_desktop->getDocument(), _("Create star"), INKSCAPE_ICON("draw-polygon-star")); + + this->star = nullptr; + } +} + +void StarTool::cancel() { + _desktop->getSelection()->clear(); + ungrabCanvasEvents(); + + if (this->star != nullptr) { + this->star->deleteObject(); + this->star = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + DocumentUndo::cancel(_desktop->getDocument()); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/star-tool.h b/src/ui/tools/star-tool.h new file mode 100644 index 0000000..4a06a42 --- /dev/null +++ b/src/ui/tools/star-tool.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_STAR_CONTEXT_H__ +#define __SP_STAR_CONTEXT_H__ + +/* + * Star drawing context + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2001-2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +class SPStar; + +namespace Inkscape { + +class Selection; + +namespace UI { +namespace Tools { + +class StarTool : public ToolBase { +public: + StarTool(SPDesktop *desktop); + ~StarTool() override; + + void set(const Inkscape::Preferences::Entry &val) override; + bool root_handler(GdkEvent *event) override; + +private: + SPStar *star; + + Geom::Point center; + + /* Number of corners */ + gint magnitude; + + /* Outer/inner radius ratio */ + gdouble proportion; + + /* flat sides or not? */ + bool isflatsided; + + /* rounded corners ratio */ + gdouble rounded; + + // randomization + gdouble randomized; + + sigc::connection sel_changed_connection; + + void drag(Geom::Point p, guint state); + void finishItem(); + void cancel(); + void selection_changed(Inkscape::Selection* selection); +}; + +} +} +} + +#endif diff --git a/src/ui/tools/text-tool.cpp b/src/ui/tools/text-tool.cpp new file mode 100644 index 0000000..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, "<"); break; + case '>': strcpy(utf8, ">"); break; + case '&': strcpy(utf8, "&"); break; + default: break; + } + } + tc->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE, + _("Unicode (<b>Enter</b> to finish): %s: %s"), tc->uni, utf8); + } else { + tc->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): ")); + } +} + +bool TextTool::root_handler(GdkEvent* event) { + +#if EVENT_DEBUG + ui_dump_event(reinterpret_cast<GdkEvent *>(event), "TextTool::root_handler"); +#endif + + indicator->hide(); + + sp_text_context_validate_cursor_iterators(this); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + if (Inkscape::have_viable_layer(_desktop, _desktop->getMessageStack()) == false) { + return TRUE; + } + + // save drag origin + this->xp = (gint) event->button.x; + this->yp = (gint) event->button.y; + this->within_tolerance = true; + + Geom::Point const button_pt(event->button.x, event->button.y); + Geom::Point button_dt(_desktop->w2d(button_pt)); + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + this->p0 = button_dt; + Inkscape::Rubberband::get(_desktop)->start(_desktop, this->p0); + + grabCanvasEvents(); + + this->creating = true; + + /* Processed */ + return TRUE; + } + break; + case GDK_MOTION_NOTIFY: { + if (this->creating && (event->motion.state & GDK_BUTTON1_MASK)) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_pt(event->motion.x, event->motion.y); + Geom::Point p = _desktop->w2d(motion_pt); + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + Inkscape::Rubberband::get(_desktop)->move(p); + gobble_motion_events(GDK_BUTTON1_MASK); + + // status text + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(fabs((p - this->p0)[Geom::X]), "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(fabs((p - this->p0)[Geom::Y]), "px"); + Glib::ustring xs = x_q.string(_desktop->namedview->display_units); + Glib::ustring ys = y_q.string(_desktop->namedview->display_units); + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Flowed text frame</b>: %s × %s"), xs.c_str(), ys.c_str()); + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(_desktop->w2d(motion_w)); + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + if ((event->motion.state & GDK_BUTTON1_MASK) && this->dragging) { + Inkscape::Text::Layout const *layout = te_get_layout(this->text); + if (!layout) + break; + // find out click point in document coordinates + Geom::Point p = _desktop->w2d(Geom::Point(event->button.x, event->button.y)); + // set the cursor closest to that point + Inkscape::Text::Layout::iterator new_end = sp_te_get_position_by_coords(this->text, p); + if (this->dragging == 2) { + // double-click dragging: go by word + if (new_end < this->text_sel_start) { + if (!layout->isStartOfWord(new_end)) + new_end.prevStartOfWord(); + } else if (!layout->isEndOfWord(new_end)) + new_end.nextEndOfWord(); + } else if (this->dragging == 3) { + // triple-click dragging: go by line + if (new_end < this->text_sel_start) + new_end.thisStartOfLine(); + else + new_end.thisEndOfLine(); + } + // update display + if (this->text_sel_end != new_end) { + this->text_sel_end = new_end; + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + } + gobble_motion_events(GDK_BUTTON1_MASK); + break; + } + // find out item under mouse, disregarding groups + SPItem *item_ungrouped = + _desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE, nullptr); + if (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 |