diff options
| author | Markus Engel <markus.engel@tum.de> | 2013-11-09 22:36:13 +0000 |
|---|---|---|
| committer | Markus Engel <markus.engel@tum.de> | 2013-11-09 22:36:13 +0000 |
| commit | c04e30df241a3ee039077425bab9b9c37abe2854 (patch) | |
| tree | 6b7904966a289832bca2636c3117c893592e5ddd /src/ui | |
| parent | added missing translation flags and a small text change (diff) | |
| download | inkscape-c04e30df241a3ee039077425bab9b9c37abe2854.tar.gz inkscape-c04e30df241a3ee039077425bab9b9c37abe2854.zip | |
Moved and renamed some tool-related files.
(bzr r12785)
Diffstat (limited to 'src/ui')
68 files changed, 24160 insertions, 23 deletions
diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 24324c874..c3e406e3f 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -14,13 +14,38 @@ set(ui_SRC tool/manipulator.cpp tool/modifier-tracker.cpp tool/multi-path-manipulator.cpp - tool/node-tool.cpp tool/node.cpp tool/path-manipulator.cpp tool/selectable-control-point.cpp tool/selector.cpp tool/transform-handle-set.cpp + tools/arc-tool.cpp + tools/box3d-tool.cpp + tools/calligraphic-tool.cpp + tools/connector-tool.cpp + tools/dropper-tool.cpp + tools/dynamic-base.cpp + tools/eraser-tool.cpp + tools/flood-tool.cpp + tools/freehand-base.cpp + tools/gradient-tool.cpp + tools/lpe-tool.cpp + tools/measure-tool.cpp + tools/mesh-tool.cpp + tools/node-tool.cpp + tools/pencil-tool.cpp + tools/pen-tool.cpp + tools/rect-tool.cpp + tools/select-tool.cpp + tools/spiral-tool.cpp + tools/spray-tool.cpp + tools/star-tool.cpp + tools/text-tool.cpp + tools/tool-base.cpp + tools/tweak-tool.cpp + tools/zoom-tool.cpp + dialog/aboutbox.cpp dialog/align-and-distribute.cpp dialog/calligraphic-profile-rename.cpp @@ -200,7 +225,6 @@ set(ui_SRC tool/manipulator.h tool/modifier-tracker.h tool/multi-path-manipulator.h - tool/node-tool.h tool/node-types.h tool/node.h tool/path-manipulator.h @@ -209,6 +233,32 @@ set(ui_SRC tool/shape-record.h tool/transform-handle-set.h + tools/arc-tool.h + tools/box3d-tool.h + tools/calligraphic-tool.h + tools/connector-tool.h + tools/dropper-tool.h + tools/dynamic-base.h + tools/eraser-tool.h + tools/flood-tool.h + tools/freehand-base.h + tools/gradient-tool.h + tools/lpe-tool.h + tools/measure-tool.h + tools/mesh-tool.h + tools/node-tool.h + tools/pencil-tool.h + tools/pen-tool.h + tools/rect-tool.h + tools/select-tool.h + tools/spiral-tool.h + tools/spray-tool.h + tools/star-tool.h + tools/text-tool.h + tools/tool-base.h + tools/tweak-tool.h + tools/zoom-tool.h + widget/attr-widget.h widget/button.h widget/color-picker.h diff --git a/src/ui/clipboard.cpp b/src/ui/clipboard.cpp index ffb2bdf8c..8e2502545 100644 --- a/src/ui/clipboard.cpp +++ b/src/ui/clipboard.cpp @@ -44,7 +44,7 @@ #include "selection.h" #include "message-stack.h" #include "context-fns.h" -#include "dropper-context.h" // used in copy() +#include "ui/tools/dropper-tool.h" // used in copy() #include "style.h" #include "extension/db.h" // extension database #include "extension/input.h" @@ -74,7 +74,7 @@ #include "live_effects/parameter/path.h" #include "svg/svg.h" // for sp_svg_transform_write, used in _copySelection #include "svg/css-ostringstream.h" // used in copy -#include "text-context.h" +#include "ui/tools/text-tool.h" #include "text-editing.h" #include "tools-switch.h" #include "path-chemistry.h" diff --git a/src/ui/dialog/align-and-distribute.cpp b/src/ui/dialog/align-and-distribute.cpp index 4a3852d5d..6b4aeebb9 100644 --- a/src/ui/dialog/align-and-distribute.cpp +++ b/src/ui/dialog/align-and-distribute.cpp @@ -40,7 +40,7 @@ #include "text-editing.h" #include "tools-switch.h" #include "ui/icon-names.h" -#include "ui/tool/node-tool.h" +#include "ui/tools/node-tool.h" #include "ui/tool/multi-path-manipulator.h" #include "util/glib-list-iterators.h" #include "verbs.h" diff --git a/src/ui/dialog/dialog.cpp b/src/ui/dialog/dialog.cpp index 7fc9c3889..3cc3f3d88 100644 --- a/src/ui/dialog/dialog.cpp +++ b/src/ui/dialog/dialog.cpp @@ -23,7 +23,7 @@ #include <gdk/gdkkeysyms.h> #include "inkscape.h" -#include "event-context.h" +#include "ui/tools/tool-base.h" #include "desktop.h" #include "desktop-handles.h" #include "modifier-fns.h" diff --git a/src/ui/dialog/guides.cpp b/src/ui/dialog/guides.cpp index 5dfafa78d..e84f25733 100644 --- a/src/ui/dialog/guides.cpp +++ b/src/ui/dialog/guides.cpp @@ -25,7 +25,7 @@ #include "sp-guide.h" #include "sp-namedview.h" #include "desktop-handles.h" -#include "event-context.h" +#include "ui/tools/tool-base.h" #include "widgets/desktop-widget.h" #include <glibmm/i18n.h> #include "dialogs/dialog-events.h" diff --git a/src/ui/dialog/layer-properties.cpp b/src/ui/dialog/layer-properties.cpp index c726f93af..f19828b1f 100644 --- a/src/ui/dialog/layer-properties.cpp +++ b/src/ui/dialog/layer-properties.cpp @@ -31,7 +31,7 @@ #include "selection-chemistry.h" #include "ui/icon-names.h" #include "ui/widget/imagetoggler.h" -#include "event-context.h" +#include "ui/tools/tool-base.h" namespace Inkscape { namespace UI { diff --git a/src/ui/dialog/layers.cpp b/src/ui/dialog/layers.cpp index ff9265c8d..12fdf6b7d 100644 --- a/src/ui/dialog/layers.cpp +++ b/src/ui/dialog/layers.cpp @@ -41,7 +41,7 @@ #include "widgets/icon.h" #include "xml/repr.h" #include "sp-root.h" -#include "event-context.h" +#include "ui/tools/tool-base.h" #include "selection-chemistry.h" //#define DUMP_LAYERS 1 diff --git a/src/ui/dialog/spellcheck.cpp b/src/ui/dialog/spellcheck.cpp index 45106755c..00bd445bf 100644 --- a/src/ui/dialog/spellcheck.cpp +++ b/src/ui/dialog/spellcheck.cpp @@ -24,7 +24,7 @@ #include "desktop.h" #include "desktop-handles.h" #include "tools-switch.h" -#include "text-context.h" +#include "ui/tools/text-tool.h" #include "interface.h" #include "preferences.h" #include "sp-text.h" diff --git a/src/ui/dialog/xml-tree.cpp b/src/ui/dialog/xml-tree.cpp index dfddfb1d3..0e1e9f7a6 100644 --- a/src/ui/dialog/xml-tree.cpp +++ b/src/ui/dialog/xml-tree.cpp @@ -27,7 +27,7 @@ #include "dialogs/dialog-events.h" #include "document.h" #include "document-undo.h" -#include "event-context.h" +#include "ui/tools/tool-base.h" #include "helper/window.h" #include "inkscape.h" #include "interface.h" diff --git a/src/ui/tool/Makefile_insert b/src/ui/tool/Makefile_insert index 2a72dd645..f46f48b72 100644 --- a/src/ui/tool/Makefile_insert +++ b/src/ui/tool/Makefile_insert @@ -19,8 +19,6 @@ ink_common_sources += \ ui/tool/node.cpp \ ui/tool/node.h \ ui/tool/node-types.h \ - ui/tool/node-tool.cpp \ - ui/tool/node-tool.h \ ui/tool/path-manipulator.cpp \ ui/tool/path-manipulator.h \ ui/tool/selectable-control-point.cpp \ diff --git a/src/ui/tool/control-point.cpp b/src/ui/tool/control-point.cpp index 3f1587492..e98c7b2a2 100644 --- a/src/ui/tool/control-point.cpp +++ b/src/ui/tool/control-point.cpp @@ -14,7 +14,7 @@ #include "desktop-handles.h" #include "display/sp-canvas.h" #include "display/snap-indicator.h" -#include "event-context.h" +#include "ui/tools/tool-base.h" #include "message-context.h" #include "preferences.h" #include "snap-preferences.h" diff --git a/src/ui/tool/manipulator.h b/src/ui/tool/manipulator.h index 2e6d80517..07e01a656 100644 --- a/src/ui/tool/manipulator.h +++ b/src/ui/tool/manipulator.h @@ -18,7 +18,7 @@ #include <glib.h> #include <gdk/gdk.h> #include <boost/shared_ptr.hpp> -#include "event-context.h" +#include "ui/tools/tool-base.h" class SPDesktop; namespace Inkscape { diff --git a/src/ui/tool/selector.cpp b/src/ui/tool/selector.cpp index d46a160bc..bdeacadc9 100644 --- a/src/ui/tool/selector.cpp +++ b/src/ui/tool/selector.cpp @@ -13,7 +13,7 @@ #include "desktop.h" #include "desktop-handles.h" #include "display/sodipodi-ctrlrect.h" -#include "event-context.h" +#include "ui/tools/tool-base.h" #include "preferences.h" #include "ui/tool/event-utils.h" #include "ui/tool/selector.h" diff --git a/src/ui/tool/transform-handle-set.cpp b/src/ui/tool/transform-handle-set.cpp index 535a138ed..f21e1661a 100644 --- a/src/ui/tool/transform-handle-set.cpp +++ b/src/ui/tool/transform-handle-set.cpp @@ -26,7 +26,7 @@ #include "ui/tool/selectable-control-point.h" #include "ui/tool/event-utils.h" #include "ui/tool/transform-handle-set.h" -#include "ui/tool/node-tool.h" +#include "ui/tools/node-tool.h" #include "ui/tool/node.h" #include "seltrans.h" diff --git a/src/ui/tools/Makefile_insert b/src/ui/tools/Makefile_insert new file mode 100644 index 000000000..cd09a3230 --- /dev/null +++ b/src/ui/tools/Makefile_insert @@ -0,0 +1,28 @@ +## Makefile.am fragment sourced by src/Makefile.am. + +ink_common_sources += \ + ui/tools/arc-tool.cpp ui/tools/arc-tool.h \ + ui/tools/box3d-tool.cpp ui/tools/box3d-tool.h \ + ui/tools/calligraphic-tool.cpp ui/tools/calligraphic-tool.h \ + ui/tools/connector-tool.cpp ui/tools/connector-tool.h \ + ui/tools/dropper-tool.cpp ui/tools/dropper-tool.h \ + ui/tools/dynamic-base.cpp ui/tools/dynamic-base.h \ + ui/tools/eraser-tool.cpp ui/tools/eraser-tool.h \ + ui/tools/flood-tool.cpp ui/tools/flood-tool.h \ + ui/tools/freehand-base.cpp ui/tools/freehand-base.h \ + ui/tools/gradient-tool.cpp ui/tools/gradient-tool.h \ + ui/tools/lpe-tool.cpp ui/tools/lpe-tool.h \ + ui/tools/measure-tool.cpp ui/tools/measure-tool.h \ + ui/tools/mesh-tool.cpp ui/tools/mesh-tool.h \ + ui/tools/node-tool.cpp ui/tools/node-tool.h \ + ui/tools/pen-tool.cpp ui/tools/pen-tool.h \ + ui/tools/pencil-tool.cpp ui/tools/pencil-tool.h \ + ui/tools/rect-tool.cpp ui/tools/rect-tool.h \ + ui/tools/select-tool.cpp ui/tools/select-tool.h \ + ui/tools/spiral-tool.cpp ui/tools/spiral-tool.h \ + ui/tools/spray-tool.cpp ui/tools/spray-tool.h \ + ui/tools/star-tool.cpp ui/tools/star-tool.h \ + ui/tools/text-tool.cpp ui/tools/text-tool.h \ + ui/tools/tool-base.cpp ui/tools/tool-base.h \ + ui/tools/tweak-tool.cpp ui/tools/tweak-tool.h \ + ui/tools/zoom-tool.cpp ui/tools/zoom-tool.h
\ No newline at end of file diff --git a/src/ui/tools/arc-tool.cpp b/src/ui/tools/arc-tool.cpp new file mode 100644 index 000000000..bb7dfa21c --- /dev/null +++ b/src/ui/tools/arc-tool.cpp @@ -0,0 +1,503 @@ +/** + * @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, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif +#include <gdk/gdkkeysyms.h> + +#include "macros.h" +#include <glibmm/i18n.h> +#include "display/sp-canvas.h" +#include "sp-ellipse.h" +#include "document.h" +#include "document-undo.h" +#include "sp-namedview.h" +#include "selection.h" +#include "desktop-handles.h" +#include "snap.h" +#include "pixmaps/cursor-ellipse.xpm" +#include "xml/repr.h" +#include "xml/node-event-vector.h" +#include "preferences.h" +#include "message-context.h" +#include "desktop.h" +#include "desktop-style.h" +#include "context-fns.h" +#include "verbs.h" +#include "shape-editor.h" +#include "ui/tools/tool-base.h" + +#include "ui/tools/arc-tool.h" +#include "display/sp-canvas-item.h" + +using Inkscape::DocumentUndo; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createArcContext() { + return new ArcTool(); + } + + bool arcContextRegistered = ToolFactory::instance().registerObject("/tools/shapes/arc", createArcContext); +} + +const std::string& ArcTool::getPrefsPath() { + return ArcTool::prefsPath; +} + +const std::string ArcTool::prefsPath = "/tools/shapes/arc"; + + +ArcTool::ArcTool() : ToolBase() { + this->cursor_shape = cursor_ellipse_xpm; + this->hot_x = 4; + this->hot_y = 4; + this->xp = 0; + this->yp = 0; + this->tolerance = 0; + this->within_tolerance = false; + this->item_to_select = NULL; + //this->tool_url = "/tools/shapes/arc"; + + this->arc = NULL; +} + +void ArcTool::finish() { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), GDK_CURRENT_TIME); + this->finishItem(); + this->sel_changed_connection.disconnect(); + + ToolBase::finish(); +} + +ArcTool::~ArcTool() { + this->enableGrDrag(false); + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = NULL; + + /* 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(SH_KNOTHOLDER); + this->shape_editor->set_item(selection->singleItem(), SH_KNOTHOLDER); +} + +void ArcTool::setup() { + ToolBase::setup(); + + Inkscape::Selection *selection = sp_desktop_selection(this->desktop); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = sp_desktop_selection(this->desktop)->singleItem(); + if (item) { + this->shape_editor->set_item(item, SH_KNOTHOLDER); + } + + 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(); + } +} + +bool ArcTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + Inkscape::setup_for_drag_start(desktop, this, event); + ret = TRUE; + } + break; + // motion and release are always on root (why?) + default: + break; + } + +// if ((SP_EVENT_CONTEXT_CLASS(sp_arc_context_parent_class))->item_handler) { +// ret = (SP_EVENT_CONTEXT_CLASS(sp_arc_context_parent_class))->item_handler(event_context, item, event); +// } + // CPPIFY: ret is overwritten... + ret = ToolBase::item_handler(item, event); + + return ret; +} + +bool ArcTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = sp_desktop_selection(desktop); + 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 && !this->space_panning) { + dragging = true; + + this->center = Inkscape::setup_for_drag_start(desktop, this, event); + + /* Snap center */ + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE); + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK, + NULL, event->button.time); + ret = TRUE; + m.unSetup(); + } + break; + case GDK_MOTION_NOTIFY: + if (dragging && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + 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 (!sp_event_context_knot_mouseover(this)){ + 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 && !this->space_panning) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the arc + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->xp = 0; + this->yp = 0; + this->item_to_select = NULL; + ret = TRUE; + } + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), event->button.time); + break; + + case GDK_KEY_PRESS: + switch (get_group0_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"), + NULL); + } + 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_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-arc"); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_space: + if (dragging) { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), event->button.time); + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + 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: + ret = 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 (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void ArcTool::drag(Geom::Point pt, guint state) { + if (!this->arc) { + if (Inkscape::have_viable_layer(desktop, this->message_context) == 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); + + this->arc = SP_GENERICELLIPSE(desktop->currentLayer()->appendChildRepr(repr)); + Inkscape::GC::release(repr); + this->arc->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + this->arc->updateRepr(); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + bool ctrl_save = false; + + if ((state & GDK_MOD1_MASK) && (state & GDK_CONTROL_MASK) && !(state & GDK_SHIFT_MASK)) { + // if Alt is pressed without Shift in addition to Control, temporarily drop the CONTROL mask + // so that the ellipse is not constrained to integer ratios + ctrl_save = true; + state = state ^ GDK_CONTROL_MASK; + } + + Geom::Rect r = Inkscape::snap_rectangular_box(desktop, this->arc, pt, this->center, state); + + if (ctrl_save) { + state = state ^ GDK_CONTROL_MASK; + } + + Geom::Point dir = r.dimensions() / 2; + + if (state & GDK_MOD1_MASK) { + /* With Alt let the ellipse pass through the mouse pointer */ + Geom::Point c = r.midpoint(); + + if (!ctrl_save) { + 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"); + GString *xs = g_string_new(rdimx_q.string(desktop->namedview->doc_units).c_str()); + GString *ys = g_string_new(rdimy_q.string(desktop->namedview->doc_units).c_str()); + + if (state & GDK_CONTROL_MASK) { + int ratio_x, ratio_y; + + if (fabs (rdimx) > fabs (rdimy)) { + ratio_x = (int) rint (rdimx / rdimy); + ratio_y = 1; + } else { + ratio_x = 1; + ratio_y = (int) rint (rdimy / rdimx); + } + + 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->str, ys->str, ratio_x, ratio_y); + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Ellipse</b>: %s × %s; with <b>Ctrl</b> to make square or integer-ratio ellipse; with <b>Shift</b> to draw around the starting point"), xs->str, ys->str); + } + + g_string_free(xs, FALSE); + g_string_free(ys, FALSE); +} + +void ArcTool::finishItem() { + this->message_context->clear(); + + if (this->arc != NULL) { + 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->getRepr(), this->arc->transform, NULL, true); + + desktop->canvas->endForcedFullRedraws(); + + sp_desktop_selection(desktop)->set(this->arc); + + DocumentUndo::done(sp_desktop_document(desktop), SP_VERB_CONTEXT_ARC, _("Create ellipse")); + + this->arc = NULL; + } +} + +void ArcTool::cancel() { + sp_desktop_selection(desktop)->clear(); + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), 0); + + if (this->arc != NULL) { + this->arc->deleteObject(); + this->arc = NULL; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = NULL; + + desktop->canvas->endForcedFullRedraws(); + + DocumentUndo::cancel(sp_desktop_document(desktop)); +} + +} +} +} + + +/* + 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 000000000..eaa50f2b9 --- /dev/null +++ b/src/ui/tools/arc-tool.h @@ -0,0 +1,76 @@ +#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, read the file 'COPYING' for more information + */ + +#include <stddef.h> +#include <sigc++/connection.h> + +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +#include "sp-ellipse.h" + +#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*>(const Inkscape::UI::Tools::ToolBase*(obj)) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ArcTool : public ToolBase { +public: + ArcTool(); + virtual ~ArcTool(); + + static const std::string prefsPath; + + virtual void setup(); + virtual void finish(); + virtual bool root_handler(GdkEvent* event); + virtual bool item_handler(SPItem* item, GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +private: + SPGenericEllipse *arc; + + Geom::Point center; + + sigc::connection sel_changed_connection; + + void selection_changed(Inkscape::Selection* selection); + + void drag(Geom::Point pt, guint state); + void finishItem(); + void cancel(); +}; + +} +} +} + +#endif /* !SEEN_ARC_CONTEXT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/tools/box3d-tool.cpp b/src/ui/tools/box3d-tool.cpp new file mode 100644 index 000000000..80cc75e79 --- /dev/null +++ b/src/ui/tools/box3d-tool.cpp @@ -0,0 +1,637 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#include "config.h" + +#include <gdk/gdkkeysyms.h> + +#include "macros.h" +#include "display/sp-canvas.h" +#include "document.h" +#include "document-undo.h" +#include "sp-namedview.h" +#include "selection.h" +#include "selection-chemistry.h" +#include "desktop-handles.h" +#include "snap.h" +#include "display/curve.h" +#include "display/sp-canvas-item.h" +#include "desktop.h" +#include "message-context.h" +#include "pixmaps/cursor-3dbox.xpm" +#include "box3d.h" +#include "ui/tools/box3d-tool.h" +#include <glibmm/i18n.h> +#include "xml/repr.h" +#include "xml/node-event-vector.h" +#include "preferences.h" +#include "context-fns.h" +#include "desktop-style.h" +#include "transf_mat_3x4.h" +#include "perspective-line.h" +#include "persp3d.h" +#include "box3d-side.h" +#include "document-private.h" +#include "line-geometry.h" +#include "shape-editor.h" +#include "verbs.h" + +using Inkscape::DocumentUndo; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createBox3dTool() { + return new Box3dTool(); + } + + bool Box3dToolRegistered = ToolFactory::instance().registerObject("/tools/shapes/3dbox", createBox3dTool); +} + +const std::string& Box3dTool::getPrefsPath() { + return Box3dTool::prefsPath; +} + +const std::string Box3dTool::prefsPath = "/tools/shapes/3dbox"; + +Box3dTool::Box3dTool() : ToolBase() { + this->cursor_shape = cursor_3dbox_xpm; + this->hot_x = 4; + this->hot_y = 4; + this->xp = 0; + this->yp = 0; + this->tolerance = 0; + this->within_tolerance = false; + this->item_to_select = NULL; + + this->box3d = NULL; + + this->ctrl_dragged = false; + this->extruded = false; + + this->_vpdrag = NULL; +} + +void Box3dTool::finish() { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), GDK_CURRENT_TIME); + this->finishItem(); + this->sel_changed_connection.disconnect(); + + ToolBase::finish(); +} + + +Box3dTool::~Box3dTool() { + this->enableGrDrag(false); + + delete (this->_vpdrag); + this->_vpdrag = NULL; + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = NULL; + + /* fixme: This is necessary because we do not grab */ + if (this->box3d) { + this->finishItem(); + } +} + +/** + * 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(SH_KNOTHOLDER); + this->shape_editor->set_item(selection->singleItem(), SH_KNOTHOLDER); + + if (selection->perspList().size() == 1) { + // selecting a single box changes the current perspective + this->desktop->doc()->setCurrentPersp3D(selection->perspList().front()); + } +} + +/* Create a default perspective in document defs if none is present (which can happen, among other + * circumstances, after 'vacuum defs' or when a pre-0.46 file is opened). + */ +static void sp_box3d_context_ensure_persp_in_defs(SPDocument *document) { + SPDefs *defs = document->getDefs(); + + bool has_persp = false; + for ( SPObject *child = defs->firstChild(); child; child = child->getNext() ) { + if (SP_IS_PERSP3D(child)) { + has_persp = true; + break; + } + } + + if (!has_persp) { + document->setCurrentPersp3D(persp3d_create_xml_element (document)); + } +} + +void Box3dTool::setup() { + ToolBase::setup(); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = sp_desktop_selection(this->desktop)->singleItem(); + if (item) { + this->shape_editor->set_item(item, SH_KNOTHOLDER); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = sp_desktop_selection(this->desktop)->connectChanged( + sigc::mem_fun(this, &Box3dTool::selection_changed) + ); + + this->_vpdrag = new Box3D::VPDrag(sp_desktop_document(this->desktop)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +bool Box3dTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if ( event->button.button == 1 && !this->space_panning) { + Inkscape::setup_for_drag_start(desktop, this, 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 = sp_desktop_document (desktop); + Inkscape::Selection *selection = sp_desktop_selection (desktop); + 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 && !this->space_panning) { + 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; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + ( GDK_KEY_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | + GDK_BUTTON_PRESS_MASK ), + NULL, event->button.time); + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning) { + 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 (!sp_event_context_knot_mouseover(this)) { + 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 && !this->space_panning) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the box + this->finishItem(); + } 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 = NULL; + ret = TRUE; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), + event->button.time); + } + break; + + case GDK_KEY_PRESS: + switch (get_group0_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: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::X, -180/snaps, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + case GDK_KEY_bracketleft: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::X, 180/snaps, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + case GDK_KEY_parenright: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Y, -180/snaps, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + case GDK_KEY_parenleft: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Y, 180/snaps, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + case GDK_KEY_braceright: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Z, -180/snaps, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + case GDK_KEY_braceleft: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Z, 180/snaps, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + /* TODO: what is this??? + case GDK_O: + if (MOD__CTRL(event) && MOD__SHIFT(event)) { + Box3D::create_canvas_point(persp3d_get_VP(document()->getCurrentPersp3D(), Proj::W).affine(), + 6, 0xff00ff00); + } + ret = true; + break; + */ + + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + sp_selection_to_guides(desktop); + ret = true; + } + break; + + case GDK_KEY_p: + case GDK_KEY_P: + if (MOD__SHIFT_ONLY(event)) { + if (document->getCurrentPersp3D()) { + persp3d_print_debugging_info (document->getCurrentPersp3D()); + } + ret = true; + } + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-box3d"); + 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: + sp_desktop_selection(desktop)->clear(); + //TODO: make dragging escapable by Esc + break; + + case GDK_KEY_space: + if (dragging) { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), + event->button.time); + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + if (!this->within_tolerance) { + // we've been dragging, finish the box + 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; + + 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, this->message_context) == false) { + return; + } + + // Create object + SPBox3D *box3d = SPBox3D::createBox3D((SPItem*)desktop->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 += box3d_side_axes_string(side); + 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.data()); + } else { + // use default style + GString *pstring = g_string_new(""); + g_string_printf (pstring, "/tools/shapes/3dbox/%s", box3d_side_axes_string(side)); + desktop->applyCurrentOrToolStyle (side, pstring->str, false); + } + + side->updateRepr(); // calls box3d_side_write() and updates, e.g., the axes string description + } + + box3d_set_z_orders(this->box3d); + 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(); */ + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + g_assert(this->box3d); + + this->box3d->orig_corner0 = this->drag_origin_proj; + this->box3d->orig_corner7 = this->drag_ptC_proj; + + box3d_check_for_swapped_coords(this->box3d); + + /* we need to call this from here (instead of from box3d_position_set(), for example) + because z-order setting must not interfere with display updates during undo/redo */ + box3d_set_z_orders (this->box3d); + + box3d_position_set(this->box3d); + + // 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 != NULL) { + SPDocument *doc = sp_desktop_document(this->desktop); + + if (!doc || !doc->getCurrentPersp3D()) { + return; + } + + this->box3d->orig_corner0 = this->drag_origin_proj; + this->box3d->orig_corner7 = this->drag_ptC_proj; + + this->box3d->updateRepr(); + + box3d_relabel_corners(this->box3d); + + desktop->canvas->endForcedFullRedraws(); + + sp_desktop_selection(desktop)->set(this->box3d); + DocumentUndo::done(sp_desktop_document(desktop), SP_VERB_CONTEXT_3DBOX, + _("Create 3D box")); + + this->box3d = NULL; + } +} + +} +} +} + +/* + 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 000000000..99bf99a7a --- /dev/null +++ b/src/ui/tools/box3d-tool.h @@ -0,0 +1,95 @@ +#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 + */ + +#include <stddef.h> +#include <sigc++/sigc++.h> +#include "ui/tools/tool-base.h" +#include "proj_pt.h" +#include "vanishing-point.h" + +#include "box3d.h" + +#define SP_BOX3D_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::Box3dTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_BOX3D_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::Box3dTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class Box3dTool : public ToolBase { +public: + Box3dTool(); + virtual ~Box3dTool(); + + Box3D::VPDrag * _vpdrag; + + static const std::string prefsPath; + + virtual void setup(); + virtual void finish(); + virtual bool root_handler(GdkEvent* event); + virtual bool item_handler(SPItem* item, GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +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 occured (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 000000000..2c5e6561c --- /dev/null +++ b/src/ui/tools/calligraphic-tool.cpp @@ -0,0 +1,1216 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#define noDYNA_DRAW_VERBOSE + +#include "config.h" + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> +#include <string> +#include <cstring> +#include <numeric> + +#include "svg/svg.h" +#include "display/canvas-bpath.h" +#include "display/cairo-utils.h" +#include <2geom/math-utils.h> +#include <2geom/pathvector.h> +#include <2geom/bezier-utils.h> +#include <2geom/circle.h> +#include "display/curve.h" +#include <glib.h> +#include "macros.h" +#include "document.h" +#include "document-undo.h" +#include "selection.h" +#include "desktop.h" +#include "desktop-events.h" +#include "desktop-handles.h" +#include "desktop-style.h" +#include "message-context.h" +#include "preferences.h" +#include "pixmaps/cursor-calligraphy.xpm" +#include "xml/repr.h" +#include "context-fns.h" +#include "sp-item.h" +#include "inkscape.h" +#include "color.h" +#include "splivarot.h" +#include "sp-item-group.h" +#include "sp-shape.h" +#include "sp-path.h" +#include "sp-text.h" +#include "display/sp-canvas.h" +#include "display/canvas-bpath.h" +#include "display/canvas-arena.h" +#include "livarot/Shape.h" +#include "verbs.h" + +#include "ui/tools/calligraphic-tool.h" + +using Inkscape::DocumentUndo; + +#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 + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void add_cap(SPCurve *curve, Geom::Point const &from, Geom::Point const &to, double rounding); + +namespace { + ToolBase* createCalligraphicContext() { + return new CalligraphicTool(); + } + + bool calligraphicContextRegistered = ToolFactory::instance().registerObject("/tools/calligraphic", createCalligraphicContext); +} + +const std::string& CalligraphicTool::getPrefsPath() { + return CalligraphicTool::prefsPath; +} + + +const std::string CalligraphicTool::prefsPath = "/tools/calligraphic"; + +CalligraphicTool::CalligraphicTool() : DynamicBase() { + this->cursor_shape = cursor_calligraphy_xpm; + this->hot_x = 4; + this->hot_y = 4; + + this->vel_thin = 0.1; + this->flatness = 0.9; + this->cap_rounding = 0.0; + + this->abs_width = false; + this->keep_selected = true; + + this->hatch_spacing = 0; + this->hatch_spacing_step = 0; + + this->hatch_last_nearest = Geom::Point(0,0); + this->hatch_last_pointer = Geom::Point(0,0); + this->hatch_escaped = false; + this->hatch_area = NULL; + this->hatch_item = NULL; + this->hatch_livarot_path = NULL; + + this->trace_bg = false; + this->just_started_drawing = false; +} + +CalligraphicTool::~CalligraphicTool() { + if (this->hatch_area) { + sp_canvas_item_destroy(this->hatch_area); + this->hatch_area = NULL; + } +} + +void CalligraphicTool::setup() { + DynamicBase::setup(); + + this->accumulated = new SPCurve(); + this->currentcurve = new SPCurve(); + + this->cal1 = new SPCurve(); + this->cal2 = new SPCurve(); + + this->currentshape = sp_canvas_item_new(sp_desktop_sketch(this->desktop), SP_TYPE_CANVAS_BPATH, NULL); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->currentshape), DDC_RED_RGBA, SP_WIND_RULE_EVENODD); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->currentshape), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + /* fixme: Cannot we cascade it to root more clearly? */ + g_signal_connect(G_OBJECT(this->currentshape), "event", G_CALLBACK(sp_desktop_root_handler), this->desktop); + + { + /* TODO: have a look at DropperTool::setup where the same is done.. generalize? */ + Geom::PathVector path; + Geom::Circle(0, 0, 1).getPath(path); + + SPCurve *c = new SPCurve(path); + + this->hatch_area = sp_canvas_bpath_new(sp_desktop_controls(this->desktop), c); + + c->unref(); + + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->hatch_area), 0x00000000,(SPWindRule)0); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x0000007f, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_hide(this->hatch_area); + } + + 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(); + } +} + +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; +} + +///* Get normalized point */ +//Geom::Point CalligraphicTool::getNormalizedPoint(Geom::Point v) const { +// Geom::Rect drect = desktop->get_display_area(); +// +// double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] ); +// +// return Geom::Point(( v[Geom::X] - drect.min()[Geom::X] ) / max, ( v[Geom::Y] - drect.min()[Geom::Y] ) / max); +//} +// +///* Get view point */ +//Geom::Point CalligraphicTool::getViewPoint(Geom::Point n) const { +// Geom::Rect drect = desktop->get_display_area(); +// +// double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] ); +// +// return Geom::Point(n[Geom::X] * max + drect.min()[Geom::X], n[Geom::Y] * max + drect.min()[Geom::Y]); +//} + +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: + gdouble length = std::sqrt(this->xtilt*this->xtilt + this->ytilt*this->ytilt);; + + if (length > 0) { + Geom::Point ang1 = Geom::Point(this->ytilt/length, this->xtilt/length); + a1 = atan2(ang1); + } + else + a1 = 0.0; + } + else { + // 1b. fixed dc->angle (absolutely flat nib): + double const radians = ( (this->angle - 90) / 180.0 ) * M_PI; + Geom::Point ang1 = Geom::Point(-sin(radians), cos(radians)); + a1 = atan2(ang1); + } + + // 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 - 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 = this->getViewPoint(this->cur); + Geom::Point brush_w = SP_EVENT_CONTEXT(this)->desktop->d2w(brush); + + double trace_thick = 1; + if (this->trace_bg) { + // pick single pixel + double R, G, B, A; + Geom::IntRect area = Geom::IntRect::from_xywh(brush_w.floor(), Geom::IntPoint(1, 1)); + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); + sp_canvas_arena_render_surface(SP_CANVAS_ARENA(sp_desktop_drawing(SP_EVENT_CONTEXT(this)->desktop)), s, area); + ink_cairo_surface_average_color_premul(s, R, G, B, A); + cairo_surface_destroy(s); + 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 /= SP_EVENT_CONTEXT(this)->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; + + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), 0); + + /* Remove all temporary line segments */ + while (this->segments) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(this->segments->data)); + this->segments = g_slist_remove(this->segments, this->segments->data); + } + + /* reset accumulated curve */ + this->accumulated->reset(); + this->clear_current(); + + if (this->repr) { + this->repr = NULL; + } +} + +bool CalligraphicTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + if (Inkscape::have_viable_layer(desktop, this->message_context) == false) { + return TRUE; + } + + this->accumulated->reset(); + + if (this->repr) { + this->repr = NULL; + } + + /* initialize first point */ + this->npoints = 0; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + ( GDK_KEY_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_BUTTON_PRESS_MASK ), + NULL, + event->button.time); + + ret = TRUE; + + desktop->canvas->forceFullRedrawAfterInterruptions(3); + 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 = sp_desktop_selection(desktop)->singleItem(); + if (selected && (SP_IS_SHAPE(selected) || SP_IS_TEXT(selected))) { + // One item selected, and it's a path; + // let's try to track it as a guide + + if (selected != this->hatch_item) { + this->hatch_item = selected; + if (this->hatch_livarot_path) + delete this->hatch_livarot_path; + this->hatch_livarot_path = Path_for_item (this->hatch_item, true, true); + this->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 + boost::optional<Path::cut_position> position = get_nearest_position_on_Path(this->hatch_livarot_path, pointer); + nearest = get_point_on_Path(this->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->space_panning) { + 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 (!IS_NAN(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; + } + + // 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)); + sp_canvas_item_affine_absolute(this->hatch_area, sm); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x7f7f7fff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_show(this->hatch_area); + } else if (this->dragging && !this->hatch_escaped) { + // 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)); + sp_canvas_item_affine_absolute(this->hatch_area, sm); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x00FF00ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_show(this->hatch_area); + } else if (this->dragging && this->hatch_escaped) { + // 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)); + + sp_canvas_item_affine_absolute(this->hatch_area, sm); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0xFF0000ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_show(this->hatch_area); + } 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 (!IS_NAN(c[Geom::X]) && !IS_NAN(c[Geom::Y])) { + Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c)); + sp_canvas_item_affine_absolute(this->hatch_area, sm); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x7f7f7fff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_show(this->hatch_area); + } + } + } else { + sp_canvas_item_hide(this->hatch_area); + } + } + 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)); + + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), event->button.time); + desktop->canvas->endForcedFullRedraws(); + this->is_drawing = false; + + if (this->dragging && event->button.button == 1 && !this->space_panning) { + this->dragging = FALSE; + + this->apply(motion_dt); + + /* Remove all temporary line segments */ + while (this->segments) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(this->segments->data)); + this->segments = g_slist_remove(this->segments, this->segments->data); + } + + /* Create object */ + this->fit_and_split(true); + if (this->accumulate()) + this->set_to_accumulated(event->button.state & GDK_SHIFT_MASK, event->button.state & GDK_MOD1_MASK); // performs document_done + else + g_warning ("Failed to create path: invalid data in dc->cal1 or dc->cal2"); + + /* reset accumulated curve */ + this->accumulated->reset(); + + this->clear_current(); + if (this->repr) { + this->repr = NULL; + } + + 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 = NULL; + this->hatch_livarot_path = NULL; + 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; + } + break; + } + + case GDK_KEY_PRESS: + switch (get_group0_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 += 0.01; + if (this->width > 1.0) + this->width = 1.0; + sp_ddc_update_toolbox (desktop, "altx-calligraphy", 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 -= 0.01; + if (this->width < 0.01) + this->width = 0.01; + sp_ddc_update_toolbox (desktop, "altx-calligraphy", this->width * 100); + ret = TRUE; + } + break; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + this->width = 0.01; + sp_ddc_update_toolbox (desktop, "altx-calligraphy", this->width * 100); + ret = TRUE; + break; + case GDK_KEY_End: + case GDK_KEY_KP_End: + this->width = 1.0; + sp_ddc_update_toolbox (desktop, "altx-calligraphy", this->width * 100); + ret = TRUE; + break; + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-calligraphy"); + 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_group0_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 */ + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), NULL); + /* reset curve */ + this->currentcurve->reset(); + this->cal1->reset(); + this->cal2->reset(); + /* reset points */ + this->npoints = 0; +} + +void CalligraphicTool::set_to_accumulated(bool unionize, bool subtract) { + if (!this->accumulated->is_empty()) { + if (!this->repr) { + /* Create object */ + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + /* Set style */ + sp_desktop_apply_style_tool (desktop, repr, "/tools/calligraphic", false); + + this->repr = repr; + + SPItem *item=SP_ITEM(desktop->currentLayer()->appendChildRepr(this->repr)); + Inkscape::GC::release(this->repr); + item->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + item->updateRepr(); + } + + Geom::PathVector pathv = this->accumulated->get_pathvector() * desktop->dt2doc(); + gchar *str = sp_svg_write_path(pathv); + g_assert( str != NULL ); + this->repr->setAttribute("d", str); + g_free(str); + + if (unionize) { + sp_desktop_selection(desktop)->add(this->repr); + sp_selected_path_union_skip_undo(sp_desktop_selection(desktop), desktop); + } else if (subtract) { + sp_desktop_selection(desktop)->add(this->repr); + sp_selected_path_diff_skip_undo(sp_desktop_selection(desktop), desktop); + } else { + if (this->keep_selected) { + sp_desktop_selection(desktop)->set(this->repr); + } + } + + // Now we need to write the transform information. + // First, find out whether our repr is still linked to a valid object. In this case, + // we need to write the transform data only for this element. + // Either there was no boolean op or it failed. + SPItem *result = SP_ITEM(desktop->doc()->getObjectByRepr(this->repr)); + + if (result == NULL) { + // 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->getRepr(), result->transform, NULL, true); + } else { + if (this->repr) { + sp_repr_unparent(this->repr); + } + + this->repr = NULL; + } + + DocumentUndo::done(sp_desktop_document(desktop), SP_VERB_CONTEXT_CALLIGRAPHIC, + _("Draw calligraphic stroke")); +} + +static void +add_cap(SPCurve *curve, + Geom::Point const &from, + Geom::Point const &to, + double rounding) +{ + if (Geom::L2( to - from ) > DYNA_EPSILON) { + Geom::Point vel = rounding * Geom::rot90( to - from ) / sqrt(2.0); + double mag = Geom::L2(vel); + + Geom::Point v = mag * Geom::rot90( to - from ) / Geom::L2( to - from ); + curve->curveto(from + v, to + v, to); + } +} + +bool CalligraphicTool::accumulate() { + if ( + this->cal1->is_empty() || + this->cal2->is_empty() || + (this->cal1->get_segment_count() <= 0) || + this->cal1->first_path()->closed() + ) { + + this->cal1->reset(); + this->cal2->reset(); + + return false; // failure + } + + SPCurve *rev_cal2 = this->cal2->create_reverse(); + + if ((rev_cal2->get_segment_count() <= 0) || rev_cal2->first_path()->closed()) { + rev_cal2->unref(); + + this->cal1->reset(); + this->cal2->reset(); + + return false; // failure + } + + Geom::Curve const * dc_cal1_firstseg = this->cal1->first_segment(); + Geom::Curve const * rev_cal2_firstseg = rev_cal2->first_segment(); + Geom::Curve const * dc_cal1_lastseg = this->cal1->last_segment(); + Geom::Curve const * rev_cal2_lastseg = rev_cal2->last_segment(); + + this->accumulated->reset(); /* Is this required ?? */ + + this->accumulated->append(this->cal1, false); + + add_cap(this->accumulated, dc_cal1_lastseg->finalPoint(), rev_cal2_firstseg->initialPoint(), this->cap_rounding); + + this->accumulated->append(rev_cal2, true); + + add_cap(this->accumulated, rev_cal2_lastseg->finalPoint(), dc_cal1_firstseg->initialPoint(), this->cap_rounding); + + this->accumulated->closepath(); + + rev_cal2->unref(); + + this->cal1->reset(); + this->cal2->reset(); + + return true; // success +} + +static double square(double const x) +{ + return x * x; +} + +void CalligraphicTool::fit_and_split(bool release) { + double const tolerance_sq = square( desktop->w2d().descrim() * TOLERANCE_CALLIGRAPHIC ); + +#ifdef DYNA_DRAW_VERBOSE + g_print("[F&S:R=%c]", release?'T':'F'); +#endif + + if (!( this->npoints > 0 && this->npoints < SAMPLING_SIZE )) { + return; // just clicked + } + + if ( this->npoints == SAMPLING_SIZE - 1 || release ) { +#define BEZIER_SIZE 4 +#define BEZIER_MAX_BEZIERS 8 +#define BEZIER_MAX_LENGTH ( BEZIER_SIZE * BEZIER_MAX_BEZIERS ) + +#ifdef DYNA_DRAW_VERBOSE + g_print("[F&S:#] dc->npoints:%d, release:%s\n", + this->npoints, release ? "TRUE" : "FALSE"); +#endif + + /* Current calligraphic */ + if ( this->cal1->is_empty() || this->cal2->is_empty() ) { + /* dc->npoints > 0 */ + /* g_print("calligraphics(1|2) reset\n"); */ + this->cal1->reset(); + this->cal2->reset(); + + this->cal1->moveto(this->point1[0]); + this->cal2->moveto(this->point2[0]); + } + + Geom::Point b1[BEZIER_MAX_LENGTH]; + gint const nb1 = Geom::bezier_fit_cubic_r(b1, this->point1, this->npoints, + tolerance_sq, BEZIER_MAX_BEZIERS); + g_assert( nb1 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b1)) ); + + Geom::Point b2[BEZIER_MAX_LENGTH]; + gint const nb2 = Geom::bezier_fit_cubic_r(b2, this->point2, this->npoints, + tolerance_sq, BEZIER_MAX_BEZIERS); + g_assert( nb2 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b2)) ); + + if ( nb1 != -1 && nb2 != -1 ) { + /* Fit and draw and reset state */ +#ifdef DYNA_DRAW_VERBOSE + g_print("nb1:%d nb2:%d\n", nb1, nb2); +#endif + /* CanvasShape */ + if (! release) { + this->currentcurve->reset(); + this->currentcurve->moveto(b1[0]); + for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) { + this->currentcurve->curveto(bp1[1], bp1[2], bp1[3]); + } + this->currentcurve->lineto(b2[BEZIER_SIZE*(nb2-1) + 3]); + for (Geom::Point *bp2 = b2 + BEZIER_SIZE * ( nb2 - 1 ); bp2 >= b2; bp2 -= BEZIER_SIZE) { + this->currentcurve->curveto(bp2[2], bp2[1], bp2[0]); + } + // FIXME: dc->segments is always NULL at this point?? + if (!this->segments) { // first segment + add_cap(this->currentcurve, b2[0], b1[0], this->cap_rounding); + } + this->currentcurve->closepath(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve); + } + + /* Current calligraphic */ + for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) { + this->cal1->curveto(bp1[1], bp1[2], bp1[3]); + } + for (Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) { + this->cal2->curveto(bp2[1], bp2[2], bp2[3]); + } + } else { + /* fixme: ??? */ +#ifdef DYNA_DRAW_VERBOSE + g_print("[fit_and_split] failed to fit-cubic.\n"); +#endif + this->draw_temporary_box(); + + for (gint i = 1; i < this->npoints; i++) { + this->cal1->lineto(this->point1[i]); + } + for (gint i = 1; i < this->npoints; i++) { + this->cal2->lineto(this->point2[i]); + } + } + + /* Fit and draw and copy last point */ +#ifdef DYNA_DRAW_VERBOSE + g_print("[%d]Yup\n", this->npoints); +#endif + if (!release) { + g_assert(!this->currentcurve->is_empty()); + + SPCanvasItem *cbp = sp_canvas_item_new(sp_desktop_sketch(desktop), + SP_TYPE_CANVAS_BPATH, + NULL); + SPCurve *curve = this->currentcurve->copy(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH (cbp), curve); + curve->unref(); + + guint32 fillColor = sp_desktop_get_color_tool (desktop, "/tools/calligraphic", true); + //guint32 strokeColor = sp_desktop_get_color_tool (desktop, "/tools/calligraphic", false); + double opacity = sp_desktop_get_master_opacity_tool (desktop, "/tools/calligraphic"); + double fillOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/calligraphic", true); + //double strokeOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/calligraphic", false); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(cbp), ((fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*fillOpacity)), SP_WIND_RULE_EVENODD); + //on second thougtht don't do stroke yet because we don't have stoke-width yet and because stoke appears between segments while drawing + //sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), ((strokeColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*strokeOpacity)), 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + /* fixme: Cannot we cascade it to root more clearly? */ + g_signal_connect(G_OBJECT(cbp), "event", G_CALLBACK(sp_desktop_root_handler), desktop); + + this->segments = g_slist_prepend(this->segments, cbp); + } + + this->point1[0] = this->point1[this->npoints - 1]; + this->point2[0] = this->point2[this->npoints - 1]; + this->npoints = 1; + } else { + this->draw_temporary_box(); + } +} + +void CalligraphicTool::draw_temporary_box() { + this->currentcurve->reset(); + + this->currentcurve->moveto(this->point2[this->npoints-1]); + + for (gint i = this->npoints-2; i >= 0; i--) { + this->currentcurve->lineto(this->point2[i]); + } + + for (gint i = 0; i < this->npoints; i++) { + this->currentcurve->lineto(this->point1[i]); + } + + if (this->npoints >= 2) { + add_cap(this->currentcurve, this->point1[this->npoints-1], this->point2[this->npoints-1], this->cap_rounding); + } + + this->currentcurve->closepath(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve); +} + +} +} +} + + +/* + 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 000000000..926e9d126 --- /dev/null +++ b/src/ui/tools/calligraphic-tool.h @@ -0,0 +1,94 @@ +#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, read the file 'COPYING' for more information + */ + +#include "ui/tools/dynamic-base.h" +#include "splivarot.h" + +#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 { +namespace UI { +namespace Tools { + +class CalligraphicTool : public DynamicBase { +public: + CalligraphicTool(); + virtual ~CalligraphicTool(); + + static const std::string prefsPath; + + virtual void setup(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +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; + SPCanvasItem *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 000000000..36e013ba8 --- /dev/null +++ b/src/ui/tools/connector-tool.cpp @@ -0,0 +1,1496 @@ +/* + * 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, 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 seperate 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 <gdk/gdkkeysyms.h> +#include <string> +#include <cstring> + +#include "ui/tools/connector-tool.h" +#include "pixmaps/cursor-connector.xpm" +#include "xml/node-event-vector.h" +#include "xml/repr.h" +#include "svg/svg.h" +#include "desktop.h" +#include "desktop-style.h" +#include "desktop-handles.h" +#include "document.h" +#include "document-undo.h" +#include "message-context.h" +#include "message-stack.h" +#include "selection.h" +#include "inkscape.h" +#include "preferences.h" +#include "sp-path.h" +#include "display/sp-canvas.h" +#include "display/canvas-bpath.h" +#include "display/sodipodi-ctrl.h" +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> +#include "snap.h" +#include "knot.h" +#include "sp-conn-end.h" +#include "sp-conn-end-pair.h" +#include "conn-avoid-ref.h" +#include "libavoid/vertices.h" +#include "libavoid/router.h" +#include "context-fns.h" +#include "sp-namedview.h" +#include "sp-text.h" +#include "sp-flowtext.h" +#include "display/curve.h" +#include "verbs.h" + +using Inkscape::DocumentUndo; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +// Stuff borrowed from DrawContext +static void spcc_connector_set_initial_point(ConnectorTool *cc, Geom::Point const p); +static void spcc_connector_set_subsequent_point(ConnectorTool *cc, Geom::Point const p); +static void spcc_connector_finish_segment(ConnectorTool *cc, Geom::Point p); +static void spcc_reset_colors(ConnectorTool *cc); +static void spcc_connector_finish(ConnectorTool *cc); +static void spcc_concat_colors_and_flush(ConnectorTool *cc); +static void spcc_flush_white(ConnectorTool *cc, SPCurve *gc); + +// Context event handlers +static gint connector_handle_button_press(ConnectorTool *const cc, GdkEventButton const &bevent); +static gint connector_handle_motion_notify(ConnectorTool *const cc, GdkEventMotion const &mevent); +static gint connector_handle_button_release(ConnectorTool *const cc, GdkEventButton const &revent); +static gint connector_handle_key_press(ConnectorTool *const cc, guint const keyval); + +static void cc_active_shape_add_knot(ConnectorTool *cc, SPItem* item); +static void cc_set_active_shape(ConnectorTool *cc, SPItem *item); +static void cc_clear_active_knots(SPKnotList k); +static void cc_clear_active_shape(ConnectorTool *cc); +static void cc_set_active_conn(ConnectorTool *cc, SPItem *item); +static void cc_clear_active_conn(ConnectorTool *cc); +static bool conn_pt_handle_test(ConnectorTool *cc, Geom::Point& p, gchar **href); +static void cc_select_handle(SPKnot* knot); +static void cc_deselect_handle(SPKnot* knot); +static bool cc_item_is_shape(SPItem *item); +static void cc_connector_rerouting_finish(ConnectorTool *const cc, + Geom::Point *const p); + +static void shape_event_attr_deleted(Inkscape::XML::Node *repr, + Inkscape::XML::Node *child, Inkscape::XML::Node *ref, gpointer data); +static void shape_event_attr_changed(Inkscape::XML::Node *repr, gchar const *name, + gchar const *old_value, gchar const *new_value, bool is_interactive, + gpointer data); + +/*static Geom::Point connector_drag_origin_w(0, 0); +static bool connector_within_tolerance = false;*/ + +static Inkscape::XML::NodeEventVector shape_repr_events = { + NULL, /* child_added */ + NULL, /* child_added */ + shape_event_attr_changed, + NULL, /* content_changed */ + NULL /* order_changed */ +}; + +static Inkscape::XML::NodeEventVector layer_repr_events = { + NULL, /* child_added */ + shape_event_attr_deleted, + NULL, /* child_added */ + NULL, /* content_changed */ + NULL /* order_changed */ +}; + +namespace { + ToolBase* createConnectorContext() { + return new ConnectorTool(); + } + + bool connectorContextRegistered = ToolFactory::instance().registerObject("/tools/connector", createConnectorContext); +} + +const std::string& ConnectorTool::getPrefsPath() { + return ConnectorTool::prefsPath; +} + +const std::string ConnectorTool::prefsPath = "/tools/connector"; + +ConnectorTool::ConnectorTool() : ToolBase() { + this->red_curve = 0; + this->isOrthogonal = false; + this->c1 = 0; + this->red_bpath = 0; + this->green_curve = 0; + this->selection = 0; + this->cl0 = 0; + this->cl1 = 0; + this->c0 = 0; + + this->cursor_shape = cursor_connector_xpm; + this->hot_x = 1; + this->hot_y = 1; + this->xp = 0; + this->yp = 0; + + this->red_color = 0xff00007f; + + this->newconn = NULL; + this->newConnRef = NULL; + this->curvature = 0.0; + + this->sel_changed_connection = sigc::connection(); + + this->active_shape = NULL; + this->active_shape_repr = NULL; + this->active_shape_layer_repr = NULL; + + this->active_conn = NULL; + this->active_conn_repr = NULL; + + this->active_handle = NULL; + + this->selected_handle = NULL; + + this->clickeditem = NULL; + this->clickedhandle = NULL; + + for (int i = 0; i < 2; ++i) { + this->endpt_handle[i] = NULL; + this->endpt_handler_id[i] = 0; + } + + this->shref = NULL; + this->ehref = NULL; + this->npoints = 0; + this->state = SP_CONNECTOR_CONTEXT_IDLE; +} + +ConnectorTool::~ConnectorTool() { + this->sel_changed_connection.disconnect(); + + for (int i = 0; i < 2; ++i) { + if (this->endpt_handle[1]) { + g_object_unref(this->endpt_handle[i]); + this->endpt_handle[i] = NULL; + } + } + + if (this->shref) { + g_free(this->shref); + this->shref = NULL; + } + + if (this->ehref) { + g_free(this->shref); + this->shref = NULL; + } + + g_assert( this->newConnRef == NULL ); +} + +void ConnectorTool::setup() { + ToolBase::setup(); + + this->selection = sp_desktop_selection(this->desktop); + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = this->selection->connectChanged( + sigc::mem_fun(this, &ConnectorTool::selection_changed) + ); + + /* Create red bpath */ + this->red_bpath = sp_canvas_bpath_new(sp_desktop_sketch(this->desktop), NULL); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->red_bpath), this->red_color, + 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->red_bpath), 0x00000000, + SP_WIND_RULE_NONZERO); + /* Create red curve */ + this->red_curve = new SPCurve(); + + /* Create green curve */ + this->green_curve = new SPCurve(); + + // Notice the initial selection. + //cc_selection_changed(this->selection, (gpointer) this); + this->selection_changed(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", 0)) { + this->enableSelectionCue(); + } + + // Make sure we see all enter events for canvas items, + // even if a mouse button is depressed. + this->desktop->canvas->gen_all_enter_events = true; +} + +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::finish() { + spcc_connector_finish(this); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + + ToolBase::finish(); + + if (this->selection) { + this->selection = NULL; + } + + cc_clear_active_shape(this); + cc_clear_active_conn(this); + + // Restore the default event generating behaviour. + this->desktop->canvas->gen_all_enter_events = false; +} + +//----------------------------------------------------------------------------- + + +static void +cc_clear_active_shape(ConnectorTool *cc) +{ + if (cc->active_shape == NULL) { + return; + } + g_assert( cc->active_shape_repr ); + g_assert( cc->active_shape_layer_repr ); + + cc->active_shape = NULL; + + if (cc->active_shape_repr) { + sp_repr_remove_listener_by_data(cc->active_shape_repr, cc); + Inkscape::GC::release(cc->active_shape_repr); + cc->active_shape_repr = NULL; + + sp_repr_remove_listener_by_data(cc->active_shape_layer_repr, cc); + Inkscape::GC::release(cc->active_shape_layer_repr); + cc->active_shape_layer_repr = NULL; + } + + cc_clear_active_knots(cc->knots); +} + +static void +cc_clear_active_knots(SPKnotList k) +{ + // Hide the connection points if they exist. + if (k.size()) { + for (SPKnotList::iterator it = k.begin(); it != k.end(); ++it) { + sp_knot_hide(it->first); + } + } +} + +static void +cc_clear_active_conn(ConnectorTool *cc) +{ + if (cc->active_conn == NULL) { + return; + } + g_assert( cc->active_conn_repr ); + + cc->active_conn = NULL; + + if (cc->active_conn_repr) { + sp_repr_remove_listener_by_data(cc->active_conn_repr, cc); + Inkscape::GC::release(cc->active_conn_repr); + cc->active_conn_repr = NULL; + } + + // Hide the endpoint handles. + for (int i = 0; i < 2; ++i) { + if (cc->endpt_handle[i]) { + sp_knot_hide(cc->endpt_handle[i]); + } + } +} + + +static bool +conn_pt_handle_test(ConnectorTool *cc, Geom::Point& p, gchar **href) +{ + if (cc->active_handle && (cc->knots.find(cc->active_handle) != cc->knots.end())) + { + p = cc->active_handle->pos; + *href = g_strdup_printf("#%s", cc->active_handle->owner->getId()); + return true; + } + *href = NULL; + return false; +} + +static void +cc_select_handle(SPKnot* knot) +{ + knot->setShape(SP_KNOT_SHAPE_SQUARE); + knot->setSize(10); + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0x0000ffff, 0x0000ffff, 0x0000ffff); + sp_knot_update_ctrl(knot); +} + +static void +cc_deselect_handle(SPKnot* knot) +{ + knot->setShape(SP_KNOT_SHAPE_SQUARE); + knot->setSize(8); + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff); + sp_knot_update_ctrl(knot); +} + +bool ConnectorTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + Geom::Point p(event->button.x, event->button.y); + + switch (event->type) { + case GDK_BUTTON_RELEASE: + if (event->button.button == 1 && !this->space_panning) { + if ((this->state == SP_CONNECTOR_CONTEXT_DRAGGING) && this->within_tolerance) { + spcc_reset_colors(this); + 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)) { + cc_set_active_shape(this, item); + } + } + + ret = TRUE; + } + break; + + case GDK_ENTER_NOTIFY: + if (!this->selected_handle) { + if (cc_item_is_shape(item)) { + cc_set_active_shape(this, item); + } + + ret = TRUE; + } + break; + + default: + break; + } + + return ret; +} + +bool ConnectorTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = connector_handle_button_press(this, event->button); + break; + + case GDK_MOTION_NOTIFY: + ret = connector_handle_motion_notify(this, event->motion); + break; + + case GDK_BUTTON_RELEASE: + ret = connector_handle_button_release(this, event->button); + break; + + case GDK_KEY_PRESS: + ret = connector_handle_key_press(this, get_group0_keyval (&event->key)); + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + + +static gint +connector_handle_button_press(ConnectorTool *const cc, GdkEventButton const &bevent) +{ + Geom::Point const event_w(bevent.x, bevent.y); + /* Find desktop coordinates */ + Geom::Point p = cc->desktop->w2d(event_w); + ToolBase *event_context = SP_EVENT_CONTEXT(cc); + + gint ret = FALSE; + + if ( bevent.button == 1 && !event_context->space_panning ) { + + SPDesktop *desktop = SP_EVENT_CONTEXT_DESKTOP(cc); + + if (Inkscape::have_viable_layer(desktop, cc->message_context) == false) { + return TRUE; + } + + Geom::Point const event_w(bevent.x, + bevent.y); + + cc->xp = bevent.x; + cc->yp = bevent.y; + cc->within_tolerance = true; + + Geom::Point const event_dt = cc->desktop->w2d(event_w); + + SnapManager &m = cc->desktop->namedview->snap_manager; + + switch (cc->state) { + case SP_CONNECTOR_CONTEXT_STOP: + /* This is allowed, if we just canceled curve */ + case SP_CONNECTOR_CONTEXT_IDLE: + { + if ( cc->npoints == 0 ) { + cc_clear_active_conn(cc); + + SP_EVENT_CONTEXT_DESKTOP(cc)->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 = conn_pt_handle_test(cc, p, &cc->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(cc->desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + } + spcc_connector_set_initial_point(cc, p); + + } + cc->state = SP_CONNECTOR_CONTEXT_DRAGGING; + ret = TRUE; + break; + } + case SP_CONNECTOR_CONTEXT_DRAGGING: + { + // This is the second click of a connector creation. + m.setup(cc->desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + + spcc_connector_set_subsequent_point(cc, p); + spcc_connector_finish_segment(cc, p); + + conn_pt_handle_test(cc, p, &cc->ehref); + if (cc->npoints != 0) { + spcc_connector_finish(cc); + } + cc_set_active_conn(cc, cc->newconn); + cc->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 (cc->state == SP_CONNECTOR_CONTEXT_REROUTING) { + // A context menu is going to be triggered here, + // so end the rerouting operation. + cc_connector_rerouting_finish(cc, &p); + + cc->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 (cc->npoints != 0) { + spcc_connector_finish(cc); + cc->state = SP_CONNECTOR_CONTEXT_IDLE; + ret = TRUE; + } + } + return ret; +} + +static gint +connector_handle_motion_notify(ConnectorTool *const cc, GdkEventMotion const &mevent) +{ + gint ret = FALSE; + ToolBase *event_context = SP_EVENT_CONTEXT(cc); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (event_context->space_panning || 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 (cc->within_tolerance) { + cc->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + if ( ( abs( (gint) mevent.x - cc->xp ) < cc->tolerance ) && + ( abs( (gint) mevent.y - cc->yp ) < cc->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) + cc->within_tolerance = false; + + SPDesktop *const dt = cc->desktop; + + /* Find desktop coordinates */ + Geom::Point p = dt->w2d(event_w); + + SnapManager &m = dt->namedview->snap_manager; + + switch (cc->state) { + case SP_CONNECTOR_CONTEXT_DRAGGING: + { + gobble_motion_events(mevent.state); + // This is movement during a connector creation. + if ( cc->npoints > 0 ) { + m.setup(dt); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + cc->selection->clear(); + spcc_connector_set_subsequent_point(cc, p); + ret = TRUE; + } + break; + } + case SP_CONNECTOR_CONTEXT_REROUTING: + { + gobble_motion_events(GDK_BUTTON1_MASK); + g_assert( SP_IS_PATH(cc->clickeditem)); + + m.setup(dt); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + + // Update the hidden path + Geom::Affine i2d ( (cc->clickeditem)->i2dt_affine() ); + Geom::Affine d2i = i2d.inverse(); + SPPath *path = SP_PATH(cc->clickeditem); + SPCurve *curve = path->get_curve(); + if (cc->clickedhandle == cc->endpt_handle[0]) { + Geom::Point o = cc->endpt_handle[1]->pos; + curve->stretch_endpoints(p * d2i, o * d2i); + } + else { + Geom::Point o = cc->endpt_handle[0]->pos; + curve->stretch_endpoints(o * d2i, p * d2i); + } + sp_conn_reroute_path_immediate(path); + + // Copy this to the temporary visible path + cc->red_curve = path->get_curve_for_edit(); + cc->red_curve->transform(i2d); + + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(cc->red_bpath), cc->red_curve); + ret = TRUE; + break; + } + case SP_CONNECTOR_CONTEXT_STOP: + /* This is perfectly valid */ + break; + default: + if (!sp_event_context_knot_mouseover(cc)) { + m.setup(dt); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + break; + } + return ret; +} + +static gint +connector_handle_button_release(ConnectorTool *const cc, GdkEventButton const &revent) +{ + gint ret = FALSE; + ToolBase *event_context = SP_EVENT_CONTEXT(cc); + if ( revent.button == 1 && !event_context->space_panning ) { + + SPDesktop *desktop = SP_EVENT_CONTEXT_DESKTOP(cc); + SPDocument *doc = sp_desktop_document(desktop); + + SnapManager &m = desktop->namedview->snap_manager; + + Geom::Point const event_w(revent.x, revent.y); + + /* Find desktop coordinates */ + Geom::Point p = cc->desktop->w2d(event_w); + + switch (cc->state) { + //case SP_CONNECTOR_CONTEXT_POINT: + case SP_CONNECTOR_CONTEXT_DRAGGING: + { + m.setup(desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + + if (cc->within_tolerance) + { + spcc_connector_finish_segment(cc, p); + return TRUE; + } + // Connector has been created via a drag, end it now. + spcc_connector_set_subsequent_point(cc, p); + spcc_connector_finish_segment(cc, p); + // Test whether we clicked on a connection point + conn_pt_handle_test(cc, p, &cc->ehref); + if (cc->npoints != 0) { + spcc_connector_finish(cc); + } + cc_set_active_conn(cc, cc->newconn); + cc->state = SP_CONNECTOR_CONTEXT_IDLE; + break; + } + case SP_CONNECTOR_CONTEXT_REROUTING: + { + m.setup(desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + cc_connector_rerouting_finish(cc, &p); + + doc->ensureUpToDate(); + cc->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; +} + +static gint +connector_handle_key_press(ConnectorTool *const cc, guint const keyval) +{ + gint ret = FALSE; + + switch (keyval) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (cc->npoints != 0) { + spcc_connector_finish(cc); + cc->state = SP_CONNECTOR_CONTEXT_IDLE; + ret = TRUE; + } + break; + case GDK_KEY_Escape: + if (cc->state == SP_CONNECTOR_CONTEXT_REROUTING) { + + SPDesktop *desktop = SP_EVENT_CONTEXT_DESKTOP(cc); + SPDocument *doc = sp_desktop_document(desktop); + + cc_connector_rerouting_finish(cc, NULL); + + DocumentUndo::undo(doc); + + cc->state = SP_CONNECTOR_CONTEXT_IDLE; + desktop->messageStack()->flash( Inkscape::NORMAL_MESSAGE, + _("Connector endpoint drag cancelled.")); + ret = TRUE; + } + else if (cc->npoints != 0) { + // if drawing, cancel, otherwise pass it up for deselecting + cc->state = SP_CONNECTOR_CONTEXT_STOP; + spcc_reset_colors(cc); + ret = TRUE; + } + break; + default: + break; + } + return ret; +} + + +static void +cc_connector_rerouting_finish(ConnectorTool *const cc, Geom::Point *const p) +{ + SPDesktop *desktop = SP_EVENT_CONTEXT_DESKTOP(cc); + SPDocument *doc = sp_desktop_document(desktop); + + // Clear the temporary path: + cc->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(cc->red_bpath), NULL); + + if (p != NULL) + { + // Test whether we clicked on a connection point + gchar *shape_label; + bool found = conn_pt_handle_test(cc, *p, &shape_label); + + if (found) { + if (cc->clickedhandle == cc->endpt_handle[0]) { + cc->clickeditem->setAttribute("inkscape:connection-start", shape_label, NULL); + } + else { + cc->clickeditem->setAttribute("inkscape:connection-end", shape_label, NULL); + } + g_free(shape_label); + } + } + cc->clickeditem->setHidden(false); + sp_conn_reroute_path_immediate(SP_PATH(cc->clickeditem)); + cc->clickeditem->updateRepr(); + DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR, + _("Reroute connector")); + cc_set_active_conn(cc, cc->clickeditem); +} + + +static void +spcc_reset_colors(ConnectorTool *cc) +{ + /* Red */ + cc->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(cc->red_bpath), NULL); + + cc->green_curve->reset(); + cc->npoints = 0; +} + + +static void +spcc_connector_set_initial_point(ConnectorTool *const cc, Geom::Point const p) +{ + g_assert( cc->npoints == 0 ); + + cc->p[0] = p; + cc->p[1] = p; + cc->npoints = 2; + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(cc->red_bpath), NULL); +} + + +static void +spcc_connector_set_subsequent_point(ConnectorTool *const cc, Geom::Point const p) +{ + g_assert( cc->npoints != 0 ); + + SPDesktop *dt = cc->desktop; + Geom::Point o = dt->dt2doc(cc->p[0]); + Geom::Point d = dt->dt2doc(p); + Avoid::Point src(o[Geom::X], o[Geom::Y]); + Avoid::Point dst(d[Geom::X], d[Geom::Y]); + + if (!cc->newConnRef) { + Avoid::Router *router = sp_desktop_document(dt)->router; + cc->newConnRef = new Avoid::ConnRef(router); + cc->newConnRef->setEndpoint(Avoid::VertID::src, src); + if (cc->isOrthogonal) + cc->newConnRef->setRoutingType(Avoid::ConnType_Orthogonal); + else + cc->newConnRef->setRoutingType(Avoid::ConnType_PolyLine); + } + // Set new endpoint. + cc->newConnRef->setEndpoint(Avoid::VertID::tar, dst); + // Immediately generate new routes for connector. + cc->newConnRef->makePathInvalid(); + cc->newConnRef->router()->processTransaction(); + // Recreate curve from libavoid route. + recreateCurve( cc->red_curve, cc->newConnRef, cc->curvature ); + cc->red_curve->transform(dt->doc2dt()); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(cc->red_bpath), cc->red_curve); +} + + +/** + * 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. + */ +static void +spcc_concat_colors_and_flush(ConnectorTool *cc) +{ + SPCurve *c = cc->green_curve; + cc->green_curve = new SPCurve(); + + cc->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(cc->red_bpath), NULL); + + if (c->is_empty()) { + c->unref(); + return; + } + + spcc_flush_white(cc, c); + + c->unref(); +} + + +/* + * 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 +spcc_flush_white(ConnectorTool *cc, SPCurve *gc) +{ + SPCurve *c; + + if (gc) { + c = gc; + c->ref(); + } else { + return; + } + + /* Now we have to go back to item coordinates at last */ + c->transform(SP_EVENT_CONTEXT_DESKTOP(cc)->dt2doc()); + + SPDesktop *desktop = SP_EVENT_CONTEXT_DESKTOP(cc); + SPDocument *doc = sp_desktop_document(desktop); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + if ( c && !c->is_empty() ) { + /* We actually have something to write */ + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + /* Set style */ + sp_desktop_apply_style_tool(desktop, repr, "/tools/connector", false); + + gchar *str = sp_svg_write_path( c->get_pathvector() ); + g_assert( str != NULL ); + repr->setAttribute("d", str); + g_free(str); + + /* Attach repr */ + cc->newconn = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr)); + cc->newconn->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + + bool connection = false; + cc->newconn->setAttribute( "inkscape:connector-type", + cc->isOrthogonal ? "orthogonal" : "polyline", NULL ); + cc->newconn->setAttribute( "inkscape:connector-curvature", + Glib::Ascii::dtostr(cc->curvature).c_str(), NULL ); + if (cc->shref) + { + cc->newconn->setAttribute( "inkscape:connection-start", cc->shref, NULL); + connection = true; + } + + if (cc->ehref) + { + cc->newconn->setAttribute( "inkscape:connection-end", cc->ehref, NULL); + connection = true; + } + // Process pending updates. + cc->newconn->updateRepr(); + doc->ensureUpToDate(); + + if (connection) { + // Adjust endpoints to shape edge. + sp_conn_reroute_path_immediate(SP_PATH(cc->newconn)); + cc->newconn->updateRepr(); + } + + cc->newconn->doWriteTransform(cc->newconn->getRepr(), cc->newconn->transform, NULL, 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. + cc->selection->set(repr); + Inkscape::GC::release(repr); + } + + c->unref(); + + DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR, _("Create connector")); +} + + +static void +spcc_connector_finish_segment(ConnectorTool *const cc, Geom::Point const /*p*/) +{ + if (!cc->red_curve->is_empty()) { + cc->green_curve->append_continuous(cc->red_curve, 0.0625); + + cc->p[0] = cc->p[3]; + cc->p[1] = cc->p[4]; + cc->npoints = 2; + + cc->red_curve->reset(); + } +} + + +static void +spcc_connector_finish(ConnectorTool *const cc) +{ + SPDesktop *const desktop = cc->desktop; + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing connector")); + + cc->red_curve->reset(); + spcc_concat_colors_and_flush(cc); + + cc->npoints = 0; + + if (cc->newConnRef) { + cc->newConnRef->removeFromGraph(); + delete cc->newConnRef; + cc->newConnRef = NULL; + } +} + + +static gboolean +cc_generic_knot_handler(SPCanvasItem *, GdkEvent *event, SPKnot *knot) +{ + g_assert (knot != NULL); + + g_object_ref(knot); + + ConnectorTool *cc = SP_CONNECTOR_CONTEXT( + knot->desktop->event_context); + + gboolean consumed = FALSE; + + gchar const *knot_tip = "Click to join at this point"; + switch (event->type) { + case GDK_ENTER_NOTIFY: + sp_knot_set_flag(knot, 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: + sp_knot_set_flag(knot, SP_KNOT_MOUSEOVER, FALSE); + + cc->active_handle = NULL; + + if (knot_tip) { + knot->desktop->event_context->defaultMessageContext()->clear(); + } + + consumed = TRUE; + break; + default: + break; + } + + g_object_unref(knot); + + return consumed; +} + + +static gboolean +endpt_handler(SPKnot */*knot*/, 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_clear_active_conn(cc); + 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. + cc->red_curve = SP_PATH(cc->clickeditem)->get_curve_for_edit(); + Geom::Affine i2d = (cc->clickeditem)->i2dt_affine(); + cc->red_curve->transform(i2d); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(cc->red_bpath), cc->red_curve); + + 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; +} + +static void cc_active_shape_add_knot(ConnectorTool *cc, SPItem* item) +{ + SPDesktop *desktop = cc->desktop; + SPKnot *knot = sp_knot_new(desktop, 0); + + knot->owner = item; + knot->setShape(SP_KNOT_SHAPE_SQUARE); + knot->setSize(8); + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff); + sp_knot_update_ctrl(knot); + + // We don't want to use the standard knot handler. + g_signal_handler_disconnect(G_OBJECT(knot->item), + knot->_event_handler_id); + knot->_event_handler_id = 0; + + g_signal_connect(G_OBJECT(knot->item), "event", + G_CALLBACK(cc_generic_knot_handler), knot); + sp_knot_set_position(knot, item->avoidRef->getConnectionPointPos() * desktop->doc2dt(), 0); + sp_knot_show(knot); + cc->knots[knot] = 1; +} + +static void cc_set_active_shape(ConnectorTool *cc, SPItem *item) +{ + g_assert(item != NULL ); + + if (cc->active_shape != item) + { + // The active shape has changed + // Rebuild everything + cc->active_shape = item; + // Remove existing active shape listeners + if (cc->active_shape_repr) { + sp_repr_remove_listener_by_data(cc->active_shape_repr, cc); + Inkscape::GC::release(cc->active_shape_repr); + + sp_repr_remove_listener_by_data(cc->active_shape_layer_repr, cc); + Inkscape::GC::release(cc->active_shape_layer_repr); + } + + // Listen in case the active shape changes + cc->active_shape_repr = item->getRepr(); + if (cc->active_shape_repr) { + Inkscape::GC::anchor(cc->active_shape_repr); + sp_repr_add_listener(cc->active_shape_repr, &shape_repr_events, cc); + + cc->active_shape_layer_repr = cc->active_shape_repr->parent(); + Inkscape::GC::anchor(cc->active_shape_layer_repr); + sp_repr_add_listener(cc->active_shape_layer_repr, &layer_repr_events, cc); + } + + cc_clear_active_knots(cc->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 (SPObject *child = item->firstChild() ; child ; child = child->getNext() ) { + if (SP_IS_PATH(child) && SP_PATH(child)->nodesInPath() == 1) { + cc_active_shape_add_knot(cc, (SPItem *) child); + } + } + cc_active_shape_add_knot(cc, item); + + } + else + { + // Ensure the item's connection_points map + // has been updated + item->document->ensureUpToDate(); + } +} + + +static void +cc_set_active_conn(ConnectorTool *cc, SPItem *item) +{ + g_assert( SP_IS_PATH(item) ); + + const SPCurve *curve = SP_PATH(item)->get_curve_reference(); + Geom::Affine i2dt = item->i2dt_affine(); + + if (cc->active_conn == item) + { + if (curve->is_empty()) + { + // Connector is invisible because it is clipped to the boundary of + // two overlpapping shapes. + sp_knot_hide(cc->endpt_handle[0]); + sp_knot_hide(cc->endpt_handle[1]); + } + else + { + // Just adjust handle positions. + Geom::Point startpt = *(curve->first_point()) * i2dt; + sp_knot_set_position(cc->endpt_handle[0], startpt, 0); + + Geom::Point endpt = *(curve->last_point()) * i2dt; + sp_knot_set_position(cc->endpt_handle[1], endpt, 0); + } + + return; + } + + cc->active_conn = item; + + // Remove existing active conn listeners + if (cc->active_conn_repr) { + sp_repr_remove_listener_by_data(cc->active_conn_repr, cc); + Inkscape::GC::release(cc->active_conn_repr); + cc->active_conn_repr = NULL; + } + + // Listen in case the active conn changes + cc->active_conn_repr = item->getRepr(); + if (cc->active_conn_repr) { + Inkscape::GC::anchor(cc->active_conn_repr); + sp_repr_add_listener(cc->active_conn_repr, &shape_repr_events, cc); + } + + for (int i = 0; i < 2; ++i) { + + // Create the handle if it doesn't exist + if ( cc->endpt_handle[i] == NULL ) { + SPKnot *knot = sp_knot_new(cc->desktop, + _("<b>Connector endpoint</b>: drag to reroute or connect to new shapes")); + + knot->setShape(SP_KNOT_SHAPE_SQUARE); + knot->setSize(7); + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff); + knot->setStroke(0x000000ff, 0x000000ff, 0x000000ff); + sp_knot_update_ctrl(knot); + + // We don't want to use the standard knot handler, + // since we don't want this knot to be draggable. + g_signal_handler_disconnect(G_OBJECT(knot->item), + knot->_event_handler_id); + knot->_event_handler_id = 0; + + g_signal_connect(G_OBJECT(knot->item), "event", + G_CALLBACK(cc_generic_knot_handler), knot); + + cc->endpt_handle[i] = knot; + } + + // Remove any existing handlers + if (cc->endpt_handler_id[i]) { + g_signal_handlers_disconnect_by_func( + G_OBJECT(cc->endpt_handle[i]->item), + (void*)G_CALLBACK(endpt_handler), (gpointer) cc ); + cc->endpt_handler_id[i] = 0; + } + + // Setup handlers for connector endpoints, this is + // is as 'after' so that cc_generic_knot_handler is + // triggered first for any endpoint. + cc->endpt_handler_id[i] = g_signal_connect_after( + G_OBJECT(cc->endpt_handle[i]->item), "event", + G_CALLBACK(endpt_handler), cc); + } + + 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; + sp_knot_set_position(cc->endpt_handle[0], startpt, 0); + + Geom::Point endpt = *(curve->last_point()) * i2dt; + sp_knot_set_position(cc->endpt_handle[1], endpt, 0); + + sp_knot_show(cc->endpt_handle[0]); + sp_knot_show(cc->endpt_handle[1]); +} + +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 = sp_knot_new(cc->desktop, 0); + // We do not process events on this knot. + g_signal_handler_disconnect(G_OBJECT(knot->item), + knot->_event_handler_id); + knot->_event_handler_id = 0; + + cc_select_handle( knot ); + cc->selected_handle = knot; + sp_knot_show(cc->selected_handle); + cc->state = SP_CONNECTOR_CONTEXT_NEWCONNPOINT; + } +} + +static bool cc_item_is_shape(SPItem *item) +{ + if (SP_IS_PATH(item)) { + const SPCurve * curve = (SP_SHAPE(item))->_curve; + if ( curve && !(curve->is_closed()) ) { + // Open paths are connectors. + return false; + } + } + else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/connector/ignoretext", true)) { + // Don't count text as a shape we can connect connector to. + return false; + } + } + return true; +} + + +bool cc_item_is_connector(SPItem *item) +{ + if (SP_IS_PATH(item)) { + bool closed = SP_PATH(item)->get_curve_reference()->is_closed(); + if (SP_PATH(item)->connEndPair.isAutoRoutingConn() && !closed) { + // To be considered a connector, an object must be a non-closed + // path that is marked with a "inkscape:connector-type" attribute. + return true; + } + } + return false; +} + + +void cc_selection_set_avoid(bool const set_avoid) +{ + SPDesktop *desktop = inkscape_active_desktop(); + if (desktop == NULL) { + return; + } + + SPDocument *document = sp_desktop_document(desktop); + + Inkscape::Selection *selection = sp_desktop_selection(desktop); + + GSList *l = const_cast<GSList *>(selection->itemList()); + + int changes = 0; + + while (l) { + SPItem *item = SP_ITEM(l->data); + + char const *value = (set_avoid) ? "true" : NULL; + + if (cc_item_is_shape(item)) { + item->setAttribute("inkscape:connector-avoid", value, NULL); + item->avoidRef->handleSettingChange(); + changes++; + } + + l = l->next; + } + + 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, SP_VERB_CONTEXT_CONNECTOR, event_desc); +} + +void ConnectorTool::selection_changed(Inkscape::Selection *selection) { + SPItem *item = selection->singleItem(); + + if (this->active_conn == item) { + // Nothing to change. + return; + } + + if (item == NULL) { + cc_clear_active_conn(this); + return; + } + + if (cc_item_is_connector(item)) { + cc_set_active_conn(this, item); + } +} + +static void +shape_event_attr_deleted(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node *child, + Inkscape::XML::Node */*ref*/, gpointer data) +{ + g_assert(data); + ConnectorTool *cc = SP_CONNECTOR_CONTEXT(data); + + if (child == cc->active_shape_repr) { + // The active shape has been deleted. Clear active shape. + cc_clear_active_shape(cc); + } +} + + +static void +shape_event_attr_changed(Inkscape::XML::Node *repr, gchar const *name, + gchar const */*old_value*/, gchar const */*new_value*/, + bool /*is_interactive*/, gpointer data) +{ + g_assert(data); + ConnectorTool *cc = SP_CONNECTOR_CONTEXT(data); + + // Look for changes that result in onscreen movement. + if (!strcmp(name, "d") || !strcmp(name, "x") || !strcmp(name, "y") || + !strcmp(name, "width") || !strcmp(name, "height") || + !strcmp(name, "transform")) + { + if (repr == cc->active_shape_repr) { + // Active shape has moved. Clear active shape. + cc_clear_active_shape(cc); + } + else if (repr == cc->active_conn_repr) { + // The active conn has been moved. + // Set it again, which just sets new handle positions. + cc_set_active_conn(cc, cc->active_conn); + } + } +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/connector-tool.h b/src/ui/tools/connector-tool.h new file mode 100644 index 000000000..7cd6a6dad --- /dev/null +++ b/src/ui/tools/connector-tool.h @@ -0,0 +1,135 @@ +#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, read the file 'COPYING' for more information + */ + +#include <stddef.h> +#include <sigc++/sigc++.h> +#include <sigc++/connection.h> +#include "ui/tools/tool-base.h" +#include <2geom/point.h> +#include "libavoid/connector.h" +#include <glibmm/i18n.h> + +#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) + +struct SPKnot; +class SPCurve; + +namespace Inkscape +{ + class Selection; +} + +enum { + SP_CONNECTOR_CONTEXT_IDLE, + SP_CONNECTOR_CONTEXT_DRAGGING, + SP_CONNECTOR_CONTEXT_CLOSE, + SP_CONNECTOR_CONTEXT_STOP, + SP_CONNECTOR_CONTEXT_REROUTING, + SP_CONNECTOR_CONTEXT_NEWCONNPOINT +}; + +typedef std::map<SPKnot *, int> SPKnotList; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ConnectorTool : public ToolBase { +public: + ConnectorTool(); + virtual ~ConnectorTool(); + + Inkscape::Selection *selection; + Geom::Point p[5]; + + /** \invar npoints in {0, 2}. */ + gint npoints; + unsigned int state : 4; + + // Red curve + SPCanvasItem *red_bpath; + SPCurve *red_curve; + guint32 red_color; + + // Green curve + SPCurve *green_curve; + + // The new connector + SPItem *newconn; + Avoid::ConnRef *newConnRef; + gdouble curvature; + bool isOrthogonal; + + // The active shape + SPItem *active_shape; + Inkscape::XML::Node *active_shape_repr; + Inkscape::XML::Node *active_shape_layer_repr; + + // Same as above, but for the active connector + SPItem *active_conn; + Inkscape::XML::Node *active_conn_repr; + sigc::connection sel_changed_connection; + + // The activehandle + SPKnot *active_handle; + + // The selected handle, used in editing mode + SPKnot *selected_handle; + + SPItem *clickeditem; + SPKnot *clickedhandle; + + SPKnotList knots; + SPKnot *endpt_handle[2]; + guint endpt_handler_id[2]; + gchar *shref; + gchar *ehref; + SPCanvasItem *c0, *c1, *cl0, *cl1; + + static const std::string prefsPath; + + virtual void setup(); + virtual void finish(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); + virtual bool item_handler(SPItem* item, GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +private: + void selection_changed(Inkscape::Selection *selection); +}; + +void cc_selection_set_avoid(bool const set_ignore); +void cc_create_connection_point(ConnectorTool* cc); +void cc_remove_connection_point(ConnectorTool* cc); +bool cc_item_is_connector(SPItem *item); + +} +} +} + +#endif /* !SEEN_CONNECTOR_CONTEXT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/dropper-tool.cpp b/src/ui/tools/dropper-tool.cpp new file mode 100644 index 000000000..9c47b50e9 --- /dev/null +++ b/src/ui/tools/dropper-tool.cpp @@ -0,0 +1,414 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <glibmm/i18n.h> +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> +#include <2geom/transforms.h> +#include <2geom/circle.h> + +#include "macros.h" +#include "display/canvas-bpath.h" +#include "display/canvas-arena.h" +#include "display/curve.h" +#include "display/cairo-utils.h" +#include "svg/svg-color.h" +#include "color.h" +#include "color-rgba.h" +#include "desktop-style.h" +#include "preferences.h" +#include "sp-namedview.h" +#include "sp-cursor.h" +#include "desktop.h" +#include "desktop-handles.h" +#include "selection.h" +#include "document.h" +#include "document-undo.h" + +#include "pixmaps/cursor-dropper-f.xpm" +#include "pixmaps/cursor-dropper-s.xpm" + +#include "ui/tools/dropper-tool.h" +#include "message-context.h" +#include "verbs.h" +#include "ui/tools/tool-base.h" + +using Inkscape::DocumentUndo; + +static GdkCursor *cursor_dropper_fill = NULL; +static GdkCursor *cursor_dropper_stroke = NULL; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createDropperContext() { + return new DropperTool(); + } + + bool dropperContextRegistered = ToolFactory::instance().registerObject("/tools/dropper", createDropperContext); +} + +const std::string& DropperTool::getPrefsPath() { + return DropperTool::prefsPath; +} + +const std::string DropperTool::prefsPath = "/tools/dropper"; + +DropperTool::DropperTool() : ToolBase() { + this->R = 0; + this->G = 0; + this->B = 0; + this->alpha = 0; + this->dragging = false; + + this->grabbed = 0; + this->area = 0; + this->centre = Geom::Point(0, 0); + + this->cursor_shape = cursor_dropper_f_xpm; + this->hot_x = 7; + this->hot_y = 7; + + cursor_dropper_fill = sp_cursor_new_from_xpm(cursor_dropper_f_xpm , 7, 7); + cursor_dropper_stroke = sp_cursor_new_from_xpm(cursor_dropper_s_xpm , 7, 7); +} + +DropperTool::~DropperTool() { +} + +void DropperTool::setup() { + ToolBase::setup(); + + /* TODO: have a look at CalligraphicTool::setup where the same is done.. generalize? */ + Geom::PathVector path; + Geom::Circle(0, 0, 1).getPath(path); + + SPCurve *c = new SPCurve(path); + + this->area = sp_canvas_bpath_new(sp_desktop_controls(this->desktop), c); + + c->unref(); + + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->area), 0x00000000,(SPWindRule)0); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->area), 0x0000007f, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_hide(this->area); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/dropper/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/dropper/gradientdrag")) { + this->enableGrDrag(); + } +} + +void DropperTool::finish() { + this->enableGrDrag(false); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, GDK_CURRENT_TIME); + this->grabbed = NULL; + } + + if (this->area) { + sp_canvas_item_destroy(this->area); + this->area = NULL; + } + + if (cursor_dropper_fill) { +#if GTK_CHECK_VERSION(3,0,0) + g_object_unref(cursor_dropper_fill); +#else + gdk_cursor_unref (cursor_dropper_fill); +#endif + cursor_dropper_fill = NULL; + } + + if (cursor_dropper_stroke) { +#if GTK_CHECK_VERSION(3,0,0) + g_object_unref(cursor_dropper_stroke); +#else + gdk_cursor_unref (cursor_dropper_stroke); +#endif + cursor_dropper_fill = NULL; + } +} + +/** + * Returns the current dropper context color. + */ +guint32 DropperTool::get_color() { + 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); + + return SP_RGBA32_F_COMPOSE(this->R, + this->G, + this->B, + (pick == SP_DROPPER_PICK_ACTUAL && setalpha) ? this->alpha : 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); + bool setalpha = prefs->getBool("/tools/dropper/setalpha", true); + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + this->centre = Geom::Point(event->button.x, event->button.y); + this->dragging = true; + ret = TRUE; + } + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK, + NULL, event->button.time); + this->grabbed = SP_CANVAS_ITEM(desktop->acetate); + + 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 if (!this->space_panning) { + // otherwise, constantly calculate color no matter is any button pressed or not + + // If one time pick with stroke set the pixmap + if (prefs->getBool("/tools/dropper/onetimepick", false) && prefs->getInt("/dialogs/fillstroke/page", 0) == 1) { + //TODO Only set when not set already + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(sp_desktop_canvas(desktop))); + gdk_window_set_cursor(window, cursor_dropper_stroke); + } + + double rw = 0.0; + double R(0), G(0), B(0), A(0); + + if (this->dragging) { + // calculate average + + // radius + 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; + } + + 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) ); + sp_canvas_item_affine_absolute(this->area, sm); + sp_canvas_item_show(this->area); + + /* Get buffer */ + Geom::Rect r(this->centre, this->centre); + r.expandBy(rw); + if (!r.hasZeroArea()) { + Geom::IntRect area = r.roundOutwards(); + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, area.width(), area.height()); + sp_canvas_arena_render_surface(SP_CANVAS_ARENA(sp_desktop_drawing(desktop)), s, area); + ink_cairo_surface_average_color_premul(s, R, G, B, A); + cairo_surface_destroy(s); + } + } else { + // pick single pixel + Geom::IntRect area = Geom::IntRect::from_xywh(floor(event->button.x), floor(event->button.y), 1, 1); + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); + sp_canvas_arena_render_surface(SP_CANVAS_ARENA(sp_desktop_drawing(desktop)), s, area); + ink_cairo_surface_average_color_premul(s, R, G, B, A); + cairo_surface_destroy(s); + } + + if (pick == SP_DROPPER_PICK_VISIBLE) { + // compose with page color + guint32 bg = sp_desktop_namedview(desktop)->pagecolor; + R = R + (SP_RGBA32_R_F(bg)) * (1 - A); + G = G + (SP_RGBA32_G_F(bg)) * (1 - A); + B = B + (SP_RGBA32_B_F(bg)) * (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 + this->R = R; + this->G = G; + this->B = B; + this->alpha = A; + + // status message + double alpha_to_set = setalpha? this->alpha : 1.0; + guint32 c32 = SP_RGBA32_F_COMPOSE(R, G, B, alpha_to_set); + + gchar c[64]; + sp_svg_write_color(c, sizeof(c), c32); + + // 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"), alpha_to_set); + // where the color is picked, to show in the statusbar + gchar *where = this->dragging ? g_strdup_printf(_(", averaged with radius %d"), (int) rw) : 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); + + ret = TRUE; + } + break; + + case GDK_BUTTON_RELEASE: + if (event->button.button == 1 && !this->space_panning) { + sp_canvas_item_hide(this->area); + this->dragging = false; + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, event->button.time); + this->grabbed = NULL; + } + + double alpha_to_set = setalpha? this->alpha : 1.0; + + bool fill = !(event->button.state & GDK_SHIFT_MASK); // Stroke if Shift key held + + if (prefs->getBool("/tools/dropper/onetimepick", false)) { + // "One time" pick from Fill/Stroke dialog stroke page, always apply fill or stroke (ignore <Shift> key) + fill = (prefs->getInt("/dialogs/fillstroke/page", 0) == 0) ? true : false; + } + + // do the actual color setting + sp_desktop_set_color(desktop, + (event->button.state & GDK_MOD1_MASK)? + ColorRGBA(1 - this->R, 1 - this->G, 1 - this->B, alpha_to_set) : ColorRGBA(this->R, this->G, this->B, alpha_to_set), + false, fill); + + // REJON: set aux. toolbar input to hex color! + + if (event->button.state & GDK_SHIFT_MASK) { + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(sp_desktop_canvas(desktop))); + gdk_window_set_cursor(window, cursor_dropper_stroke); + } + + if (!(sp_desktop_selection(desktop)->isEmpty())) { + DocumentUndo::done(sp_desktop_document(desktop), SP_VERB_CONTEXT_DROPPER, + _("Set picked color")); + } + + if (prefs->getBool("/tools/dropper/onetimepick", false)) { + prefs->setBool("/tools/dropper/onetimepick", false); + sp_toggle_dropper(desktop); + } + + ret = TRUE; + } + break; + + case GDK_KEY_PRESS: + switch (get_group0_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: + sp_desktop_selection(desktop)->clear(); + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + if (!desktop->isWaitingCursor() && !prefs->getBool("/tools/dropper/onetimepick", false)) { + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(sp_desktop_canvas(desktop))); + gdk_window_set_cursor(window, cursor_dropper_stroke); + } + + break; + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (get_group0_keyval(&event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + if (!desktop->isWaitingCursor() && !prefs->getBool("/tools/dropper/onetimepick", false)) { + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(sp_desktop_canvas(desktop))); + gdk_window_set_cursor(window, cursor_dropper_fill); + } + 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/dropper-tool.h b/src/ui/tools/dropper-tool.h new file mode 100644 index 000000000..dd6f25bb6 --- /dev/null +++ b/src/ui/tools/dropper-tool.h @@ -0,0 +1,73 @@ +#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, read the file 'COPYING' for more information + */ + +#include "ui/tools/tool-base.h" + +#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 +}; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class DropperTool : public ToolBase { +public: + DropperTool(); + virtual ~DropperTool(); + + static const std::string prefsPath; + + virtual const std::string& getPrefsPath(); + + guint32 get_color(); + +protected: + virtual void setup(); + virtual void finish(); + virtual bool root_handler(GdkEvent* event); + +private: + double R; + double G; + double B; + double alpha; + + bool dragging; + + SPCanvasItem* grabbed; + SPCanvasItem* area; + Geom::Point centre; +}; + +} +} +} + +#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 000000000..cec58dce9 --- /dev/null +++ b/src/ui/tools/dynamic-base.cpp @@ -0,0 +1,162 @@ + +#include "ui/tools/dynamic-base.h" + +#include <gtk/gtk.h> + +#include "config.h" + +#include "message-context.h" +#include "streq.h" +#include "preferences.h" +#include "display/sp-canvas-item.h" +#include "desktop.h" + +#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() : + ToolBase(), + accumulated(NULL), + segments(NULL), + currentshape(NULL), + currentcurve(NULL), + cal1(NULL), + cal2(NULL), + point1(), + point2(), + repr(NULL), + 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() { + if (this->accumulated) { + this->accumulated = this->accumulated->unref(); + this->accumulated = 0; + } + + while (this->segments) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(this->segments->data)); + this->segments = g_slist_remove(this->segments, this->segments->data); + } + + if (this->currentcurve) { + this->currentcurve = this->currentcurve->unref(); + this->currentcurve = 0; + } + + if (this->cal1) { + this->cal1 = this->cal1->unref(); + this->cal1 = 0; + } + + if (this->cal2) { + this->cal2 = this->cal2->unref(); + this->cal2 = 0; + } + + if (this->currentshape) { + sp_canvas_item_destroy(this->currentshape); + this->currentshape = 0; + } +} + +void DynamicBase::set(const Inkscape::Preferences::Entry& value) { + Glib::ustring path = value.getEntryName(); + + // ignore preset modifications + static Glib::ustring const presets_path = this->pref_observer->observed_path + "/preset"; + Glib::ustring const &full_path = value.getPath(); + + 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.getInt(10), 1, 100); + } 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(), 0, 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 { + Geom::Rect drect = this->desktop->get_display_area(); + + double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] ); + + return Geom::Point(( v[Geom::X] - drect.min()[Geom::X] ) / max, ( v[Geom::Y] - drect.min()[Geom::Y] ) / max); +} + +/* Get view point */ +Geom::Point DynamicBase::getViewPoint(Geom::Point n) const { + Geom::Rect drect = this->desktop->get_display_area(); + + double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] ); + + return Geom::Point(n[Geom::X] * max + drect.min()[Geom::X], n[Geom::Y] * max + drect.min()[Geom::Y]); +} + +} +} +} + +/* + 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 000000000..9218eabd3 --- /dev/null +++ b/src/ui/tools/dynamic-base.h @@ -0,0 +1,123 @@ +#ifndef COMMON_CONTEXT_H_SEEN +#define COMMON_CONTEXT_H_SEEN + +/* + * Common 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, read the file 'COPYING' for more information + */ + +#include "ui/tools/tool-base.h" +#include "display/curve.h" +#include <2geom/point.h> + +#define SAMPLING_SIZE 8 /* fixme: ?? */ + +namespace Inkscape { +namespace UI { +namespace Tools { + +class DynamicBase : public ToolBase { +public: + DynamicBase(); + virtual ~DynamicBase(); + + virtual void set(const Inkscape::Preferences::Entry& val); + +protected: + /** accumulated shape which ultimately goes in svg:path */ + SPCurve *accumulated; + + /** canvas items for "comitted" segments */ + GSList *segments; + + /** canvas item for red "leading" segment */ + SPCanvasItem *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 */ + guint dragging : 1; /* mouse state: mouse is dragging */ + guint usepressure : 1; + guint usetilt : 1; + double mass, drag; + double angle; + double width; + + double vel_thin; + double flatness; + double tremor; + double cap_rounding; + + //Inkscape::MessageContext *_message_context; + + 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 000000000..270987d27 --- /dev/null +++ b/src/ui/tools/eraser-tool.cpp @@ -0,0 +1,1019 @@ +/* + * 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 + * + * 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, read the file 'COPYING' for more information + */ + +#define noERASER_VERBOSE + +#include "config.h" + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> +#include <string> +#include <cstring> +#include <numeric> + +#include "svg/svg.h" +#include "display/sp-canvas.h" +#include "display/canvas-bpath.h" +#include <2geom/bezier-utils.h> + +#include <glib.h> +#include "macros.h" +#include "document.h" +#include "selection.h" +#include "desktop.h" +#include "desktop-events.h" +#include "desktop-handles.h" +#include "desktop-style.h" +#include "message-context.h" +#include "preferences.h" +#include "pixmaps/cursor-eraser.xpm" +#include "xml/repr.h" +#include "context-fns.h" +#include "sp-item.h" +#include "color.h" +#include "rubberband.h" +#include "splivarot.h" +#include "sp-item-group.h" +#include "sp-shape.h" +#include "sp-path.h" +#include "sp-text.h" +#include "display/canvas-bpath.h" +#include "display/canvas-arena.h" +#include "livarot/Shape.h" +#include "document-undo.h" +#include "verbs.h" +#include <2geom/math-utils.h> +#include <2geom/pathvector.h> + +#include "ui/tools/eraser-tool.h" + +using Inkscape::DocumentUndo; + +#define ERC_RED_RGBA 0xff0000ff + +#define TOLERANCE_ERASER 0.1 + +#define ERASER_EPSILON 0.5e-6 +#define ERASER_EPSILON_START 0.5e-2 +#define ERASER_VEL_START 1e-5 + +#define DRAG_MIN 0.0 +#define DRAG_DEFAULT 1.0 +#define DRAG_MAX 1.0 + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createEraserContext() { + return new EraserTool(); + } + + bool eraserContextRegistered = ToolFactory::instance().registerObject("/tools/eraser", createEraserContext); +} + +const std::string& EraserTool::getPrefsPath() { + return EraserTool::prefsPath; +} + +const std::string EraserTool::prefsPath = "/tools/eraser"; + +EraserTool::EraserTool() : DynamicBase() { + this->cursor_shape = cursor_eraser_xpm; + this->hot_x = 4; + this->hot_y = 4; +} + +EraserTool::~EraserTool() { +} + +void EraserTool::setup() { + DynamicBase::setup(); + + this->accumulated = new SPCurve(); + this->currentcurve = new SPCurve(); + + this->cal1 = new SPCurve(); + this->cal2 = new SPCurve(); + + this->currentshape = sp_canvas_item_new(sp_desktop_sketch(desktop), SP_TYPE_CANVAS_BPATH, NULL); + + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->currentshape), ERC_RED_RGBA, SP_WIND_RULE_EVENODD); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->currentshape), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + /* fixme: Cannot we cascade it to root more clearly? */ + g_signal_connect(G_OBJECT(this->currentshape), "event", G_CALLBACK(sp_desktop_root_handler), desktop); + +/* +static ProfileFloatElement f_profile[PROFILE_FLOAT_SIZE] = { + {"mass",0.02, 0.0, 1.0}, + {"wiggle",0.0, 0.0, 1.0}, + {"angle",30.0, -90.0, 90.0}, + {"thinning",0.1, -1.0, 1.0}, + {"tremor",0.0, 0.0, 1.0}, + {"flatness",0.9, 0.0, 1.0}, + {"cap_rounding",0.0, 0.0, 5.0} +}; +*/ + + 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"); + + this->is_drawing = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/eraser/selcue", 0) != 0) { + this->enableSelectionCue(); + } + + // TODO temp force: + this->enableSelectionCue(); +} + +static double +flerp(double f0, double f1, double p) +{ + return f0 + ( f1 - f0 ) * p; +} + +void EraserTool::reset(Geom::Point p) { + this->last = this->cur = 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 EraserTool::extinput(GdkEvent *event) { + if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &this->pressure)) + this->pressure = CLAMP (this->pressure, ERC_MIN_PRESSURE, ERC_MAX_PRESSURE); + else + this->pressure = ERC_DEFAULT_PRESSURE; + + if (gdk_event_get_axis (event, GDK_AXIS_XTILT, &this->xtilt)) + this->xtilt = CLAMP (this->xtilt, ERC_MIN_TILT, ERC_MAX_TILT); + else + this->xtilt = ERC_DEFAULT_TILT; + + if (gdk_event_get_axis (event, GDK_AXIS_YTILT, &this->ytilt)) + this->ytilt = CLAMP (this->ytilt, ERC_MIN_TILT, ERC_MAX_TILT); + else + this->ytilt = ERC_DEFAULT_TILT; +} + + +bool EraserTool::apply(Geom::Point p) { + Geom::Point n = 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 ERASER_EPSILON, + // or we haven't yet reached ERASER_VEL_START (i.e. at the beginning of stroke) + // _and_ the force is below the (higher) ERASER_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) < ERASER_EPSILON || (this->vel_max < ERASER_VEL_START && Geom::L2(force) < ERASER_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: + gdouble length = std::sqrt(this->xtilt*this->xtilt + this->ytilt*this->ytilt);; + + if (length > 0) { + Geom::Point ang1 = Geom::Point(this->ytilt/length, this->xtilt/length); + a1 = atan2(ang1); + } + else + a1 = 0.0; + } + else { + // 1b. fixed dc->angle (absolutely flat nib): + double const radians = ( (this->angle - 90) / 180.0 ) * M_PI; + Geom::Point ang1 = Geom::Point(-sin(radians), cos(radians)); + a1 = atan2(ang1); + } + + // 2. perpendicular to dc->vel (absolutely non-flat nib): + gdouble const mag_vel = Geom::L2(this->vel); + if ( mag_vel < ERASER_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 - 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 EraserTool::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 = SP_EVENT_CONTEXT(dc)->desktop->d2w(brush); + + double trace_thick = 1; + + 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 /= SP_EVENT_CONTEXT(this)->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_erc_update_toolbox (SPDesktop *desktop, const gchar *id, double value) +{ + desktop->setToolboxAdjustmentValue (id, value); +} + +void EraserTool::cancel() { + SPDesktop *desktop = SP_EVENT_CONTEXT(this)->desktop; + this->dragging = FALSE; + this->is_drawing = false; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), 0); + /* Remove all temporary line segments */ + while (this->segments) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(this->segments->data)); + this->segments = g_slist_remove(this->segments, this->segments->data); + } + /* reset accumulated curve */ + this->accumulated->reset(); + this->clear_current(); + if (this->repr) { + this->repr = NULL; + } +} + +bool EraserTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + if (Inkscape::have_viable_layer(desktop, this->message_context) == false) { + return TRUE; + } + + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point const button_dt(desktop->w2d(button_w)); + + this->reset(button_dt); + this->extinput(event); + this->apply(button_dt); + + this->accumulated->reset(); + + if (this->repr) { + this->repr = NULL; + } + + Inkscape::Rubberband::get(desktop)->start(desktop, button_dt); + Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH); + + /* initialize first point */ + this->npoints = 0; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + ( GDK_KEY_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_BUTTON_PRESS_MASK ), + NULL, + event->button.time); + + ret = TRUE; + + desktop->canvas->forceFullRedrawAfterInterruptions(3); + this->is_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(); + + if ( this->is_drawing && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + this->dragging = TRUE; + + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> an eraser stroke")); + + 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; + } + + Inkscape::Rubberband::get(desktop)->move(motion_dt); + } + 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)); + + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), event->button.time); + desktop->canvas->endForcedFullRedraws(); + this->is_drawing = false; + + if (this->dragging && event->button.button == 1 && !this->space_panning) { + this->dragging = FALSE; + + this->apply(motion_dt); + + /* Remove all temporary line segments */ + while (this->segments) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(this->segments->data)); + this->segments = g_slist_remove(this->segments, this->segments->data); + } + + /* Create object */ + this->fit_and_split(true); + this->accumulate(); + this->set_to_accumulated(); // performs document_done + + /* reset accumulated curve */ + this->accumulated->reset(); + + this->clear_current(); + if (this->repr) { + this->repr = NULL; + } + + this->message_context->clear(); + ret = TRUE; + } + + if (Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->stop(); + } + + break; + } + + case GDK_KEY_PRESS: + switch (get_group0_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_erc_update_toolbox (desktop, "eraser-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_erc_update_toolbox (desktop, "eraser-angle", this->angle); + 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; + } + + sp_erc_update_toolbox (desktop, "altx-eraser", 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 -= 0.01; + + if (this->width < 0.01) { + this->width = 0.01; + } + + sp_erc_update_toolbox (desktop, "altx-eraser", this->width * 100); + ret = TRUE; + } + break; + + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + this->width = 0.01; + sp_erc_update_toolbox (desktop, "altx-eraser", this->width * 100); + ret = TRUE; + break; + + case GDK_KEY_End: + case GDK_KEY_KP_End: + this->width = 1.0; + sp_erc_update_toolbox (desktop, "altx-eraser", this->width * 100); + ret = TRUE; + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-eraser"); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + Inkscape::Rubberband::get(desktop)->stop(); + + 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_group0_keyval(&event->key)) { + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + this->message_context->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = DynamicBase::root_handler(event); + } + + return ret; +} + +void EraserTool::clear_current() { + // reset bpath + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), NULL); + + // reset curve + this->currentcurve->reset(); + this->cal1->reset(); + this->cal2->reset(); + + // reset points + this->npoints = 0; +} + +void EraserTool::set_to_accumulated() { + bool workDone = false; + + if (!this->accumulated->is_empty()) { + if (!this->repr) { + /* Create object */ + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + /* Set style */ + sp_desktop_apply_style_tool (desktop, repr, "/tools/eraser", false); + + this->repr = repr; + + SPItem *item=SP_ITEM(desktop->currentLayer()->appendChildRepr(this->repr)); + Inkscape::GC::release(this->repr); + + item->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + item->updateRepr(); + } + + Geom::PathVector pathv = this->accumulated->get_pathvector() * desktop->dt2doc(); + gchar *str = sp_svg_write_path(pathv); + g_assert( str != NULL ); + this->repr->setAttribute("d", str); + g_free(str); + + if ( this->repr ) { + bool wasSelection = false; + Inkscape::Selection *selection = sp_desktop_selection(desktop); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + gint eraserMode = prefs->getBool("/tools/eraser/mode") ? 1 : 0; + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + + SPItem* acid = SP_ITEM(desktop->doc()->getObjectByRepr(this->repr)); + Geom::OptRect eraserBbox = acid->visualBounds(); + Geom::Rect bounds = (*eraserBbox) * desktop->doc2dt(); + std::vector<SPItem*> remainingItems; + GSList* toWorkOn = 0; + + if (selection->isEmpty()) { + if ( eraserMode ) { + toWorkOn = sp_desktop_document(desktop)->getItemsPartiallyInBox(desktop->dkey, bounds); + } else { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + toWorkOn = sp_desktop_document(desktop)->getItemsAtPoints(desktop->dkey, r->getPoints()); + } + + toWorkOn = g_slist_remove( toWorkOn, acid ); + } else { + toWorkOn = g_slist_copy(const_cast<GSList*>(selection->itemList())); + wasSelection = true; + } + + if ( g_slist_length(toWorkOn) > 0 ) { + if ( eraserMode ) { + for (GSList *i = toWorkOn ; i ; i = i->next ) { + SPItem *item = SP_ITEM(i->data); + + if ( eraserMode ) { + Geom::OptRect bbox = item->visualBounds(); + + if (bbox && bbox->intersects(*eraserBbox)) { + Inkscape::XML::Node* dup = this->repr->duplicate(xml_doc); + this->repr->parent()->appendChild(dup); + Inkscape::GC::release(dup); // parent takes over + + selection->set(item); + selection->add(dup); + sp_selected_path_diff_skip_undo(selection, desktop); + workDone = true; // TODO set this only if something was cut. + + if ( !selection->isEmpty() ) { + // If the item was not completely erased, track the new remainder. + GSList *nowSel = g_slist_copy(const_cast<GSList *>(selection->itemList())); + + for (GSList const *i2 = nowSel ; i2 ; i2 = i2->next ) { + remainingItems.push_back(SP_ITEM(i2->data)); + } + + g_slist_free(nowSel); + } + } else { + remainingItems.push_back(item); + } + } + } + } else { + for (GSList *i = toWorkOn ; i ; i = i->next ) { + sp_object_ref( SP_ITEM(i->data), 0 ); + } + + for (GSList *i = toWorkOn ; i ; i = i->next ) { + SPItem *item = SP_ITEM(i->data); + item->deleteObject(true); + sp_object_unref(item); + workDone = true; + } + } + + g_slist_free(toWorkOn); + + if ( !eraserMode ) { + //sp_selection_delete(desktop); + remainingItems.clear(); + } + + selection->clear(); + + if ( wasSelection ) { + if ( !remainingItems.empty() ) { + selection->add(remainingItems.begin(), remainingItems.end()); + } + } + } + + // Remove the eraser stroke itself: + sp_repr_unparent( this->repr ); + this->repr = 0; + } + } else { + if (this->repr) { + sp_repr_unparent(this->repr); + this->repr = 0; + } + } + + + if ( workDone ) { + DocumentUndo::done(sp_desktop_document(desktop), SP_VERB_CONTEXT_ERASER, _("Draw eraser stroke")); + } else { + DocumentUndo::cancel(sp_desktop_document(desktop)); + } +} + +static void +add_cap(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 ) / sqrt(2.0); + double mag = Geom::L2(vel); + + Geom::Point v_in = from - pre; + double mag_in = Geom::L2(v_in); + + if ( mag_in > ERASER_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 > ERASER_EPSILON ) { + v_out = mag * v_out / mag_out; + } else { + v_out = Geom::Point(0, 0); + } + + if ( Geom::L2(v_in) > ERASER_EPSILON || Geom::L2(v_out) > ERASER_EPSILON ) { + curve->curveto(from + v_in, to + v_out, to); + } +} + +void EraserTool::accumulate() { + if ( !this->cal1->is_empty() && !this->cal2->is_empty() ) { + this->accumulated->reset(); /* Is this required ?? */ + SPCurve *rev_cal2 = this->cal2->create_reverse(); + + g_assert(this->cal1->get_segment_count() > 0); + g_assert(rev_cal2->get_segment_count() > 0); + g_assert( ! this->cal1->first_path()->closed() ); + g_assert( ! rev_cal2->first_path()->closed() ); + + Geom::CubicBezier const * dc_cal1_firstseg = dynamic_cast<Geom::CubicBezier const *>( this->cal1->first_segment() ); + Geom::CubicBezier const * rev_cal2_firstseg = dynamic_cast<Geom::CubicBezier const *>( rev_cal2->first_segment() ); + Geom::CubicBezier const * dc_cal1_lastseg = dynamic_cast<Geom::CubicBezier const *>( this->cal1->last_segment() ); + Geom::CubicBezier const * rev_cal2_lastseg = dynamic_cast<Geom::CubicBezier 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 ); + + this->accumulated->append(this->cal1, FALSE); + + add_cap(this->accumulated, (*dc_cal1_lastseg)[2], (*dc_cal1_lastseg)[3], (*rev_cal2_firstseg)[0], (*rev_cal2_firstseg)[1], this->cap_rounding); + + this->accumulated->append(rev_cal2, TRUE); + + add_cap(this->accumulated, (*rev_cal2_lastseg)[2], (*rev_cal2_lastseg)[3], (*dc_cal1_firstseg)[0], (*dc_cal1_firstseg)[1], this->cap_rounding); + + this->accumulated->closepath(); + + rev_cal2->unref(); + + this->cal1->reset(); + this->cal2->reset(); + } +} + +static double square(double const x) +{ + return x * x; +} + +void EraserTool::fit_and_split(bool release) { + double const tolerance_sq = square( desktop->w2d().descrim() * TOLERANCE_ERASER ); + +#ifdef ERASER_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 ERASER_VERBOSE + g_print("[F&S:#] this->npoints:%d, release:%s\n", + dc->npoints, release ? "TRUE" : "FALSE"); +#endif + + /* Current eraser */ + if ( this->cal1->is_empty() || this->cal2->is_empty() ) { + /* dc->npoints > 0 */ + /* g_print("erasers(1|2) reset\n"); */ + this->cal1->reset(); + this->cal2->reset(); + + this->cal1->moveto(this->point1[0]); + this->cal2->moveto(this->point2[0]); + } + + Geom::Point b1[BEZIER_MAX_LENGTH]; + gint const nb1 = Geom::bezier_fit_cubic_r(b1, this->point1, this->npoints, tolerance_sq, BEZIER_MAX_BEZIERS); + g_assert( nb1 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b1)) ); + + Geom::Point b2[BEZIER_MAX_LENGTH]; + gint const nb2 = Geom::bezier_fit_cubic_r(b2, this->point2, this->npoints, tolerance_sq, BEZIER_MAX_BEZIERS); + g_assert( nb2 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b2)) ); + + if ( nb1 != -1 && nb2 != -1 ) { + /* Fit and draw and reset state */ +#ifdef ERASER_VERBOSE + g_print("nb1:%d nb2:%d\n", nb1, nb2); +#endif + + /* CanvasShape */ + if (! release) { + this->currentcurve->reset(); + this->currentcurve->moveto(b1[0]); + + for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) { + this->currentcurve->curveto(bp1[1], bp1[2], bp1[3]); + } + + this->currentcurve->lineto(b2[BEZIER_SIZE*(nb2-1) + 3]); + + for (Geom::Point *bp2 = b2 + BEZIER_SIZE * ( nb2 - 1 ); bp2 >= b2; bp2 -= BEZIER_SIZE) { + this->currentcurve->curveto(bp2[2], bp2[1], bp2[0]); + } + + // FIXME: this->segments is always NULL at this point?? + if (!this->segments) { // first segment + add_cap(this->currentcurve, b2[1], b2[0], b1[0], b1[1], this->cap_rounding); + } + + this->currentcurve->closepath(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve); + } + + /* Current eraser */ + for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) { + this->cal1->curveto(bp1[1], bp1[2], bp1[3]); + } + + for (Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) { + this->cal2->curveto(bp2[1], bp2[2], bp2[3]); + } + } else { + /* fixme: ??? */ +#ifdef ERASER_VERBOSE + g_print("[fit_and_split] failed to fit-cubic.\n"); +#endif + this->draw_temporary_box(); + + for (gint i = 1; i < this->npoints; i++) { + this->cal1->lineto(this->point1[i]); + } + + for (gint i = 1; i < this->npoints; i++) { + this->cal2->lineto(this->point2[i]); + } + } + + /* Fit and draw and copy last point */ +#ifdef ERASER_VERBOSE + g_print("[%d]Yup\n", this->npoints); +#endif + if (!release) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint eraserMode = prefs->getBool("/tools/eraser/mode") ? 1 : 0; + g_assert(!this->currentcurve->is_empty()); + + SPCanvasItem *cbp = sp_canvas_item_new(sp_desktop_sketch(desktop), SP_TYPE_CANVAS_BPATH, NULL); + SPCurve *curve = this->currentcurve->copy(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH (cbp), curve); + curve->unref(); + + guint32 fillColor = sp_desktop_get_color_tool (desktop, "/tools/eraser", true); + //guint32 strokeColor = sp_desktop_get_color_tool (desktop, "/tools/eraser", false); + double opacity = sp_desktop_get_master_opacity_tool (desktop, "/tools/eraser"); + double fillOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/eraser", true); + //double strokeOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/eraser", false); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(cbp), ((fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*fillOpacity)), SP_WIND_RULE_EVENODD); + //on second thougtht don't do stroke yet because we don't have stoke-width yet and because stoke appears between segments while drawing + //sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), ((strokeColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*strokeOpacity)), 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + /* fixme: Cannot we cascade it to root more clearly? */ + g_signal_connect(G_OBJECT(cbp), "event", G_CALLBACK(sp_desktop_root_handler), desktop); + + this->segments = g_slist_prepend(this->segments, cbp); + + if ( !eraserMode ) { + sp_canvas_item_hide(cbp); + sp_canvas_item_hide(this->currentshape); + } + } + + 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 EraserTool::draw_temporary_box() { + this->currentcurve->reset(); + + this->currentcurve->moveto(this->point1[this->npoints-1]); + + for (gint i = this->npoints-2; i >= 0; i--) { + this->currentcurve->lineto(this->point1[i]); + } + + for (gint i = 0; i < this->npoints; i++) { + this->currentcurve->lineto(this->point2[i]); + } + + if (this->npoints >= 2) { + add_cap(this->currentcurve, this->point2[this->npoints-2], this->point2[this->npoints-1], this->point1[this->npoints-1], this->point1[this->npoints-2], this->cap_rounding); + } + + this->currentcurve->closepath(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve); +} + +} +} +} + +/* + 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 000000000..eb7eb16e8 --- /dev/null +++ b/src/ui/tools/eraser-tool.h @@ -0,0 +1,76 @@ +#ifndef SP_ERASER_CONTEXT_H_SEEN +#define SP_ERASER_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. + * Copyright (C) 2008 Jon A. Cruz + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include "ui/tools/dynamic-base.h" + +#define ERC_MIN_PRESSURE 0.0 +#define ERC_MAX_PRESSURE 1.0 +#define ERC_DEFAULT_PRESSURE 1.0 + +#define ERC_MIN_TILT -1.0 +#define ERC_MAX_TILT 1.0 +#define ERC_DEFAULT_TILT 0.0 + +namespace Inkscape { +namespace UI { +namespace Tools { + +class EraserTool : public DynamicBase { +public: + EraserTool(); + virtual ~EraserTool(); + + static const std::string prefsPath; + + virtual void setup(); + virtual bool root_handler(GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +private: + void reset(Geom::Point p); + void extinput(GdkEvent *event); + bool apply(Geom::Point p); + void brush(); + void cancel(); + void clear_current(); + void set_to_accumulated(); + void accumulate(); + void fit_and_split(bool release); + void draw_temporary_box(); +}; + +} +} +} + +#endif // SP_ERASER_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/flood-tool.cpp b/src/ui/tools/flood-tool.cpp new file mode 100644 index 000000000..0b72bc9f2 --- /dev/null +++ b/src/ui/tools/flood-tool.cpp @@ -0,0 +1,1263 @@ +/** + * @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, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "trace/potrace/inkscape-potrace.h" +#include <2geom/pathvector.h> +#include <gdk/gdkkeysyms.h> +#include <queue> +#include <deque> +#include <glibmm/i18n.h> + +#include "color.h" +#include "context-fns.h" +#include "desktop.h" +#include "desktop-handles.h" +#include "desktop-style.h" +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-image.h" +#include "display/drawing-item.h" +#include "display/drawing.h" +#include "display/sp-canvas.h" +#include "document.h" +#include "document-undo.h" +#include "ui/tools/flood-tool.h" +#include "livarot/Path.h" +#include "livarot/Shape.h" +#include "macros.h" +#include "message-context.h" +#include "message-stack.h" +#include "preferences.h" +#include "rubberband.h" +#include "selection.h" +#include "shape-editor.h" +#include "sp-defs.h" +#include "sp-item.h" +#include "splivarot.h" +#include "sp-namedview.h" +#include "sp-object.h" +#include "sp-path.h" +#include "sp-rect.h" +#include "sp-root.h" +#include "svg/svg.h" +#include "trace/imagemap.h" +#include "trace/trace.h" +#include "xml/node-event-vector.h" +#include "xml/repr.h" +#include "verbs.h" + +#include "pixmaps/cursor-paintbucket.xpm" + +using Inkscape::DocumentUndo; + +using Inkscape::Display::ExtractARGB32; +using Inkscape::Display::ExtractRGB32; +using Inkscape::Display::AssembleARGB32; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createPaintbucketContext() { + return new FloodTool(); + } + + bool paintbucketContextRegistered = ToolFactory::instance().registerObject("/tools/paintbucket", createPaintbucketContext); +} + +const std::string& FloodTool::getPrefsPath() { + return FloodTool::prefsPath; +} + +const std::string FloodTool::prefsPath = "/tools/paintbucket"; + +FloodTool::FloodTool() : ToolBase() { + this->cursor_shape = cursor_paintbucket_xpm; + this->hot_x = 11; + this->hot_y = 30; + this->xp = 0; + this->yp = 0; + this->tolerance = 4; + this->within_tolerance = false; + this->item_to_select = NULL; + + this->item = NULL; +} + +FloodTool::~FloodTool() { + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = NULL; + + /* 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(SH_KNOTHOLDER); + this->shape_editor->set_item(selection->singleItem(), SH_KNOTHOLDER); +} + +void FloodTool::setup() { + ToolBase::setup(); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = sp_desktop_selection(this->desktop)->singleItem(); + if (item) { + this->shape_editor->set_item(item, SH_KNOTHOLDER); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = sp_desktop_selection(this->desktop)->connectChanged( + sigc::mem_fun(this, &FloodTool::selection_changed) + ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/paintbucket/selcue")) { + this->enableSelectionCue(); + } +} + + +// 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); +} + +/** + * Generate the list of trace channel selection entries. + */ +GList * flood_channels_dropdown_items_list() { + GList *glist = NULL; + + glist = g_list_append (glist, _("Visible Colors")); + glist = g_list_append (glist, _("Red")); + glist = g_list_append (glist, _("Green")); + glist = g_list_append (glist, _("Blue")); + glist = g_list_append (glist, _("Hue")); + glist = g_list_append (glist, _("Saturation")); + glist = g_list_append (glist, _("Lightness")); + glist = g_list_append (glist, _("Alpha")); + + return glist; +} + +/** + * Generate the list of autogap selection entries. + */ +GList * flood_autogap_dropdown_items_list() { + GList *glist = NULL; + + glist = g_list_append (glist, (void*) C_("Flood autogap", "None")); + glist = g_list_append (glist, (void*) C_("Flood autogap", "Small")); + glist = g_list_append (glist, (void*) C_("Flood autogap", "Medium")); + glist = g_list_append (glist, (void*) C_("Flood autogap", "Large")); + + return glist; +} + +/** + * 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) +{ + int diff = 0; + 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; + sp_color_rgb_to_hsl_floatv(hsl_check, rc / dac, gc / dac, bc / dac); + sp_color_rgb_to_hsl_floatv(hsl_orig, ro / dao, go / dao, bo / dao); + } + + switch (method) { + case FLOOD_CHANNELS_ALPHA: + return abs(static_cast<int>(ac) - ao) <= threshold; + case FLOOD_CHANNELS_R: + return abs(static_cast<int>(ac ? unpremul_alpha(rc, ac) : 0) - (ao ? unpremul_alpha(ro, ao) : 0)) <= threshold; + case FLOOD_CHANNELS_G: + return abs(static_cast<int>(ac ? unpremul_alpha(gc, ac) : 0) - (ao ? unpremul_alpha(go, ao) : 0)) <= threshold; + case FLOOD_CHANNELS_B: + return abs(static_cast<int>(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; + + diff += abs(static_cast<int>(amc ? unpremul_alpha(rmc, amc) : 0) - (amop ? unpremul_alpha(rmop, amop) : 0)); + diff += abs(static_cast<int>(amc ? unpremul_alpha(gmc, amc) : 0) - (amop ? unpremul_alpha(gmop, amop) : 0)); + diff += abs(static_cast<int>(amc ? unpremul_alpha(bmc, amc) : 0) - (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_unchecked(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 = sp_desktop_document(desktop); + + unsigned char *trace_t; + + GrayMap *gray_map = GrayMapCreate((max_x - min_x + 1), (max_y - min_y + 1)); + unsigned int gray_map_y = 0; + for (unsigned int y = min_y; y <= max_y; y++) { + unsigned long *gray_map_t = gray_map->rows[gray_map_y]; + + trace_t = get_trace_pixel(trace_px, min_x, y, bci.width); + for (unsigned int x = min_x; x <= max_x; x++) { + *gray_map_t = is_pixel_colored(trace_t) ? GRAYMAP_BLACK : GRAYMAP_WHITE; + gray_map_t++; + trace_t++; + } + gray_map_y++; + } + + Inkscape::Trace::Potrace::PotraceTracingEngine pte; + pte.keepGoing = 1; + std::vector<Inkscape::Trace::TracingEngineResult> results = pte.traceGrayMap(gray_map); + gray_map->destroy(gray_map); + + //XML Tree being used here directly while it shouldn't be...." + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + + long totalNodeCount = 0L; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double offset = prefs->getDouble("/tools/paintbucket/offset", 0.0); + + for (unsigned int i=0 ; i<results.size() ; i++) { + Inkscape::Trace::TracingEngineResult result = results[i]; + totalNodeCount += result.getNodeCount(); + + Inkscape::XML::Node *pathRepr = xml_doc->createElement("svg:path"); + /* Set style */ + sp_desktop_apply_style_tool (desktop, pathRepr, "/tools/paintbucket", false); + + Geom::PathVector pathv = sp_svg_read_pathv(result.getPathData().c_str()); + Path *path = new Path; + path->LoadPathVector(pathv); + + if (offset != 0) { + + Shape *path_shape = new Shape(); + + path->ConvertWithBackData(0.03); + path->Fill(path_shape, 0); + delete path; + + Shape *expanded_path_shape = new Shape(); + + expanded_path_shape->ConvertToShape(path_shape, fill_nonZero); + path_shape->MakeOffset(expanded_path_shape, offset * desktop->current_zoom(), join_round, 4); + expanded_path_shape->ConvertToShape(path_shape, fill_positive); + + Path *expanded_path = new Path(); + + expanded_path->Reset(); + expanded_path_shape->ConvertToForme(expanded_path); + expanded_path->ConvertEvenLines(1.0); + expanded_path->Simplify(1.0); + + delete path_shape; + delete expanded_path_shape; + + gchar *str = expanded_path->svg_dump_path(); + if (str && *str) { + pathRepr->setAttribute("d", str); + g_free(str); + } else { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Too much inset</b>, the result is empty.")); + Inkscape::GC::release(pathRepr); + g_free(str); + return; + } + + delete expanded_path; + + } else { + gchar *str = path->svg_dump_path(); + delete path; + pathRepr->setAttribute("d", str); + g_free(str); + } + + desktop->currentLayer()->addChild(pathRepr,NULL); + + SPObject *reprobj = document->getObjectByRepr(pathRepr); + if (reprobj) { + SP_ITEM(reprobj)->doWriteTransform(pathRepr, transform, NULL); + + // premultiply the item transform by the accumulated parent transform in the paste layer + Geom::Affine local (SP_GROUP(desktop->currentLayer())->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) + gchar *affinestr=sp_svg_transform_write(item_t); + pathRepr->setAttribute("transform", affinestr); + g_free(affinestr); + } + + Inkscape::Selection *selection = sp_desktop_selection(desktop); + + pathRepr->setPosition(-1); + + if (union_with_selection) { + desktop->messageStack()->flashF( Inkscape::WARNING_MESSAGE, + ngettext("Area filled, path with <b>%d</b> node created and unioned with selection.","Area filled, path with <b>%d</b> nodes created and unioned with selection.", + SP_PATH(reprobj)->nodesInPath()), SP_PATH(reprobj)->nodesInPath() ); + selection->add(reprobj); + sp_selected_path_union_skip_undo(sp_desktop_selection(desktop), desktop); + } else { + desktop->messageStack()->flashF( Inkscape::WARNING_MESSAGE, + ngettext("Area filled, path with <b>%d</b> node created.","Area filled, path with <b>%d</b> nodes created.", + SP_PATH(reprobj)->nodesInPath()), SP_PATH(reprobj)->nodesInPath() ); + selection->set(reprobj); + } + + } + + Inkscape::GC::release(pathRepr); + + } +} + +/** + * The possible return states of perform_bitmap_scanline_check(). + */ +enum ScanlineCheckResult { + SCANLINE_CHECK_OK, + SCANLINE_CHECK_ABORTED, + SCANLINE_CHECK_BOUNDARY +}; + +/** + * Determine if the provided coordinates are within the pixel buffer limits. + * @param x The X coordinate. + * @param y The Y coordinate. + * @param bci The bitmap_coords_info structure. + */ +inline static bool coords_in_range(unsigned int x, unsigned int y, bitmap_coords_info bci) { + return (x < bci.width) && + (y < bci.height); +} + +#define PAINT_DIRECTION_LEFT 1 +#define PAINT_DIRECTION_RIGHT 2 +#define PAINT_DIRECTION_UP 4 +#define PAINT_DIRECTION_DOWN 8 +#define PAINT_DIRECTION_ALL 15 + +/** + * Paint a pixel or a square (if autogap is enabled) on the trace pixel buffer. + * @param px The rendered pixel buffer to check. + * @param trace_px The trace pixel buffer. + * @param orig_color The original selected pixel to use as the fill target color. + * @param bci The bitmap_coords_info structure. + * @param original_point_trace_t The original pixel in the trace pixel buffer to check. + */ +inline static unsigned int paint_pixel(guchar *px, guchar *trace_px, guint32 orig_color, bitmap_coords_info bci, unsigned char *original_point_trace_t) { + if (bci.radius == 0) { + mark_pixel_colored(original_point_trace_t); + return PAINT_DIRECTION_ALL; + } else { + unsigned char *trace_t; + + bool can_paint_up = true; + bool can_paint_down = true; + bool can_paint_left = true; + bool can_paint_right = true; + + for (unsigned int ty = bci.y - bci.radius; ty <= bci.y + bci.radius; ty++) { + for (unsigned int tx = bci.x - bci.radius; tx <= bci.x + bci.radius; tx++) { + if (coords_in_range(tx, ty, bci)) { + trace_t = get_trace_pixel(trace_px, tx, ty, bci.width); + if (!is_pixel_colored(trace_t)) { + if (check_if_pixel_is_paintable(px, trace_t, tx, ty, orig_color, bci)) { + mark_pixel_colored(trace_t); + } else { + if (tx < bci.x) { can_paint_left = false; } + if (tx > bci.x) { can_paint_right = false; } + if (ty < bci.y) { can_paint_up = false; } + if (ty > bci.y) { can_paint_down = false; } + } + } + } + } + } + + unsigned int paint_directions = 0; + if (can_paint_left) { paint_directions += PAINT_DIRECTION_LEFT; } + if (can_paint_right) { paint_directions += PAINT_DIRECTION_RIGHT; } + if (can_paint_up) { paint_directions += PAINT_DIRECTION_UP; } + if (can_paint_down) { paint_directions += PAINT_DIRECTION_DOWN; } + + return paint_directions; + } +} + +/** + * Push a point to be checked onto the bottom of the rendered pixel buffer check queue. + * @param fill_queue The fill queue to add the point to. + * @param max_queue_size The maximum size of the fill queue. + * @param trace_t The trace pixel buffer pixel. + * @param x The X coordinate. + * @param y The Y coordinate. + */ +static void push_point_onto_queue(std::deque<Geom::Point> *fill_queue, unsigned int max_queue_size, unsigned char *trace_t, unsigned int x, unsigned int y) { + if (!is_pixel_queued(trace_t)) { + if ((fill_queue->size() < max_queue_size)) { + fill_queue->push_back(Geom::Point(x, y)); + mark_pixel_queued(trace_t); + } + } +} + +/** + * Shift a point to be checked onto the top of the rendered pixel buffer check queue. + * @param fill_queue The fill queue to add the point to. + * @param max_queue_size The maximum size of the fill queue. + * @param trace_t The trace pixel buffer pixel. + * @param x The X coordinate. + * @param y The Y coordinate. + */ +static void shift_point_onto_queue(std::deque<Geom::Point> *fill_queue, unsigned int max_queue_size, unsigned char *trace_t, unsigned int x, unsigned int y) { + if (!is_pixel_queued(trace_t)) { + if ((fill_queue->size() < max_queue_size)) { + fill_queue->push_front(Geom::Point(x, y)); + mark_pixel_queued(trace_t); + } + } +} + +/** + * Scan a row in the rendered pixel buffer and add points to the fill queue as necessary. + * @param fill_queue The fill queue to add the point to. + * @param px The rendered pixel buffer. + * @param trace_px The trace pixel buffer. + * @param orig_color The original selected pixel to use as the fill target color. + * @param bci The bitmap_coords_info structure. + */ +static ScanlineCheckResult perform_bitmap_scanline_check(std::deque<Geom::Point> *fill_queue, guchar *px, guchar *trace_px, guint32 orig_color, bitmap_coords_info bci, unsigned int *min_x, unsigned int *max_x) { + bool aborted = false; + bool reached_screen_boundary = false; + bool ok; + + bool keep_tracing; + bool initial_paint = true; + + unsigned char *current_trace_t = get_trace_pixel(trace_px, bci.x, bci.y, bci.width); + unsigned int paint_directions; + + bool currently_painting_top = false; + bool currently_painting_bottom = false; + + unsigned int top_ty = bci.y - 1; + unsigned int bottom_ty = bci.y + 1; + + bool can_paint_top = (top_ty > 0); + bool can_paint_bottom = (bottom_ty < bci.height); + + Geom::Point t = 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 ((t[Geom::X] == bci.x) && (t[Geom::Y] == bci.y)) { + fill_queue->pop_front(); t = 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 event_context The event context for this tool. + * @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(ToolBase *event_context, GdkEvent *event, bool union_with_selection, bool is_point_fill, bool is_touch_fill) { + SPDesktop *desktop = event_context->desktop; + SPDocument *document = sp_desktop_document(desktop); + + 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; + } + + double zoom_scale = desktop->current_zoom(); + + // 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; + + Geom::Rect screen = desktop->get_display_area(); + + unsigned int width = (int)ceil(screen.width() * zoom_scale * padding); + unsigned int height = (int)ceil(screen.height() * zoom_scale * padding); + + Geom::Point origin(screen.min()[Geom::X], + document->getHeight().value("px") - screen.height() - screen.min()[Geom::Y]); + + origin[Geom::X] += (screen.width() * ((1 - padding) / 2)); + origin[Geom::Y] += (screen.height() * ((1 - padding) / 2)); + + Geom::Scale scale(zoom_scale, zoom_scale); + Geom::Affine affine = scale * Geom::Translate(-origin * scale); + + 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(affine); + 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 ct(s, Geom::Point(0,0)); + // cairo_translate not necessary here - surface origin is at 0,0 + + SPNamedView *nv = sp_desktop_namedview(desktop); + bgcolor = nv->pagecolor; + // bgcolor is 0xrrggbbaa, we need 0xaarrggbb + dtc = (bgcolor >> 8) | (bgcolor << 24); + + ct.setSource(bgcolor); + ct.setOperator(CAIRO_OPERATOR_SOURCE); + ct.paint(); + ct.setOperator(CAIRO_OPERATOR_OVER); + + drawing.render(ct, 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.push_back(Geom::Point(event->button.x, event->button.y)); + } else { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + fill_points = r->getPoints(); + } + + for (unsigned int i = 0; i < fill_points.size(); i++) { + Geom::Point pw = Geom::Point(fill_points[i][Geom::X] / zoom_scale, document->getHeight().value("px") + (fill_points[i][Geom::Y] / zoom_scale)) * affine; + + pw[Geom::X] = (int)MIN(width - 1, MAX(0, pw[Geom::X])); + pw[Geom::Y] = (int)MIN(height - 1, MAX(0, pw[Geom::Y])); + + 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 = sort_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::Point min_start = Geom::Point(min_x, min_y); + + affine = scale * Geom::Translate(-origin * scale - min_start); + Geom::Affine inverted_affine = Geom::Affine(affine).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, SP_VERB_CONTEXT_PAINTBUCKET, _("Fill bounded area")); +} + +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 && !this->space_panning) { + 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(sp_desktop_document(desktop), SP_VERB_CONTEXT_PAINTBUCKET, _("Set style on object")); + + 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 && !this->space_panning) { + 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 ) && !this->space_panning) { + 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 && !this->space_panning) { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + + if (r->is_started()) { + // set "busy" cursor + desktop->setWaitingCursor(); + + if (SP_IS_EVENT_CONTEXT(this)) { + // Since setWaitingCursor runs main loop iterations, we may have already left this tool! + // So check if the tool is valid before doing anything + dragging = false; + + bool is_point_fill = this->within_tolerance; + bool is_touch_fill = event->button.state & GDK_MOD1_MASK; + + sp_flood_do_flood_fill(this, event, event->button.state & GDK_SHIFT_MASK, is_point_fill, is_touch_fill); + + desktop->clearWaitingCursor(); + // restore cursor when done; note that it may already be different if e.g. user + // switched to another tool during interruptible tracing or drawing, in which case do nothing + + ret = TRUE; + } + + r->stop(); + + //if (SP_IS_EVENT_CONTEXT(this)) { + this->defaultMessageContext()->clear(); + //} + } + } + break; + case GDK_KEY_PRESS: + switch (get_group0_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 != NULL) { + this->item->updateRepr(); + + desktop->canvas->endForcedFullRedraws(); + + sp_desktop_selection(desktop)->set(this->item); + + DocumentUndo::done(sp_desktop_document(desktop), SP_VERB_CONTEXT_PAINTBUCKET, _("Fill bounded area")); + + this->item = NULL; + } +} + +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 000000000..77dd2f13a --- /dev/null +++ b/src/ui/tools/flood-tool.h @@ -0,0 +1,74 @@ +#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 + */ + +#include <stddef.h> +#include <sigc++/sigc++.h> +#include <gtk/gtk.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) + + +#define FLOOD_COLOR_CHANNEL_R 1 +#define FLOOD_COLOR_CHANNEL_G 2 +#define FLOOD_COLOR_CHANNEL_B 4 +#define FLOOD_COLOR_CHANNEL_A 8 + +namespace Inkscape { +namespace UI { +namespace Tools { + +class FloodTool : public ToolBase { +public: + FloodTool(); + virtual ~FloodTool(); + + SPItem *item; + + sigc::connection sel_changed_connection; + + static const std::string prefsPath; + + virtual void setup(); + virtual bool root_handler(GdkEvent* event); + virtual bool item_handler(SPItem* item, GdkEvent* event); + + virtual const std::string& getPrefsPath(); + + static void set_channels(gint channels); + +private: + void selection_changed(Inkscape::Selection* selection); + void finishItem(); +}; + +GList* flood_channels_dropdown_items_list (void); +GList* flood_autogap_dropdown_items_list (void); + +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 000000000..6cf1d7a5b --- /dev/null +++ b/src/ui/tools/freehand-base.cpp @@ -0,0 +1,785 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#define DRAW_VERBOSE + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "live_effects/lpe-patternalongpath.h" +#include "display/canvas-bpath.h" +#include "xml/repr.h" +#include "svg/svg.h" +#include <glibmm/i18n.h> +#include "display/curve.h" +#include "desktop.h" +#include "desktop-handles.h" +#include "desktop-style.h" +#include "document.h" +#include "draw-anchor.h" +#include "macros.h" +#include "message-stack.h" +#include "ui/tools/pen-tool.h" +#include "ui/tools/lpe-tool.h" +#include "preferences.h" +#include "selection.h" +#include "selection-chemistry.h" +#include "snap.h" +#include "sp-path.h" +#include "sp-namedview.h" +#include "live_effects/lpe-powerstroke.h" +#include "style.h" +#include "ui/control-manager.h" +#include "ui/tools/freehand-base.h" + +#include <gdk/gdkkeysyms.h> + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void spdc_selection_changed(Inkscape::Selection *sel, FreehandBase *dc); +static void spdc_selection_modified(Inkscape::Selection *sel, guint flags, FreehandBase *dc); + +static void spdc_attach_selection(FreehandBase *dc, Inkscape::Selection *sel); + +/** + * Flushes white curve(s) and additional curve into object. + * + * No cleaning of colored curves - this has to be done by caller + * No rereading of white data, so if you cannot rely on ::modified, do it in caller + */ +static void spdc_flush_white(FreehandBase *dc, SPCurve *gc); + +static void spdc_reset_white(FreehandBase *dc); +static void spdc_free_colors(FreehandBase *dc); + +FreehandBase::FreehandBase() : ToolBase() { + this->selection = 0; + this->grab = 0; + this->anchor_statusbar = false; + + this->attach = FALSE; + + this->red_color = 0xff00007f; + this->blue_color = 0x0000ff7f; + this->green_color = 0x00ff007f; + this->red_curve_is_valid = false; + + this->red_bpath = NULL; + this->red_curve = NULL; + + this->blue_bpath = NULL; + this->blue_curve = NULL; + + this->green_bpaths = NULL; + this->green_curve = NULL; + this->green_anchor = NULL; + this->green_closed = false; + + this->white_item = NULL; + this->white_curves = NULL; + this->white_anchors = NULL; + + this->sa = NULL; + this->ea = NULL; + + this->waiting_LPE_type = Inkscape::LivePathEffect::INVALID_LPE; +} + +FreehandBase::~FreehandBase() { + if (this->grab) { + sp_canvas_item_ungrab(this->grab, GDK_CURRENT_TIME); + this->grab = NULL; + } + + if (this->selection) { + this->selection = NULL; + } + + spdc_free_colors(this); +} + +void FreehandBase::setup() { + ToolBase::setup(); + + this->selection = sp_desktop_selection(desktop); + + // Connect signals to track selection changes + this->sel_changed_connection = this->selection->connectChanged( + sigc::bind(sigc::ptr_fun(&spdc_selection_changed), this) + ); + this->sel_modified_connection = this->selection->connectModified( + sigc::bind(sigc::ptr_fun(&spdc_selection_modified), this) + ); + + // Create red bpath + this->red_bpath = sp_canvas_bpath_new(sp_desktop_sketch(this->desktop), NULL); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->red_bpath), this->red_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + // Create red curve + this->red_curve = new SPCurve(); + + // Create blue bpath + this->blue_bpath = sp_canvas_bpath_new(sp_desktop_sketch(this->desktop), NULL); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->blue_bpath), this->blue_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + // Create blue curve + this->blue_curve = new SPCurve(); + + // Create green curve + this->green_curve = new SPCurve(); + + // No green anchor by default + this->green_anchor = NULL; + this->green_closed = FALSE; + + this->attach = TRUE; + spdc_attach_selection(this, this->selection); +} + +void FreehandBase::finish() { + this->sel_changed_connection.disconnect(); + this->sel_modified_connection.disconnect(); + + if (this->grab) { + sp_canvas_item_ungrab(this->grab, GDK_CURRENT_TIME); + } + + if (this->selection) { + this->selection = NULL; + } + + 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_group0_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; +} + +static Glib::ustring const tool_name(FreehandBase *dc) +{ + return ( SP_IS_PEN_CONTEXT(dc) + ? "/tools/freehand/pen" + : "/tools/freehand/pencil" ); +} + +static void spdc_paste_curve_as_freehand_shape(const SPCurve *c, FreehandBase *dc, SPItem *item) +{ + using namespace Inkscape::LivePathEffect; + + // TODO: Don't paste path if nothing is on the clipboard + + Effect::createAndApply(PATTERN_ALONG_PATH, dc->desktop->doc(), item); + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + gchar *svgd = sp_svg_write_path(c->get_pathvector()); + static_cast<LPEPatternAlongPath*>(lpe)->pattern.paste_param_path(svgd); +} + +static void spdc_apply_powerstroke_shape(const std::vector<Geom::Point> & points, FreehandBase *dc, SPItem *item) +{ + using namespace Inkscape::LivePathEffect; + + Effect::createAndApply(POWERSTROKE, dc->desktop->doc(), item); + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + static_cast<LPEPowerStroke*>(lpe)->offset_points.param_set_and_write_new_value(points); + + // write powerstroke parameters: + lpe->getRepr()->setAttribute("start_linecap_type", "zerowidth"); + lpe->getRepr()->setAttribute("end_linecap_type", "zerowidth"); + lpe->getRepr()->setAttribute("cusp_linecap_type", "round"); + lpe->getRepr()->setAttribute("sort_points", "true"); + lpe->getRepr()->setAttribute("interpolator_type", "CubicBezierJohan"); + lpe->getRepr()->setAttribute("interpolator_beta", "0.2"); +} + +static void spdc_check_for_and_apply_waiting_LPE(FreehandBase *dc, SPItem *item, SPCurve *curve) +{ + using namespace Inkscape::LivePathEffect; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (item && SP_IS_LPE_ITEM(item)) { + if (prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 1) { + Effect::createAndApply(SPIRO, dc->desktop->doc(), item); + } + + int shape = prefs->getInt(tool_name(dc) + "/shape", 0); + bool shape_applied = false; + SPCSSAttr *css_item = sp_css_attr_from_object(item, SP_STYLE_FLAG_ALWAYS); + const char *cstroke = sp_repr_css_property(css_item, "stroke", "none"); + +#define SHAPE_LENGTH 10 +#define SHAPE_HEIGHT 10 + + switch (shape) { + case 0: + // don't apply any shape + break; + case 1: + { + // "triangle in" + std::vector<Geom::Point> points(1); + points[0] = Geom::Point(0., SHAPE_HEIGHT/2); + spdc_apply_powerstroke_shape(points, dc, item); + + shape_applied = true; + break; + } + case 2: + { + // "triangle out" + guint curve_length = curve->get_segment_count(); + std::vector<Geom::Point> points(1); + points[0] = Geom::Point((double)curve_length, SHAPE_HEIGHT/2); + spdc_apply_powerstroke_shape(points, dc, item); + + shape_applied = true; + break; + } + case 3: + { + // "ellipse" + SPCurve *c = new SPCurve(); + const double C1 = 0.552; + c->moveto(0, SHAPE_HEIGHT/2); + c->curveto(0, (1 - C1) * SHAPE_HEIGHT/2, (1 - C1) * SHAPE_LENGTH/2, 0, SHAPE_LENGTH/2, 0); + c->curveto((1 + C1) * SHAPE_LENGTH/2, 0, SHAPE_LENGTH, (1 - C1) * SHAPE_HEIGHT/2, SHAPE_LENGTH, SHAPE_HEIGHT/2); + c->curveto(SHAPE_LENGTH, (1 + C1) * SHAPE_HEIGHT/2, (1 + C1) * SHAPE_LENGTH/2, SHAPE_HEIGHT, SHAPE_LENGTH/2, SHAPE_HEIGHT); + c->curveto((1 - C1) * SHAPE_LENGTH/2, SHAPE_HEIGHT, 0, (1 + C1) * SHAPE_HEIGHT/2, 0, SHAPE_HEIGHT/2); + c->closepath(); + spdc_paste_curve_as_freehand_shape(c, dc, item); + c->unref(); + shape_applied = true; + break; + } + case 4: + { + // take shape from clipboard; TODO: catch the case where clipboard is empty + Effect::createAndApply(PATTERN_ALONG_PATH, dc->desktop->doc(), item); + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + static_cast<LPEPatternAlongPath*>(lpe)->pattern.on_paste_button_click(); + + shape_applied = true; + break; + } + default: + break; + } + if (shape_applied) { + // apply original stroke color as fill and unset stroke; then return + SPCSSAttr *css = sp_repr_css_attr_new(); + + if (!strcmp(cstroke, "none")){ + sp_repr_css_set_property (css, "fill", "black"); + } else { + sp_repr_css_set_property (css, "fill", cstroke); + } + sp_repr_css_set_property (css, "stroke", "none"); + sp_desktop_apply_css_recursive(item, css, true); + sp_repr_css_attr_unref(css); + return; + } + + if (dc->waiting_LPE_type != INVALID_LPE) { + Effect::createAndApply(dc->waiting_LPE_type, dc->desktop->doc(), 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_set_polyline_mode(SP_PEN_CONTEXT(dc)); + } + } +} + +/* + * Selection handlers + */ + +static void spdc_selection_changed(Inkscape::Selection *sel, FreehandBase *dc) +{ + if (dc->attach) { + spdc_attach_selection(dc, sel); + } +} + +/* fixme: We have to ensure this is not delayed (Lauris) */ + +static void spdc_selection_modified(Inkscape::Selection *sel, guint /*flags*/, FreehandBase *dc) +{ + if (dc->attach) { + spdc_attach_selection(dc, sel); + } +} + +static void spdc_attach_selection(FreehandBase *dc, Inkscape::Selection */*sel*/) +{ + // We reset white and forget white/start/end anchors + spdc_reset_white(dc); + dc->sa = NULL; + dc->ea = NULL; + + SPItem *item = dc->selection ? dc->selection->singleItem() : NULL; + + if ( item && SP_IS_PATH(item) ) { + // Create new white data + // Item + dc->white_item = item; + + // Curve list + // We keep it in desktop coordinates to eliminate calculation errors + SPCurve *norm = SP_PATH(item)->get_curve_for_edit(); + norm->transform((dc->white_item)->i2dt_affine()); + g_return_if_fail( norm != NULL ); + dc->white_curves = g_slist_reverse(norm->split()); + norm->unref(); + + // Anchor list + for (GSList *l = dc->white_curves; l != NULL; l = l->next) { + SPCurve *c; + c = static_cast<SPCurve*>(l->data); + g_return_if_fail( c->get_segment_count() > 0 ); + if ( !c->is_closed() ) { + SPDrawAnchor *a; + a = sp_draw_anchor_new(dc, c, TRUE, *(c->first_point())); + if (a) + dc->white_anchors = g_slist_prepend(dc->white_anchors, a); + a = sp_draw_anchor_new(dc, c, FALSE, *(c->last_point())); + if (a) + dc->white_anchors = g_slist_prepend(dc->white_anchors, a); + } + } + // fixme: recalculate active anchor? + } +} + + +void spdc_endpoint_snap_rotation(ToolBase const *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 = SP_EVENT_CONTEXT_DESKTOP(ec)->namedview->snap_manager; + m.setup(SP_EVENT_CONTEXT_DESKTOP(ec)); + + 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), boost::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 * const ec, Geom::Point& p, boost::optional<Geom::Point> &start_of_line, guint const /*state*/) +{ + SPDesktop *dt = SP_EVENT_CONTEXT_DESKTOP(ec); + SnapManager &m = dt->namedview->snap_manager; + Inkscape::Selection *selection = sp_desktop_selection (dt); + + // 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(); +} + +static SPCurve *reverse_then_unref(SPCurve *orig) +{ + SPCurve *ret = orig->create_reverse(); + orig->unref(); + return ret; +} + +void spdc_concat_colors_and_flush(FreehandBase *dc, gboolean forceclosed) +{ + // Concat RBG + SPCurve *c = dc->green_curve; + + // Green + dc->green_curve = new SPCurve(); + while (dc->green_bpaths) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(dc->green_bpaths->data)); + dc->green_bpaths = g_slist_remove(dc->green_bpaths, dc->green_bpaths->data); + } + + // Blue + c->append_continuous(dc->blue_curve, 0.0625); + dc->blue_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(dc->blue_bpath), NULL); + + // Red + if (dc->red_curve_is_valid) { + c->append_continuous(dc->red_curve, 0.0625); + } + dc->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(dc->red_bpath), NULL); + + if (c->is_empty()) { + c->unref(); + return; + } + + // Step A - test, whether we ended on green anchor + if ( forceclosed || ( dc->green_anchor && dc->green_anchor->active ) ) { + // We hit green anchor, closing Green-Blue-Red + SP_EVENT_CONTEXT_DESKTOP(dc)->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed.")); + c->closepath_current(); + // Closed path, just flush + spdc_flush_white(dc, c); + c->unref(); + 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 + SP_EVENT_CONTEXT_DESKTOP(dc)->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Closing path.")); + if (dc->sa->start && !(dc->sa->curve->is_closed()) ) { + c = reverse_then_unref(c); + } + dc->sa->curve->append_continuous(c, 0.0625); + c->unref(); + dc->sa->curve->closepath_current(); + spdc_flush_white(dc, NULL); + return; + } + + // Step C - test start + if (dc->sa) { + SPCurve *s = dc->sa->curve; + dc->white_curves = g_slist_remove(dc->white_curves, s); + if (dc->sa->start) { + s = reverse_then_unref(s); + } + s->append_continuous(c, 0.0625); + c->unref(); + c = s; + } else /* Step D - test end */ if (dc->ea) { + SPCurve *e = dc->ea->curve; + dc->white_curves = g_slist_remove(dc->white_curves, e); + if (!dc->ea->start) { + e = reverse_then_unref(e); + } + c->append_continuous(e, 0.0625); + e->unref(); + } + + + spdc_flush_white(dc, c); + + c->unref(); +} + +static void spdc_flush_white(FreehandBase *dc, SPCurve *gc) +{ + SPCurve *c; + + if (dc->white_curves) { + g_assert(dc->white_item); + c = SPCurve::concat(dc->white_curves); + g_slist_free(dc->white_curves); + dc->white_curves = NULL; + if (gc) { + c->append(gc, FALSE); + } + } else if (gc) { + c = gc; + c->ref(); + } else { + return; + } + + // Now we have to go back to item coordinates at last + c->transform( dc->white_item + ? (dc->white_item)->dt2i_affine() + : SP_EVENT_CONTEXT_DESKTOP(dc)->dt2doc() ); + + SPDesktop *desktop = SP_EVENT_CONTEXT_DESKTOP(dc); + SPDocument *doc = sp_desktop_document(desktop); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + if ( c && !c->is_empty() ) { + // We actually have something to write + + bool has_lpe = false; + Inkscape::XML::Node *repr; + if (dc->white_item) { + repr = dc->white_item->getRepr(); + has_lpe = SP_LPE_ITEM(dc->white_item)->hasPathEffectRecursive(); + } else { + repr = xml_doc->createElement("svg:path"); + // Set style + sp_desktop_apply_style_tool(desktop, repr, tool_name(dc).data(), false); + } + + gchar *str = sp_svg_write_path( c->get_pathvector() ); + g_assert( str != NULL ); + if (has_lpe) + repr->setAttribute("inkscape:original-d", str); + else + repr->setAttribute("d", str); + g_free(str); + + if (!dc->white_item) { + // Attach repr + SPItem *item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr)); + + // we finished the path; now apply any waiting LPEs or freehand shapes + spdc_check_for_and_apply_waiting_LPE(dc, item, c); + + dc->selection->set(repr); + Inkscape::GC::release(repr); + item->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + item->doWriteTransform(item->getRepr(), item->transform, NULL, true); + item->updateRepr(); + } + + DocumentUndo::done(doc, SP_IS_PEN_CONTEXT(dc)? SP_VERB_CONTEXT_PEN : SP_VERB_CONTEXT_PENCIL, + _("Draw path")); + + // When quickly drawing several subpaths with Shift, the next subpath may be finished and + // flushed before the selection_modified signal is fired by the previous change, which + // results in the tool losing all of the selected path's curve except that last subpath. To + // fix this, we force the selection_modified callback now, to make sure the tool's curve is + // in sync immediately. + spdc_selection_modified(sp_desktop_selection(desktop), 0, dc); + } + + c->unref(); + + // Flush pending updates + doc->ensureUpToDate(); +} + +SPDrawAnchor *spdc_test_inside(FreehandBase *dc, Geom::Point p) +{ + SPDrawAnchor *active = NULL; + + // Test green anchor + if (dc->green_anchor) { + active = sp_draw_anchor_test(dc->green_anchor, p, TRUE); + } + + for (GSList *l = dc->white_anchors; l != NULL; l = l->next) { + SPDrawAnchor *na = sp_draw_anchor_test(static_cast<SPDrawAnchor*>(l->data), p, !active); + if ( !active && na ) { + active = na; + } + } + + return active; +} + +static void spdc_reset_white(FreehandBase *dc) +{ + if (dc->white_item) { + // We do not hold refcount + dc->white_item = NULL; + } + while (dc->white_curves) { + reinterpret_cast<SPCurve *>(dc->white_curves->data)->unref(); + dc->white_curves = g_slist_remove(dc->white_curves, dc->white_curves->data); + } + while (dc->white_anchors) { + sp_draw_anchor_destroy(static_cast<SPDrawAnchor*>(dc->white_anchors->data)); + dc->white_anchors = g_slist_remove(dc->white_anchors, dc->white_anchors->data); + } +} + +static void spdc_free_colors(FreehandBase *dc) +{ + // Red + if (dc->red_bpath) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(dc->red_bpath)); + dc->red_bpath = NULL; + } + if (dc->red_curve) { + dc->red_curve = dc->red_curve->unref(); + } + + // Blue + if (dc->blue_bpath) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(dc->blue_bpath)); + dc->blue_bpath = NULL; + } + if (dc->blue_curve) { + dc->blue_curve = dc->blue_curve->unref(); + } + + // Green + while (dc->green_bpaths) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(dc->green_bpaths->data)); + dc->green_bpaths = g_slist_remove(dc->green_bpaths, dc->green_bpaths->data); + } + if (dc->green_curve) { + dc->green_curve = dc->green_curve->unref(); + } + if (dc->green_anchor) { + dc->green_anchor = sp_draw_anchor_destroy(dc->green_anchor); + } + + // White + if (dc->white_item) { + // We do not hold refcount + dc->white_item = NULL; + } + while (dc->white_curves) { + reinterpret_cast<SPCurve *>(dc->white_curves->data)->unref(); + dc->white_curves = g_slist_remove(dc->white_curves, dc->white_curves->data); + } + while (dc->white_anchors) { + sp_draw_anchor_destroy(static_cast<SPDrawAnchor *>(dc->white_anchors->data)); + dc->white_anchors = g_slist_remove(dc->white_anchors, dc->white_anchors->data); + } +} + +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")); + Glib::ustring tool_path = tool; + + SPDesktop *desktop = SP_EVENT_CONTEXT_DESKTOP(ec); + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "arc"); + SPItem *item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr)); + 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 = NULL; + style_str = repr->attribute("style"); + if (style_str) { + SPStyle *style = sp_style_new(SP_ACTIVE_DOCUMENT); + sp_style_merge_from_style_string(style, style_str); + stroke_width = style->stroke_width.computed; + style->stroke_width.computed = 0; + sp_style_unref(style); + } + + // unset stroke and set fill color to former stroke color + gchar * str; + str = g_strdup_printf("fill:#%06x;stroke:none;", 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 (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; + } + + sp_repr_set_svg_double (repr, "sodipodi:cx", pp[Geom::X]); + sp_repr_set_svg_double (repr, "sodipodi:cy", pp[Geom::Y]); + sp_repr_set_svg_double (repr, "sodipodi:rx", rad * stroke_width); + sp_repr_set_svg_double (repr, "sodipodi:ry", rad * stroke_width); + item->updateRepr(); + + sp_desktop_selection(desktop)->set(item); + + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating single dot")); + DocumentUndo::done(sp_desktop_document(desktop), SP_VERB_NONE, _("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 000000000..7e53684e3 --- /dev/null +++ b/src/ui/tools/freehand-base.h @@ -0,0 +1,144 @@ +#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 + */ + +#include <stddef.h> +#include <sigc++/sigc++.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" +#include "live_effects/effect.h" + +/* Freehand context */ + +#define SP_DRAW_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::FreehandBase*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_DRAW_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::FreehandBase*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +struct SPDrawAnchor; +namespace Inkscape +{ + class Selection; +} + +namespace Inkscape { +namespace UI { +namespace Tools { + +class FreehandBase : public ToolBase { +public: + FreehandBase(); + virtual ~FreehandBase(); + + Inkscape::Selection *selection; + SPCanvasItem *grab; + + guint attach : 1; + + guint32 red_color; + guint32 blue_color; + guint32 green_color; + + // Red + SPCanvasItem *red_bpath; + SPCurve *red_curve; + + // Blue + SPCanvasItem *blue_bpath; + SPCurve *blue_curve; + + // Green + GSList *green_bpaths; + SPCurve *green_curve; + SPDrawAnchor *green_anchor; + gboolean green_closed; // a flag meaning we hit the green anchor, so close the path on itself + + // White + SPItem *white_item; + GSList *white_curves; + GSList *white_anchors; + + // 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; + +protected: + virtual void setup(); + virtual void finish(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); +}; + +/** + * 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 *const ec, Geom::Point &p, Geom::Point const &o, guint state); + +void spdc_endpoint_snap_free(ToolBase const *ec, Geom::Point &p, boost::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 000000000..e4ab7b424 --- /dev/null +++ b/src/ui/tools/gradient-tool.cpp @@ -0,0 +1,977 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + + +#include <gdk/gdkkeysyms.h> + +#include "macros.h" +#include "document.h" +#include "selection.h" +#include "desktop.h" +#include "desktop-handles.h" +#include "message-context.h" +#include "message-stack.h" +#include "pixmaps/cursor-gradient.xpm" +#include "pixmaps/cursor-gradient-add.xpm" +#include "ui/tools/gradient-tool.h" +#include "gradient-chemistry.h" +#include <glibmm/i18n.h> +#include "preferences.h" +#include "gradient-drag.h" +#include "gradient-chemistry.h" +#include "xml/repr.h" +#include "sp-item.h" +#include "display/sp-ctrlline.h" +#include "sp-linear-gradient.h" +#include "sp-radial-gradient.h" +#include "sp-stop.h" +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" +#include "snap.h" +#include "sp-namedview.h" +#include "rubberband.h" +#include "document-undo.h" +#include "verbs.h" +#include "selection-chemistry.h" + +using Inkscape::DocumentUndo; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void sp_gradient_drag(GradientTool &rc, Geom::Point const pt, guint state, guint32 etime); + +namespace { + ToolBase* createGradientContext() { + return new GradientTool(); + } + + bool gradientContextRegistered = ToolFactory::instance().registerObject("/tools/gradient", createGradientContext); +} + +const std::string& GradientTool::getPrefsPath() { + return GradientTool::prefsPath; +} + +const std::string GradientTool::prefsPath = "/tools/gradient"; + + +GradientTool::GradientTool() : ToolBase() { + this->node_added = false; + this->subselcon = 0; + this->selcon = 0; + + this->cursor_addnode = false; + this->cursor_shape = cursor_gradient_xpm; + this->hot_x = 4; + this->hot_y = 4; + this->xp = 0; + this->yp = 0; + this->tolerance = 6; + this->within_tolerance = false; + this->item_to_select = NULL; +} + +GradientTool::~GradientTool() { + this->enableGrDrag(false); + + this->selcon->disconnect(); + delete this->selcon; + + this->subselcon->disconnect(); + delete this->subselcon; +} + +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>") +}; + +void GradientTool::selection_changed(Inkscape::Selection*) { + GradientTool *rc = (GradientTool *) this; + + GrDrag *drag = rc->_grdrag; + Inkscape::Selection *selection = sp_desktop_selection(SP_EVENT_CONTEXT(rc)->desktop); + if (selection == NULL) { + return; + } + guint n_obj = g_slist_length((GSList *) selection->itemList()); + + 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),NULL); + rc->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),NULL); + rc->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),NULL); + rc->message_context->setF(Inkscape::NORMAL_MESSAGE,message, n_sel, n_tot, n_obj); + } else if (n_sel == 0) { + rc->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::setup() { + ToolBase::setup(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/gradient/selcue", true)) { + this->enableSelectionCue(); + } + + this->enableGrDrag(); + Inkscape::Selection *selection = sp_desktop_selection(this->desktop); + + this->selcon = new sigc::connection(selection->connectChanged( + sigc::mem_fun(this, &GradientTool::selection_changed) + )); + + this->subselcon = new sigc::connection(this->desktop->connectToolSubselectionChanged( + sigc::hide(sigc::bind( + sigc::mem_fun(this, &GradientTool::selection_changed), + (Inkscape::Selection*)NULL + )) + )); + + this->selection_changed(selection); +} + +void +sp_gradient_context_select_next (ToolBase *event_context) +{ + GrDrag *drag = event_context->_grdrag; + g_assert (drag); + + GrDragger *d = drag->select_next(); + + event_context->desktop->scroll_to_point(d->point, 1.0); +} + +void +sp_gradient_context_select_prev (ToolBase *event_context) +{ + GrDrag *drag = event_context->_grdrag; + g_assert (drag); + + GrDragger *d = drag->select_prev(); + + event_context->desktop->scroll_to_point(d->point, 1.0); +} + +static bool +sp_gradient_context_is_over_line (GradientTool *rc, SPItem *item, Geom::Point event_p) +{ + SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop; + + //Translate mouse point into proper coord system + rc->mousepoint_doc = desktop->w2d(event_p); + + SPCtrlLine* line = SP_CTRLLINE(item); + + Geom::LineSegment ls(line->s, line->e); + Geom::Point nearest = ls.pointAt(ls.nearestPoint(rc->mousepoint_doc)); + double dist_screen = Geom::L2 (rc->mousepoint_doc - nearest) * desktop->current_zoom(); + + double tolerance = (double) SP_EVENT_CONTEXT(rc)->tolerance; + + bool close = (dist_screen < tolerance); + + return close; +} + +static std::vector<Geom::Point> +sp_gradient_context_get_stop_intervals (GrDrag *drag, GSList **these_stops, GSList **next_stops) +{ + std::vector<Geom::Point> coords; + + // for all selected draggers + for (GList *i = drag->selected; i != NULL; i = i->next) { + GrDragger *dragger = (GrDragger *) i->data; + // remember the coord of the dragger to reselect it later + coords.push_back(dragger->point); + // for all draggables of dragger + for (GSList const* j = dragger->draggables; j != NULL; j = j->next) { + GrDraggable *d = (GrDraggable *) j->data; + + // 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 = NULL; + // 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 (!g_slist_find(*these_stops, this_stop) && 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 = g_slist_prepend (*these_stops, this_stop); + *next_stops = g_slist_prepend (*next_stops, next_stop); + } + } + } + } + return coords; +} + +void +sp_gradient_context_add_stops_between_selected_stops (GradientTool *rc) +{ + SPDocument *doc = NULL; + GrDrag *drag = rc->_grdrag; + + GSList *these_stops = NULL; + GSList *next_stops = NULL; + + std::vector<Geom::Point> coords = sp_gradient_context_get_stop_intervals (drag, &these_stops, &next_stops); + + if (g_slist_length(these_stops) == 0 && drag->numSelected() == 1) { + // if a single stop is selected, add between that stop and the next one + GrDragger *dragger = (GrDragger *) drag->selected->data; + for (GSList const* j = dragger->draggables; j != NULL; j = j->next) { + GrDraggable *d = (GrDraggable *) j->data; + if (d->point_type == POINT_RG_FOCUS) { + /* + * There are 2 draggables at the center (start) of a radial gradient + * To avoid creating 2 seperate 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); + SPStop *next_stop = this_stop->getNextStop(); + if (this_stop && next_stop) { + these_stops = g_slist_prepend (these_stops, this_stop); + next_stops = g_slist_prepend (next_stops, next_stop); + } + } + } + + // now actually create the new stops + GSList *i = these_stops; + GSList *j = next_stops; + GSList *new_stops = NULL; + + for (; i != NULL && j != NULL; i = i->next, j = j->next) { + SPStop *this_stop = (SPStop *) i->data; + SPStop *next_stop = (SPStop *) j->data; + gfloat offset = 0.5*(this_stop->offset + next_stop->offset); + SPObject *parent = this_stop->parent; + if (SP_IS_GRADIENT (parent)) { + doc = parent->document; + SPStop *new_stop = sp_vector_add_stop (SP_GRADIENT (parent), this_stop, next_stop, offset); + new_stops = g_slist_prepend (new_stops, new_stop); + SP_GRADIENT(parent)->ensureVector(); + } + } + + if (g_slist_length(these_stops) > 0 && doc) { + DocumentUndo::done(doc, SP_VERB_CONTEXT_GRADIENT, _("Add gradient stop")); + 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 (GSList *s = new_stops; s != NULL; s = s->next) { + drag->selectByStop((SPStop *)s->data); + } + + } + + g_slist_free (these_stops); + g_slist_free (next_stops); + g_slist_free (new_stops); +} + +static double sqr(double x) {return x*x;} + +static void +sp_gradient_simplify(GradientTool *rc, double tolerance) +{ + SPDocument *doc = NULL; + GrDrag *drag = rc->_grdrag; + + GSList *these_stops = NULL; + GSList *next_stops = NULL; + + std::vector<Geom::Point> coords = sp_gradient_context_get_stop_intervals (drag, &these_stops, &next_stops); + + GSList *todel = NULL; + + GSList *i = these_stops; + GSList *j = next_stops; + for (; i != NULL && j != NULL; i = i->next, j = j->next) { + SPStop *stop0 = (SPStop *) i->data; + SPStop *stop1 = (SPStop *) j->data; + + gint i1 = g_slist_index(these_stops, stop1); + if (i1 != -1) { + GSList *next_next = g_slist_nth (next_stops, i1); + if (next_next) { + SPStop *stop2 = (SPStop *) next_next->data; + + if (g_slist_find(todel, stop0) || g_slist_find(todel, stop2)) + continue; + + 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 = g_slist_prepend (todel, stop1); + } + } + } + + for (i = todel; i != NULL; i = i->next) { + SPStop *stop = (SPStop*) i->data; + doc = stop->document; + Inkscape::XML::Node * parent = stop->getRepr()->parent(); + parent->removeChild( stop->getRepr() ); + } + + if (g_slist_length(todel) > 0) { + DocumentUndo::done(doc, SP_VERB_CONTEXT_GRADIENT, _("Simplify gradient")); + drag->local_change = true; + drag->updateDraggers(); + drag->selectByCoords(coords); + } + + g_slist_free (todel); + g_slist_free (these_stops); + g_slist_free (next_stops); +} + + +static void +sp_gradient_context_add_stop_near_point (GradientTool *rc, 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 + + ToolBase *ec = SP_EVENT_CONTEXT(rc); + SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop; + + double tolerance = (double) ec->tolerance; + + SPStop *newstop = ec->get_drag()->addStopNearPoint (item, mouse_p, tolerance/desktop->current_zoom()); + + DocumentUndo::done(sp_desktop_document (desktop), SP_VERB_CONTEXT_GRADIENT, + _("Add gradient stop")); + + ec->get_drag()->updateDraggers(); + ec->get_drag()->local_change = true; + ec->get_drag()->selectByStop(newstop); +} + +bool GradientTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = sp_desktop_selection (desktop); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + double const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px + + GrDrag *drag = this->_grdrag; + g_assert (drag); + + gint ret = FALSE; + + switch (event->type) { + case GDK_2BUTTON_PRESS: + if ( event->button.button == 1 ) { + bool over_line = false; + SPCtrlLine *line = NULL; + + if (drag->lines) { + for (GSList *l = drag->lines; (l != NULL) && (!over_line); l = l->next) { + line = (SPCtrlLine*) l->data; + over_line |= sp_gradient_context_is_over_line (this, (SPItem*) line, Geom::Point(event->motion.x, event->motion.y)); + } + } + + if (over_line) { + // we take the first item in selection, because with doubleclick, the first click + // always resets selection to the single object under cursor + sp_gradient_context_add_stop_near_point(this, SP_ITEM(selection->itemList()->data), this->mousepoint_doc, event->button.time); + } else { + for (GSList const* i = selection->itemList(); i != NULL; i = i->next) { + SPItem *item = SP_ITEM(i->data); + 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(sp_desktop_document(desktop), desktop, item, fsmode); + + SPGradient *priv = sp_item_set_gradient(item, vector, new_type, fsmode); + sp_gradient_reset_to_userspace(priv, item); + } + + DocumentUndo::done(sp_desktop_document (desktop), SP_VERB_CONTEXT_GRADIENT, + _("Create default gradient")); + } + ret = TRUE; + } + break; + + case GDK_BUTTON_PRESS: + if ( event->button.button == 1 && !this->space_panning ) { + Geom::Point button_w(event->button.x, event->button.y); + + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + dragging = true; + + Geom::Point button_dt = desktop->w2d(button_w); + if (event->button.state & GDK_SHIFT_MASK) { + Inkscape::Rubberband::get(desktop)->start(desktop, button_dt); + } else { + // remember clicked item, disregarding groups, honoring Alt; do nothing with Crtl to + // enable Ctrl+doubleclick of exactly the selected item(s) + if (!(event->button.state & GDK_CONTROL_MASK)) { + this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + } + + if (!selection->isEmpty()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + } + + this->origin = button_dt; + } + + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning) { + 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 = this->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 { + sp_gradient_drag(*this, 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 = this->desktop->w2d(motion_w); + + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + + bool over_line = false; + + if (drag->lines) { + for (GSList *l = drag->lines; l != NULL; l = l->next) { + over_line |= sp_gradient_context_is_over_line (this, (SPItem*) l->data, Geom::Point(event->motion.x, event->motion.y)); + } + } + + if (this->cursor_addnode && !over_line) { + this->cursor_shape = cursor_gradient_xpm; + this->sp_event_context_update_cursor(); + this->cursor_addnode = false; + } else if (!this->cursor_addnode && over_line) { + this->cursor_shape = cursor_gradient_add_xpm; + this->sp_event_context_update_cursor(); + this->cursor_addnode = true; + } + } + break; + + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + + if ( event->button.button == 1 && !this->space_panning ) { + bool over_line = false; + SPCtrlLine *line = NULL; + + if (drag->lines) { + for (GSList *l = drag->lines; (l != NULL) && (!over_line); l = l->next) { + line = (SPCtrlLine*) l->data; + over_line = sp_gradient_context_is_over_line (this, (SPItem*) line, Geom::Point(event->motion.x, event->motion.y)); + if (over_line) + break; + } + } + + if ( (event->button.state & GDK_CONTROL_MASK) && (event->button.state & GDK_MOD1_MASK ) ) { + if (over_line && line) { + sp_gradient_context_add_stop_near_point(this, line->item, this->mousepoint_doc, 0); + ret = TRUE; + } + } else { + dragging = false; + + // unless clicked with Ctrl (to enable Ctrl+doubleclick). + if (event->button.state & GDK_CONTROL_MASK) { + ret = TRUE; + break; + } + + if (!this->within_tolerance) { + // we've been dragging, either do nothing (grdrag handles that), + // or rubberband-select if we have rubberband + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + + if (r->is_started() && !this->within_tolerance) { + // this was a rubberband drag + if (r->getMode() == RUBBERBAND_MODE_RECT) { + Geom::OptRect const b = r->getRectangle(); + drag->selectRect(*b); + } + } + } else if (this->item_to_select) { + if (over_line && line) { + // Clicked on an existing gradient line, dont 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) { + drag->deselectAll(); + } else { + selection->clear(); + } + } + + this->item_to_select = NULL; + ret = TRUE; + } + + Inkscape::Rubberband::get(desktop)->stop(); + } + break; + + case GDK_KEY_PRESS: + switch (get_group0_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"), + NULL); + 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()) { + sp_gradient_simplify(this, 1e-4); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (drag->selected) { + drag->deselectAll(); + } else { + Inkscape::SelectionHelper::selectNone(desktop); + } + ret = TRUE; + //TODO: make dragging escapable by Esc + break; + + case GDK_KEY_Left: // move handle left + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events( + get_group0_keyval(&event->key), 0); // with any mask + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + drag->selected_move_screen(mul*-10, 0); // shift + } else { + drag->selected_move_screen(mul*-1, 0); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + drag->selected_move(mul*-10*nudge, 0); // shift + } else { + drag->selected_move(mul*-nudge, 0); // no shift + } + } + ret = TRUE; + } + break; + + case GDK_KEY_Up: // move handle up + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events( + get_group0_keyval(&event->key), 0); // with any mask + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + drag->selected_move_screen(0, mul*10); // shift + } else { + drag->selected_move_screen(0, mul*1); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + drag->selected_move(0, mul*10*nudge); // shift + } else { + drag->selected_move(0, mul*nudge); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Right: // move handle right + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events( + get_group0_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + drag->selected_move_screen(mul*10, 0); // shift + } else { + drag->selected_move_screen(mul*1, 0); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + drag->selected_move(mul*10*nudge, 0); // shift + } else { + drag->selected_move(mul*nudge, 0); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Down: // move handle down + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events( + get_group0_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + drag->selected_move_screen(0, mul*-10); // shift + } else { + drag->selected_move_screen(0, mul*-1); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + drag->selected_move(0, mul*-10*nudge); // shift + } else { + drag->selected_move(0, mul*-nudge); // no shift + } + } + + ret = TRUE; + } + 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: + sp_gradient_context_add_stops_between_selected_stops (this); + ret = TRUE; + 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_group0_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; +} + +static void sp_gradient_drag(GradientTool &rc, Geom::Point const pt, guint /*state*/, guint32 etime) +{ + SPDesktop *desktop = SP_EVENT_CONTEXT(&rc)->desktop; + Inkscape::Selection *selection = sp_desktop_selection(desktop); + SPDocument *document = sp_desktop_document(desktop); + ToolBase *ec = SP_EVENT_CONTEXT(&rc); + + 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 (ec->item_to_select) { + // pick color from the object where drag started + vector = sp_gradient_vector_for_object(document, desktop, ec->item_to_select, fill_or_stroke); + } else { + // Starting from empty space: + // Sort items so that the topmost comes last + GSList *items = g_slist_copy ((GSList *) selection->itemList()); + items = g_slist_sort(items, (GCompareFunc) sp_item_repr_compare_position); + // take topmost + vector = sp_gradient_vector_for_object(document, desktop, SP_ITEM(g_slist_last(items)->data), fill_or_stroke); + g_slist_free (items); + } + + // 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"); + + for (GSList const *i = selection->itemList(); i != NULL; i = i->next) { + + //FIXME: see above + sp_repr_css_change_recursive(SP_OBJECT(i->data)->getRepr(), css, "style"); + + sp_item_set_gradient(SP_ITEM(i->data), vector, (SPGradientType) type, fill_or_stroke); + + if (type == SP_GRADIENT_TYPE_LINEAR) { + sp_item_gradient_set_coords (SP_ITEM(i->data), POINT_LG_BEGIN, 0, rc.origin, fill_or_stroke, true, false); + sp_item_gradient_set_coords (SP_ITEM(i->data), POINT_LG_END, 0, pt, fill_or_stroke, true, false); + } else if (type == SP_GRADIENT_TYPE_RADIAL) { + sp_item_gradient_set_coords (SP_ITEM(i->data), POINT_RG_CENTER, 0, rc.origin, fill_or_stroke, true, false); + sp_item_gradient_set_coords (SP_ITEM(i->data), POINT_RG_R1, 0, pt, fill_or_stroke, true, false); + } + SP_OBJECT(i->data)->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + if (ec->_grdrag) { + ec->_grdrag->updateDraggers(); + // prevent regenerating draggers by selection modified signal, which sometimes + // comes too late and thus destroys the knot which we will now grab: + ec->_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 + ec->_grdrag->grabKnot (SP_ITEM(selection->itemList()->data), + 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 = g_slist_length((GSList *) selection->itemList()); + rc.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 { + sp_desktop_message_stack(desktop)->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 000000000..6fe3bca9f --- /dev/null +++ b/src/ui/tools/gradient-tool.h @@ -0,0 +1,76 @@ +#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 + */ + +#include <stddef.h> +#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) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class GradientTool : public ToolBase { +public: + GradientTool(); + virtual ~GradientTool(); + + Geom::Point origin; + + bool cursor_addnode; + + bool node_added; + + Geom::Point mousepoint_doc; // stores mousepoint when over_line in doc coords + + sigc::connection *selcon; + sigc::connection *subselcon; + + static const std::string prefsPath; + + virtual void setup(); + virtual bool root_handler(GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +private: + void selection_changed(Inkscape::Selection*); +}; + +void sp_gradient_context_select_next (ToolBase *event_context); +void sp_gradient_context_select_prev (ToolBase *event_context); +void sp_gradient_context_add_stops_between_selected_stops (GradientTool *rc); + +} +} +} + +#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 000000000..a5406f1c5 --- /dev/null +++ b/src/ui/tools/lpe-tool.cpp @@ -0,0 +1,503 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <2geom/sbasis-geometric.h> +#include <gdk/gdkkeysyms.h> + +#include <glibmm/i18n.h> +#include "macros.h" +#include "pixmaps/cursor-crosshairs.xpm" +#include <gtk/gtk.h> +#include "desktop.h" +#include "message-context.h" +#include "preferences.h" +#include "shape-editor.h" +#include "selection.h" +#include "desktop-handles.h" +#include "document.h" +#include "display/curve.h" +#include "display/canvas-bpath.h" +#include "display/canvas-text.h" +#include "message-stack.h" +#include "sp-path.h" +#include "util/units.h" + +#include "ui/tools/lpe-tool.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"} +}; + + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +void sp_lpetool_context_selection_changed(Inkscape::Selection *selection, gpointer data); + +namespace { + ToolBase* createLPEToolContext() { + return new LpeTool(); + } + + bool lpetoolContextRegistered = ToolFactory::instance().registerObject("/tools/lpetool", createLPEToolContext); +} + +const std::string& LpeTool::getPrefsPath() { + return LpeTool::prefsPath; +} + +const std::string LpeTool::prefsPath = "/tools/lpetool"; + +LpeTool::LpeTool() : PenTool() { + this->mode = Inkscape::LivePathEffect::BEND_PATH; + this->shape_editor = 0; + + this->cursor_shape = cursor_crosshairs_xpm; + this->hot_x = 7; + this->hot_y = 7; + + this->canvas_bbox = NULL; + this->measuring_items = new std::map<SPPath *, SPCanvasItem*>; +} + +LpeTool::~LpeTool() { + delete this->shape_editor; + this->shape_editor = NULL; + + if (this->canvas_bbox) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(this->canvas_bbox)); + this->canvas_bbox = NULL; + } + + lpetool_delete_measuring_items(this); + delete this->measuring_items; + this->measuring_items = NULL; + + this->sel_changed_connection.disconnect(); +} + +void LpeTool::setup() { + PenTool::setup(); + + Inkscape::Selection *selection = sp_desktop_selection (this->desktop); + SPItem *item = selection->singleItem(); + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = + selection->connectChanged(sigc::bind(sigc::ptr_fun(&sp_lpetool_context_selection_changed), (gpointer)this)); + + this->shape_editor = new ShapeEditor(this->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, SH_NODEPATH); + this->shape_editor->set_item(item, SH_KNOTHOLDER); + } + + if (prefs->getBool("/tools/lpetool/selcue")) { + this->enableSelectionCue(); + } +} + +/** + * 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(SH_KNOTHOLDER); + SPItem *item = selection->singleItem(); + lc->shape_editor->set_item(item, SH_KNOTHOLDER); +} + +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 = sp_desktop_selection(this->desktop); + 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 = sp_desktop_selection (desktop); + + bool ret = false; + + if (sp_pen_context_has_waiting_LPE(this)) { + // 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 && !this->space_panning) { + 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); + + sp_pen_context_wait_for_LPE_mouse_clicks(this, 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_group0_keyval (&event->key)) { + } + break; + **/ + + case GDK_KEY_RELEASE: + /** + switch (get_group0_keyval(&event->key)) { + case GDK_Control_L: + case GDK_Control_R: + dc->_message_context->clear(); + break; + default: + break; + } + **/ + + default: + break; + } + + if (!ret) { + ret = PenTool::root_handler(event); + } + + return ret; +} + +/* + * Finds the index in the list of geometric subtools corresponding to the given LPE type. + * Returns -1 if no subtool is found. + */ +int +lpetool_mode_to_index(Inkscape::LivePathEffect::EffectType const type) { + for (int i = 0; i < num_subtools; ++i) { + if (lpesubtools[i].type == type) { + return i; + } + } + return -1; +} + +/* + * Checks whether an item has a construction applied as LPE and if so returns the index in + * lpesubtools of this construction + */ +int lpetool_item_has_construction(LpeTool */*lc*/, SPItem *item) +{ + if (!SP_IS_LPE_ITEM(item)) { + return -1; + } + + Inkscape::LivePathEffect::Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + if (!lpe) { + return -1; + } + return lpetool_mode_to_index(lpe->effectType()); +} + +/* + * Attempts to perform the construction of the given type (i.e., to apply the corresponding LPE) to + * a single selected item. Returns whether we succeeded. + */ +bool +lpetool_try_construction(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type) +{ + Inkscape::Selection *selection = sp_desktop_selection(lc->desktop); + SPItem *item = selection->singleItem(); + + // TODO: should we check whether type represents a valid geometric construction? + if (item && SP_IS_LPE_ITEM(item) && Inkscape::LivePathEffect::Effect::acceptsNumClicks(type) == 0) { + Inkscape::LivePathEffect::Effect::createAndApply(type, sp_desktop_document(lc->desktop), 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; + lc->desktop->setToolboxSelectOneValue ("lpetool_mode_action", index); + } else { + g_warning ("Invalid mode selected: %d", type); + return; + } +} + +void +lpetool_get_limiting_bbox_corners(SPDocument *document, Geom::Point &A, Geom::Point &B) { + Geom::Coord w = document->getWidth().value("px"); + Geom::Coord h = document->getHeight().value("px"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + double ulx = prefs->getDouble("/tools/lpetool/bbox_upperleftx", 0); + double uly = prefs->getDouble("/tools/lpetool/bbox_upperlefty", 0); + double lrx = prefs->getDouble("/tools/lpetool/bbox_lowerrightx", w); + double lry = prefs->getDouble("/tools/lpetool/bbox_lowerrighty", h); + + A = Geom::Point(ulx, uly); + B = Geom::Point(lrx, lry); +} + +/* + * Reads the limiting bounding box from preferences and draws it on the screen + */ +// TODO: Note that currently the bbox is not user-settable; we simply use the page borders +void +lpetool_context_reset_limiting_bbox(LpeTool *lc) +{ + if (lc->canvas_bbox) { + sp_canvas_item_destroy(lc->canvas_bbox); + lc->canvas_bbox = NULL; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs->getBool("/tools/lpetool/show_bbox", true)) + return; + + SPDocument *document = sp_desktop_document(lc->desktop); + + Geom::Point A, B; + lpetool_get_limiting_bbox_corners(document, A, B); + Geom::Affine doc2dt(lc->desktop->doc2dt()); + A *= doc2dt; + B *= doc2dt; + + Geom::Rect rect(A, B); + SPCurve *curve = SPCurve::new_from_rect(rect); + + lc->canvas_bbox = sp_canvas_bpath_new (sp_desktop_controls(lc->desktop), curve); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(lc->canvas_bbox), 0x0000ffff, 0.8, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT, 5, 5); +} + +static void +set_pos_and_anchor(SPCanvasText *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)); + + sp_canvastext_set_coords(canvas_text, pos + n * length); + sp_canvastext_set_anchor_manually(canvas_text, std::sin(angle), -std::cos(angle)); +} + +void +lpetool_create_measuring_items(LpeTool *lc, Inkscape::Selection *selection) +{ + if (!selection) { + selection = sp_desktop_selection(lc->desktop); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show = prefs->getBool("/tools/lpetool/show_measuring_info", true); + + SPPath *path; + SPCurve *curve; + SPCanvasText *canvas_text; + SPCanvasGroup *tmpgrp = sp_desktop_tempgroup(lc->desktop); + gchar *arc_length; + double lengthval; + + for (GSList const *i = selection->itemList(); i != NULL; i = i->next) { + if (SP_IS_PATH(i->data)) { + path = SP_PATH(i->data); + curve = path->getCurve(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = paths_to_pw(curve->get_pathvector()); + canvas_text = (SPCanvasText *) sp_canvastext_new(tmpgrp, lc->desktop, Geom::Point(0,0), ""); + if (!show) + sp_canvas_item_hide(SP_CANVAS_ITEM(canvas_text)); + + Inkscape::Util::Unit const * unit = NULL; + if (prefs->getString("/tools/lpetool/unit").compare("")) { + unit = unit_table.getUnit(prefs->getString("/tools/lpetool/unit")); + } else { + unit = unit_table.getUnit("px"); + } + + lengthval = Geom::length(pwd2); + lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit); + arc_length = g_strdup_printf("%.2f %s", lengthval, unit->abbr.c_str()); + sp_canvastext_set_text (canvas_text, arc_length); + set_pos_and_anchor(canvas_text, pwd2, 0.5, 10); + // TODO: must we free arc_length? + (*lc->measuring_items)[path] = SP_CANVAS_ITEM(canvas_text); + } + } +} + +void +lpetool_delete_measuring_items(LpeTool *lc) +{ + std::map<SPPath *, SPCanvasItem*>::iterator i; + for (i = lc->measuring_items->begin(); i != lc->measuring_items->end(); ++i) { + sp_canvas_item_destroy(i->second); + } + lc->measuring_items->clear(); +} + +void +lpetool_update_measuring_items(LpeTool *lc) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + for ( std::map<SPPath *, SPCanvasItem*>::iterator i = lc->measuring_items->begin(); + i != lc->measuring_items->end(); + ++i ) + { + SPPath *path = i->first; + SPCurve *curve = path->getCurve(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = Geom::paths_to_pw(curve->get_pathvector()); + Inkscape::Util::Unit const * unit = NULL; + if (prefs->getString("/tools/lpetool/unit").compare("")) { + unit = unit_table.getUnit(prefs->getString("/tools/lpetool/unit")); + } else { + unit = unit_table.getUnit("px"); + } + double lengthval = Geom::length(pwd2); + lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit); + gchar *arc_length = g_strdup_printf("%.2f %s", lengthval, unit->abbr.c_str()); + sp_canvastext_set_text (SP_CANVASTEXT(i->second), arc_length); + set_pos_and_anchor(SP_CANVASTEXT(i->second), pwd2, 0.5, 10); + // TODO: must we free arc_length? + } +} + +void +lpetool_show_measuring_info(LpeTool *lc, bool show) +{ + std::map<SPPath *, SPCanvasItem*>::iterator i; + for (i = lc->measuring_items->begin(); i != lc->measuring_items->end(); ++i) { + if (show) { + sp_canvas_item_show(i->second); + } else { + sp_canvas_item_hide(i->second); + } + } +} + +} +} +} + +/* + 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 000000000..df78c205c --- /dev/null +++ b/src/ui/tools/lpe-tool.h @@ -0,0 +1,99 @@ +#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, 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 { +namespace UI { +namespace Tools { + +class LpeTool : public PenTool { +public: + LpeTool(); + virtual ~LpeTool(); + + ShapeEditor* shape_editor; + SPCanvasItem *canvas_bbox; + Inkscape::LivePathEffect::EffectType mode; + + std::map<SPPath *, SPCanvasItem*> *measuring_items; + + sigc::connection sel_changed_connection; + sigc::connection sel_modified_connection; + + static const std::string prefsPath; + + virtual const std::string& getPrefsPath(); + +protected: + virtual void setup(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); + virtual bool item_handler(SPItem* item, GdkEvent* event); +}; + +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 = NULL); +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/measure-tool.cpp b/src/ui/tools/measure-tool.cpp new file mode 100644 index 000000000..0d823dfda --- /dev/null +++ b/src/ui/tools/measure-tool.cpp @@ -0,0 +1,777 @@ +/* + * Our nice measuring tool + * + * Authors: + * Felipe Correa da Silva Sanches <juca@members.fsf.org> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2011 Authors + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + + +#include <gdk/gdkkeysyms.h> +#include <boost/none_t.hpp> +#include "util/units.h" +#include "macros.h" +#include "display/curve.h" +#include "sp-shape.h" +#include "sp-text.h" +#include "sp-flowtext.h" +#include "text-editing.h" +#include "display/sp-ctrlline.h" +#include "display/sodipodi-ctrl.h" +#include "display/sp-canvas-item.h" +#include "display/sp-canvas-util.h" +#include "desktop.h" +#include "document.h" +#include "pixmaps/cursor-measure.xpm" +#include "preferences.h" +#include "inkscape.h" +#include "desktop-handles.h" +#include "ui/tools/measure-tool.h" +#include "ui/tools/freehand-base.h" +#include "display/canvas-text.h" +#include "path-chemistry.h" +#include "2geom/line.h" +#include <2geom/path-intersection.h> +#include <2geom/pathvector.h> +#include <2geom/crossing.h> +#include <2geom/angle.h> +#include "snap.h" +#include "sp-namedview.h" +#include "enums.h" +#include "ui/control-manager.h" + +using Inkscape::ControlManager; +using Inkscape::CTLINE_SECONDARY; +using Inkscape::Util::unit_table; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +std::vector<Inkscape::Display::TemporaryItem*> measure_tmp_items; + +namespace { + ToolBase* createMeasureContext() { + return new MeasureTool(); + } + + bool measureContextRegistered = ToolFactory::instance().registerObject("/tools/measure", createMeasureContext); +} + +const std::string& MeasureTool::getPrefsPath() { + return MeasureTool::prefsPath; +} + +const std::string MeasureTool::prefsPath = "/tools/measure"; + +namespace +{ + +gint const DIMENSION_OFFSET = 35; + +/** + * 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]; + } +} + +void repositionOverlappingLabels(std::vector<LabelPlacement> &placements, SPDesktop *desktop, Geom::Point const &normal, double fontsize) +{ + std::sort(placements.begin(), placements.end(), SortLabelPlacement); + + double border = 3; + Geom::Rect box; + { + Geom::Point tmp(fontsize * 8 + (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 visibleArea = desktop->get_display_area(); + // Bring it in to "title safe" for the anchor point + Geom::Point textBox = desktop->w2d(Geom::Point(fontsize * 3, fontsize / 2)); + textBox[Geom::Y] = std::abs(textBox[Geom::Y]); + + visibleArea = Geom::Rect(visibleArea.min()[Geom::X] + textBox[Geom::X], + visibleArea.min()[Geom::Y] + textBox[Geom::Y], + visibleArea.max()[Geom::X] - textBox[Geom::X], + visibleArea.max()[Geom::Y] - textBox[Geom::Y]); + + where[Geom::X] = std::min(where[Geom::X], visibleArea.max()[Geom::X]); + where[Geom::X] = std::max(where[Geom::X], visibleArea.min()[Geom::X]); + where[Geom::Y] = std::min(where[Geom::Y], visibleArea.max()[Geom::Y]); + where[Geom::Y] = std::max(where[Geom::Y], visibleArea.min()[Geom::Y]); + + return where; +} + +/** + * 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. + */ +void createAngleDisplayCurve(SPDesktop *desktop, Geom::Point const ¢er, Geom::Point const &end, Geom::Point const &anchor, double angle) +{ + // 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 = (4.0 / 3.0) * (std::sqrt(2 * q1 * q2) - q2) / ((ax * by) - (ay * bx)); + + Geom::Point p2(xc + ax - (k2 * ay), + yc + ay + (k2 * ax)); + Geom::Point p3(xc + bx + (k2 * by), + yc + by - (k2 * bx)); + SPCtrlCurve *curve = ControlManager::getManager().createControlCurve(sp_desktop_tempgroup(desktop), p1, p2, p3, p4, CTLINE_SECONDARY); + + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(SP_CANVAS_ITEM(curve), 0, true)); + } +} + +} // namespace + + +MeasureTool::MeasureTool() : ToolBase() { + this->grabbed = 0; + + this->cursor_shape = cursor_measure_xpm; + this->hot_x = 4; + this->hot_y = 4; +} + +MeasureTool::~MeasureTool() { +} + +void MeasureTool::finish() { + this->enableGrDrag(false); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, GDK_CURRENT_TIME); + this->grabbed = NULL; + } +} + +//void MeasureTool::setup() { +// ToolBase* ec = this; +// +//// if (SP_EVENT_CONTEXT_CLASS(sp_measure_context_parent_class)->setup) { +//// SP_EVENT_CONTEXT_CLASS(sp_measure_context_parent_class)->setup(ec); +//// } +// ToolBase::setup(); +//} + +//gint MeasureTool::item_handler(SPItem* item, GdkEvent* event) { +// gint ret = FALSE; +// +//// if (SP_EVENT_CONTEXT_CLASS(sp_measure_context_parent_class)->item_handler) { +//// ret = SP_EVENT_CONTEXT_CLASS(sp_measure_context_parent_class)->item_handler(event_context, item, event); +//// } +// ret = ToolBase::item_handler(item, event); +// +// return ret; +//} + +static bool GeomPointSortPredicate(const Geom::Point& p1, const Geom::Point& p2) +{ + if (p1[Geom::Y] == p2[Geom::Y]) { + return p1[Geom::X] < p2[Geom::X]; + } else { + return p1[Geom::Y] < p2[Geom::Y]; + } +} + +static void calculate_intersections(SPDesktop * /*desktop*/, SPItem* item, Geom::PathVector const &lineseg, SPCurve *curve, std::vector<Geom::Point> &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()); + + // Reconstruct and store the points of intersection + for (Geom::Crossings::const_iterator m = cs[0].begin(); m != cs[0].end(); ++m) { +#if 0 +//TODO: consider only visible intersections + Geom::Point intersection = lineseg[0].pointAt((*m).ta); + double eps = 0.0001; + SPDocument* doc = sp_desktop_document(desktop); + if (((*m).ta > eps && + item == doc->getItemAtPoint(desktop->dkey, lineseg[0].pointAt((*m).ta - eps), false, NULL)) || + ((*m).ta + eps < 1 && + item == doc->getItemAtPoint(desktop->dkey, lineseg[0].pointAt((*m).ta + eps), false, NULL)) ) { + intersections.push_back(intersection); + } +#else + intersections.push_back(lineseg[0].pointAt((*m).ta)); +#endif + } +} + +bool MeasureTool::root_handler(GdkEvent* event) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: { + Geom::Point const button_w(event->button.x, event->button.y); + explicitBase = boost::none; + lastEnd = boost::none; + start_point = desktop->w2d(button_w); + + if (event->button.button == 1 && !this->space_panning) { + // save drag origin + xp = static_cast<gint>(event->button.x); + yp = static_cast<gint>(event->button.y); + within_tolerance = true; + + ret = TRUE; + } + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(start_point, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK, + NULL, event->button.time); + this->grabbed = SP_CANVAS_ITEM(desktop->acetate); + break; + } + case GDK_KEY_PRESS: { + if ((event->key.keyval == GDK_KEY_Shift_L) || (event->key.keyval == GDK_KEY_Shift_R)) { + if (lastEnd) { + explicitBase = lastEnd; + } + } + break; + } + case GDK_MOTION_NOTIFY: { + if (!((event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning)) { + 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); + scp.addOrigin(start_point); + + m.preSnap(scp); + m.unSetup(); + } + } else { + ret = TRUE; + + if ( within_tolerance + && ( abs( static_cast<gint>(event->motion.x) - xp ) < tolerance ) + && ( abs( static_cast<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; + + //clear previous temporary canvas items, we'll draw new ones + for (size_t idx = 0; idx < measure_tmp_items.size(); ++idx) { + desktop->remove_temporary_canvasitem(measure_tmp_items[idx]); + } + + measure_tmp_items.clear(); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(desktop->w2d(motion_w)); + Geom::Point end_point = motion_dt; + + if (event->motion.state & GDK_CONTROL_MASK) { + spdc_endpoint_snap_rotation(this, end_point, start_point, event->motion.state); + } else { + if (!(event->motion.state & GDK_SHIFT_MASK)) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + Inkscape::SnapCandidatePoint scp(end_point, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(start_point); + Inkscape::SnappedPoint sp = m.freeSnap(scp); + end_point = sp.getPoint(); + m.unSetup(); + } + } + + Geom::PathVector lineseg; + Geom::Path p; + p.start(desktop->dt2doc(start_point)); + p.appendNew<Geom::LineSegment>(desktop->dt2doc(end_point)); + lineseg.push_back(p); + + double deltax = end_point[Geom::X] - start_point[Geom::X]; + double deltay = end_point[Geom::Y] - start_point[Geom::Y]; + double angle = atan2(deltay, deltax); + double baseAngle = 0; + + if (explicitBase) { + double deltax2 = explicitBase.get()[Geom::X] - start_point[Geom::X]; + double deltay2 = explicitBase.get()[Geom::Y] - start_point[Geom::Y]; + + baseAngle = atan2(deltay2, deltax2); + angle -= baseAngle; + + if (angle < -M_PI) { + angle += 2 * M_PI; + } else if (angle > M_PI) { + angle -= 2 * M_PI; + } + } + +//TODO: calculate NPOINTS +//800 seems to be a good value for 800x600 resolution +#define NPOINTS 800 + + std::vector<Geom::Point> points; + + for (double i = 0; i < NPOINTS; i++) { + points.push_back(desktop->d2w(start_point + (i / NPOINTS) * (end_point - start_point))); + } + +// TODO: Felipe, why don't you simply iterate over all items, and test whether their bounding boxes intersect +// with the measurement line, instead of interpolating? E.g. bbox_of_measurement_line.intersects(*bbox_of_item). +// That's also how the object-snapper works, see _findCandidates() in object-snapper.cpp. + + //select elements crossed by line segment: + GSList *items = sp_desktop_document(desktop)->getItemsAtPoints(desktop->dkey, points); + std::vector<Geom::Point> intersections; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool ignore_1st_and_last = prefs->getBool("/tools/measure/ignore_1st_and_last", true); + + if (!ignore_1st_and_last) { + intersections.push_back(desktop->dt2doc(start_point)); + } + + std::vector<LabelPlacement> placements; + + // TODO switch to a different variable name. The single letter 'l' is easy to misread. + for (GSList *l = items; l != NULL; l = l->next) { + SPItem *item = static_cast<SPItem*>(l->data); + + if (SP_IS_SHAPE(item)) { + calculate_intersections(desktop, item, lineseg, SP_SHAPE(item)->getCurve(), intersections); + } else { + if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + Inkscape::Text::Layout::iterator iter = te_get_layout(item)->begin(); + do { + Inkscape::Text::Layout::iterator iter_next = iter; + iter_next.nextGlyph(); // iter_next is one glyph ahead from iter + if (iter == iter_next) { + break; + } + + // get path from iter to iter_next: + SPCurve *curve = te_get_layout(item)->convertToCurves(iter, iter_next); + iter = iter_next; // shift to next glyph + if (!curve) { + continue; // error converting this glyph + } + if (curve->is_empty()) { // whitespace glyph? + curve->unref(); + continue; + } + + curve->transform(item->i2doc_affine()); + + calculate_intersections(desktop, item, lineseg, curve, intersections); + + if (iter == te_get_layout(item)->end()) { + break; + } + } while (true); + } + } + } + + if (!ignore_1st_and_last) { + intersections.push_back(desktop->dt2doc(end_point)); + } + + //sort intersections + if (intersections.size() > 2) { + std::sort(intersections.begin(), intersections.end(), GeomPointSortPredicate); + } + + Glib::ustring unit_name = prefs->getString("/tools/measure/unit"); + if (!unit_name.compare("")) { + unit_name = "px"; + } + + double fontsize = prefs->getInt("/tools/measure/fontsize"); + + // Normal will be used for lines and text + Geom::Point windowNormal = Geom::unit_vector(Geom::rot90(desktop->d2w(end_point - start_point))); + Geom::Point normal = desktop->w2d(windowNormal); + + 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; + placement.start = desktop->doc2dt( (intersections[idx - 1] + intersections[idx]) / 2 ); + placement.end = placement.start - (normal * placement.offset); + + placements.push_back(placement); + } + + // Adjust positions + repositionOverlappingLabels(placements, desktop, windowNormal, fontsize); + + for (std::vector<LabelPlacement>::iterator it = placements.begin(); it != placements.end(); ++it) + { + LabelPlacement &place = *it; + + // TODO cleanup memory, Glib::ustring, etc.: + gchar *measure_str = g_strdup_printf("%.2f %s", place.lengthVal, unit_name.c_str()); + SPCanvasText *canvas_tooltip = sp_canvastext_new(sp_desktop_tempgroup(desktop), + desktop, + place.end, + measure_str); + sp_canvastext_set_fontsize(canvas_tooltip, fontsize); + canvas_tooltip->rgba = 0xffffffff; + canvas_tooltip->rgba_background = 0x0000007f; + canvas_tooltip->outline = false; + canvas_tooltip->background = true; + canvas_tooltip->anchor_position = TEXT_ANCHOR_CENTER; + + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(canvas_tooltip, 0)); + g_free(measure_str); + } + + Geom::Point angleDisplayPt = calcAngleDisplayAnchor(desktop, angle, baseAngle, + start_point, end_point, + fontsize); + + { + // TODO cleanup memory, Glib::ustring, etc.: + gchar *angle_str = g_strdup_printf("%.2f °", angle * 180/M_PI); + + SPCanvasText *canvas_tooltip = sp_canvastext_new(sp_desktop_tempgroup(desktop), + desktop, + angleDisplayPt, + angle_str); + sp_canvastext_set_fontsize(canvas_tooltip, fontsize); + canvas_tooltip->rgba = 0xffffffff; + canvas_tooltip->rgba_background = 0x337f337f; + canvas_tooltip->outline = false; + canvas_tooltip->background = true; + canvas_tooltip->anchor_position = TEXT_ANCHOR_CENTER; + + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(canvas_tooltip, 0)); + g_free(angle_str); + } + + { + double totallengthval = (end_point - start_point).length(); + totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name); + + // TODO cleanup memory, Glib::ustring, etc.: + gchar *totallength_str = g_strdup_printf("%.2f %s", totallengthval, unit_name.c_str()); + SPCanvasText *canvas_tooltip = sp_canvastext_new(sp_desktop_tempgroup(desktop), + desktop, + end_point + desktop->w2d(Geom::Point(3*fontsize, -fontsize)), + totallength_str); + sp_canvastext_set_fontsize(canvas_tooltip, fontsize); + canvas_tooltip->rgba = 0xffffffff; + canvas_tooltip->rgba_background = 0x3333337f; + canvas_tooltip->outline = false; + canvas_tooltip->background = true; + canvas_tooltip->anchor_position = TEXT_ANCHOR_LEFT; + + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(canvas_tooltip, 0)); + g_free(totallength_str); + } + + if (intersections.size() > 2) { + double totallengthval = (intersections[intersections.size()-1] - intersections[0]).length(); + totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name); + + // TODO cleanup memory, Glib::ustring, etc.: + gchar *total_str = g_strdup_printf("%.2f %s", totallengthval, unit_name.c_str()); + SPCanvasText *canvas_tooltip = sp_canvastext_new(sp_desktop_tempgroup(desktop), + desktop, + desktop->doc2dt((intersections[0] + intersections[intersections.size()-1])/2) + normal * 60, + total_str); + sp_canvastext_set_fontsize(canvas_tooltip, fontsize); + canvas_tooltip->rgba = 0xffffffff; + canvas_tooltip->rgba_background = 0x33337f7f; + canvas_tooltip->outline = false; + canvas_tooltip->background = true; + canvas_tooltip->anchor_position = TEXT_ANCHOR_CENTER; + + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(canvas_tooltip, 0)); + g_free(total_str); + } + + // 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) { + // Display the intersection indicator (i.e. the cross) + SPCanvasItem * canvasitem = sp_canvas_item_new(sp_desktop_tempgroup(desktop), + SP_TYPE_CTRL, + "anchor", SP_ANCHOR_CENTER, + "size", 8.0, + "stroked", TRUE, + "stroke_color", 0xff0000ff, + "mode", SP_KNOT_MODE_XOR, + "shape", SP_KNOT_SHAPE_CROSS, + NULL ); + + SP_CTRL(canvasitem)->moveto(desktop->doc2dt(intersections[idx])); + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(canvasitem, 0)); + } + + // Since adding goes to the bottom, do all lines last. + + // draw main control line + { + SPCtrlLine *control_line = ControlManager::getManager().createControlLine(sp_desktop_tempgroup(desktop), + start_point, + end_point); + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(control_line, 0)); + + if ((end_point[Geom::X] != start_point[Geom::X]) && (end_point[Geom::Y] != start_point[Geom::Y])) { + double length = std::abs((end_point - start_point).length()); + Geom::Point anchorEnd = start_point; + anchorEnd[Geom::X] += length; + if (explicitBase) { + anchorEnd *= (Geom::Affine(Geom::Translate(-start_point)) + * Geom::Affine(Geom::Rotate(baseAngle)) + * Geom::Affine(Geom::Translate(start_point))); + } + + SPCtrlLine *control_line = ControlManager::getManager().createControlLine(sp_desktop_tempgroup(desktop), + start_point, + anchorEnd, + CTLINE_SECONDARY); + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(control_line, 0)); + + createAngleDisplayCurve(desktop, start_point, end_point, angleDisplayPt, angle); + } + } + + if (intersections.size() > 2) { + ControlManager &mgr = ControlManager::getManager(); + SPCtrlLine *control_line = 0; + control_line = mgr.createControlLine(sp_desktop_tempgroup(desktop), + desktop->doc2dt(intersections[0]) + normal * 60, + desktop->doc2dt(intersections[intersections.size() - 1]) + normal * 60); + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(control_line, 0)); + + control_line = mgr.createControlLine(sp_desktop_tempgroup(desktop), + desktop->doc2dt(intersections[0]), + desktop->doc2dt(intersections[0]) + normal * 65); + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(control_line, 0)); + + control_line = mgr.createControlLine(sp_desktop_tempgroup(desktop), + desktop->doc2dt(intersections[intersections.size() - 1]), + desktop->doc2dt(intersections[intersections.size() - 1]) + normal * 65); + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(control_line, 0)); + } + + // call-out lines + for (std::vector<LabelPlacement>::iterator it = placements.begin(); it != placements.end(); ++it) + { + LabelPlacement &place = *it; + + ControlManager &mgr = ControlManager::getManager(); + SPCtrlLine *control_line = mgr.createControlLine(sp_desktop_tempgroup(desktop), + place.start, + place.end, + CTLINE_SECONDARY); + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(control_line, 0)); + } + + { + for (size_t idx = 1; idx < intersections.size(); ++idx) { + Geom::Point measure_text_pos = (intersections[idx - 1] + intersections[idx]) / 2; + + ControlManager &mgr = ControlManager::getManager(); + SPCtrlLine *control_line = mgr.createControlLine(sp_desktop_tempgroup(desktop), + desktop->doc2dt(measure_text_pos), + desktop->doc2dt(measure_text_pos) - (normal * DIMENSION_OFFSET), + CTLINE_SECONDARY); + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(control_line, 0)); + } + } + + // Initial point + { + SPCanvasItem * canvasitem = sp_canvas_item_new(sp_desktop_tempgroup(desktop), + SP_TYPE_CTRL, + "anchor", SP_ANCHOR_CENTER, + "size", 8.0, + "stroked", TRUE, + "stroke_color", 0xff0000ff, + "mode", SP_KNOT_MODE_XOR, + "shape", SP_KNOT_SHAPE_CROSS, + NULL ); + + SP_CTRL(canvasitem)->moveto(start_point); + measure_tmp_items.push_back(desktop->add_temporary_canvasitem(canvasitem, 0)); + } + + lastEnd = end_point; // track in case we get a anchoring key-press later + + gobble_motion_events(GDK_BUTTON1_MASK); + } + break; + } + case GDK_BUTTON_RELEASE: { + sp_event_context_discard_delayed_snap_event(this); + explicitBase = boost::none; + lastEnd = boost::none; + + //clear all temporary canvas items related to the measurement tool. + for (size_t idx = 0; idx < measure_tmp_items.size(); ++idx) { + desktop->remove_temporary_canvasitem(measure_tmp_items[idx]); + } + + measure_tmp_items.clear(); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, event->button.time); + this->grabbed = NULL; + } + + xp = 0; + yp = 0; + 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/measure-tool.h b/src/ui/tools/measure-tool.h new file mode 100644 index 000000000..9701ba6ea --- /dev/null +++ b/src/ui/tools/measure-tool.h @@ -0,0 +1,50 @@ +#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> + * + * Copyright (C) 2011 Authors + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include "ui/tools/tool-base.h" +#include <2geom/point.h> +#include <boost/optional.hpp> + +#define SP_MEASURE_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::MeasureTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_MEASURE_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::MeasureTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class MeasureTool : public ToolBase { +public: + MeasureTool(); + virtual ~MeasureTool(); + + static const std::string prefsPath; + + virtual void finish(); + virtual bool root_handler(GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +private: + SPCanvasItem* grabbed; + + Geom::Point start_point; + boost::optional<Geom::Point> explicitBase; + boost::optional<Geom::Point> lastEnd; +}; + +} +} +} + +#endif // SEEN_SP_MEASURING_CONTEXT_H diff --git a/src/ui/tools/mesh-tool.cpp b/src/ui/tools/mesh-tool.cpp new file mode 100644 index 000000000..4e7617f44 --- /dev/null +++ b/src/ui/tools/mesh-tool.cpp @@ -0,0 +1,1017 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +//#define DEBUG_MESH + + +// Libraries +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +// General +#include "desktop.h" +#include "desktop-handles.h" +#include "document.h" +#include "document-undo.h" +#include "macros.h" +#include "message-context.h" +#include "message-stack.h" +#include "preferences.h" +#include "rubberband.h" +#include "selection.h" +#include "snap.h" +#include "sp-namedview.h" +#include "verbs.h" + +// Gradient specific +#include "gradient-drag.h" +#include "gradient-chemistry.h" +#include "pixmaps/cursor-gradient.xpm" +#include "pixmaps/cursor-gradient-add.xpm" + +// Mesh specific +#include "ui/tools/mesh-tool.h" +#include "sp-mesh-gradient.h" +#include "display/sp-ctrlcurve.h" + +using Inkscape::DocumentUndo; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void sp_mesh_drag(MeshTool &rc, Geom::Point const pt, guint state, guint32 etime); + +namespace { + ToolBase* createMeshContext() { + return new MeshTool(); + } + + bool meshContextRegistered = ToolFactory::instance().registerObject("/tools/mesh", createMeshContext); +} + +const std::string& MeshTool::getPrefsPath() { + return MeshTool::prefsPath; +} + +const std::string MeshTool::prefsPath = "/tools/mesh"; + +MeshTool::MeshTool() : ToolBase() { + this->selcon = 0; + this->node_added = false; + this->subselcon = 0; + + this->cursor_addnode = false; + this->cursor_shape = cursor_gradient_xpm; + this->hot_x = 4; + this->hot_y = 4; + this->xp = 0; + this->yp = 0; + this->tolerance = 6; + this->within_tolerance = false; + this->item_to_select = NULL; +} + +MeshTool::~MeshTool() { + this->enableGrDrag(false); + + this->selcon->disconnect(); + delete this->selcon; + + this->subselcon->disconnect(); + delete this->subselcon; +} + +const gchar *ms_handle_descr [] = { + 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*/) { + GrDrag *drag = this->_grdrag; + Inkscape::Selection *selection = sp_desktop_selection(this->desktop); + + if (selection == NULL) { + return; + } + + guint n_obj = g_slist_length((GSList *) selection->itemList()); + + 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 mesh handle"," out of %d mesh handles",n_tot), + ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL); + this->message_context->setF(Inkscape::NORMAL_MESSAGE, + message,_(ms_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 mesh handle"," out of %d mesh handles",n_tot), + ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL); + this->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 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),NULL); + 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.. + // std::cout << "mesh_selection_changed: selection: objects: " << n_obj << std::endl; + // GSList *itemList = (GSList *) selection->itemList(); + // while( itemList ) { + + // SPItem *item = SP_ITEM( itemList->data ); + // // std::cout << " item: " << SP_OBJECT(item)->getId() << std::endl; + + // SPStyle *style = item->style; + // if (style && (style->fill.isPaintserver())) { + + // SPPaintServer *server = item->style->getFillPaintServer(); + // if ( SP_IS_MESHGRADIENT(server) ) { + + // SPMeshGradient *mg = SP_MESHGRADIENT(server); + + // guint rows = 0;//mg->array.patches.size(); + // for ( guint i = 0; i < rows; ++i ) { + // guint columns = 0;//mg->array.patches[0].size(); + // for ( guint j = 0; j < columns; ++j ) { + // } + // } + // } + // } + // itemList = itemList->next; + // } + + // GList* dragger_ptr = drag->draggers; // Points to GrDragger class (group of GrDraggable) + // guint count = 0; + // while( dragger_ptr ) { + + // std::cout << "mesh_selection_changed: dragger: " << ++count << std::endl; + // GSList* draggable_ptr = ((GrDragger *) dragger_ptr->data)->draggables; + + // while( draggable_ptr ) { + + // std::cout << "mesh_selection_changed: draggable: " << draggable_ptr << std::endl; + // GrDraggable *draggable = (GrDraggable *) draggable_ptr->data; + + // gint point_type = draggable->point_type; + // gint point_i = draggable->point_i; + // bool fill_or_stroke = draggable->fill_or_stroke; + + // if( point_type == POINT_MG_CORNER ) { + + // //std::cout << "mesh_selection_changed: POINT_MG_CORNER: " << point_i << std::endl; + // // Now we must create or destroy corresponding handles. + + // if( g_list_find( drag->selected, dragger_ptr->data ) ) { + // //std::cout << "gradient_selection_changed: Selected: " << point_i << std::endl; + // // Which meshes does this point belong to? + + // } else { + // //std::cout << "mesh_selection_changed: Not Selected: " << point_i << std::endl; + // } + // } + + // draggable_ptr = draggable_ptr->next; + + // } + + // dragger_ptr = dragger_ptr->next; + // } +} + +void MeshTool::setup() { + ToolBase::setup(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/mesh/selcue", true)) { + this->enableSelectionCue(); + } + + this->enableGrDrag(); + Inkscape::Selection *selection = sp_desktop_selection(this->desktop); + + this->selcon = new sigc::connection(selection->connectChanged( + sigc::mem_fun(this, &MeshTool::selection_changed) + )); + + this->subselcon = new sigc::connection(this->desktop->connectToolSubselectionChanged( + sigc::hide(sigc::bind( + sigc::mem_fun(*this, &MeshTool::selection_changed), + (Inkscape::Selection*)NULL) + ) + )); + + this->selection_changed(selection); +} + +void +sp_mesh_context_select_next (ToolBase *event_context) +{ + GrDrag *drag = event_context->_grdrag; + g_assert (drag); + + GrDragger *d = drag->select_next(); + + event_context->desktop->scroll_to_point(d->point, 1.0); +} + +void +sp_mesh_context_select_prev (ToolBase *event_context) +{ + GrDrag *drag = event_context->_grdrag; + g_assert (drag); + + GrDragger *d = drag->select_prev(); + + event_context->desktop->scroll_to_point(d->point, 1.0); +} + +/** +Returns true if mouse cursor over mesh edge. +*/ +static bool +sp_mesh_context_is_over_line (MeshTool *rc, SPItem *item, Geom::Point event_p) +{ + SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop; + + //Translate mouse point into proper coord system + rc->mousepoint_doc = desktop->w2d(event_p); + + SPCtrlCurve *curve = SP_CTRLCURVE(item); + Geom::BezierCurveN<3> b( curve->p0, curve->p1, curve->p2, curve->p3 ); + Geom::Coord coord = b.nearestPoint( rc->mousepoint_doc ); // Coord == double + Geom::Point nearest = b( coord ); + + double dist_screen = Geom::L2 (rc->mousepoint_doc - nearest) * desktop->current_zoom(); + + double tolerance = (double) SP_EVENT_CONTEXT(rc)->tolerance; + + bool close = (dist_screen < tolerance); + + return close; +} + + +/** +Split row/column near the mouse point. +*/ +static void sp_mesh_context_split_near_point(MeshTool *rc, SPItem *item, Geom::Point mouse_p, guint32 /*etime*/) +{ + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_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 + + ToolBase *ec = SP_EVENT_CONTEXT(rc); + SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop; + + double tolerance = (double) ec->tolerance; + + ec->get_drag()->addStopNearPoint (item, mouse_p, tolerance/desktop->current_zoom()); + + DocumentUndo::done(sp_desktop_document (desktop), SP_VERB_CONTEXT_MESH, + _("Split mesh row/column")); + + ec->get_drag()->updateDraggers(); +} + +/** +Wrapper for various mesh operations that require a list of selected corner nodes. + */ +static void +sp_mesh_context_corner_operation (MeshTool *rc, MeshCornerOperation operation ) +{ + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_corner_operation: entrance: " << operation << std::endl; +#endif + + SPDocument *doc = NULL; + GrDrag *drag = rc->_grdrag; + + std::map<SPMeshGradient*, std::vector<guint> > points; + std::map<SPMeshGradient*, SPItem*> items; + + // Get list of selected draggers for each mesh. + // For all selected draggers + for (GList *i = drag->selected; i != NULL; i = i->next) { + GrDragger *dragger = (GrDragger *) i->data; + // For all draggables of dragger + for (GSList const* j = dragger->draggables; j != NULL; j = j->next) { + GrDraggable *d = (GrDraggable *) j->data; + + // Only mesh corners + if( d->point_type != POINT_MG_CORNER ) continue; + + // Find the gradient + SPMeshGradient *gradient = SP_MESHGRADIENT( getGradient (d->item, d->fill_or_stroke) ); + + // Collect points together for same gradient + points[gradient].push_back( d->point_i ); + items[gradient] = d->item; + } + } + + // Loop over meshes. + for( std::map<SPMeshGradient*, std::vector<guint> >::const_iterator iter = points.begin(); iter != points.end(); ++iter) { + SPMeshGradient *mg = SP_MESHGRADIENT( 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; + + default: + std::cout << "sp_mesh_corner_operation: unknown operation" << std::endl; + } + + if( noperation > 0 ) { + mg->array.write( mg ); + mg->requestModified(SP_OBJECT_MODIFIED_FLAG); + doc = mg->document; + + switch (operation) { + + case MG_CORNER_SIDE_TOGGLE: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Toggled mesh path type.")); + break; + + case MG_CORNER_SIDE_ARC: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Approximated arc for mesh side.")); + break; + + case MG_CORNER_TENSOR_TOGGLE: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Toggled mesh tensors.")); + break; + + case MG_CORNER_COLOR_SMOOTH: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Smoothed mesh corner color.")); + break; + + case MG_CORNER_COLOR_PICK: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Picked mesh corner color.")); + break; + + default: + std::cout << "sp_mesh_corner_operation: unknown operation" << std::endl; + } + } + } + } + drag->updateDraggers(); + +} + + +/** +Handles all keyboard and mouse input for meshs. +*/ +bool MeshTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = sp_desktop_selection (desktop); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + double const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px + + GrDrag *drag = this->_grdrag; + g_assert (drag); + + gint ret = FALSE; + + switch (event->type) { + case GDK_2BUTTON_PRESS: + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_2BUTTON_PRESS" << std::endl; +#endif + + // Double click: + // If over a mesh line, divide mesh row/column + // If not over a line, create new gradients for selected objects. + + if ( event->button.button == 1 ) { + // Are we over a mesh line? + bool over_line = false; + SPCtrlCurve *line = NULL; + + if (drag->lines) { + for (GSList *l = drag->lines; (l != NULL) && (!over_line); l = l->next) { + line = (SPCtrlCurve*) l->data; + over_line |= sp_mesh_context_is_over_line (this, (SPItem*) line, Geom::Point(event->motion.x, event->motion.y)); + } + } + + if (over_line) { + // We take the first item in selection, because with doubleclick, the first click + // always resets selection to the single object under cursor + sp_mesh_context_split_near_point(this, SP_ITEM(selection->itemList()->data), this->mousepoint_doc, event->button.time); + } else { + // Create a new gradient with default coordinates. + for (GSList const* i = selection->itemList(); i != NULL; i = i->next) { + SPItem *item = SP_ITEM(i->data); + SPGradientType new_type = SP_GRADIENT_TYPE_MESH; + Inkscape::PaintTarget fsmode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: creating new mesh on: " << (fsmode == Inkscape::FOR_FILL ? "Fill" : "Stroke") << std::endl; +#endif + SPGradient *vector = sp_gradient_vector_for_object(sp_desktop_document(desktop), desktop, item, fsmode); + + SPGradient *priv = sp_item_set_gradient(item, vector, new_type, fsmode); + sp_gradient_reset_to_userspace(priv, item); + } + + DocumentUndo::done(sp_desktop_document (desktop), SP_VERB_CONTEXT_MESH, + _("Create default mesh")); + } + + ret = TRUE; + } + break; + + case GDK_BUTTON_PRESS: + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_BUTTON_PRESS" << std::endl; +#endif + // Button down + // If Shift key down: do rubber band selection + // Else set origin for drag. A drag creates a new gradient if one does not exist + if ( event->button.button == 1 && !this->space_panning ) { + Geom::Point button_w(event->button.x, event->button.y); + + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + dragging = true; + + Geom::Point button_dt = desktop->w2d(button_w); + if (event->button.state & GDK_SHIFT_MASK) { + Inkscape::Rubberband::get(desktop)->start(desktop, button_dt); + } else { + // remember clicked item, disregarding groups, honoring Alt; do nothing with Crtl to + // enable Ctrl+doubleclick of exactly the selected item(s) + if (!(event->button.state & GDK_CONTROL_MASK)) { + this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + } + + if (!selection->isEmpty()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + } + + this->origin = button_dt; + } + + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + // Mouse move + if ( dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning ) { + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_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 = this->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 { + // Create new gradient with coordinates determined by drag. + sp_mesh_drag(*this, motion_dt, event->motion.state, event->motion.time); + } + + gobble_motion_events(GDK_BUTTON1_MASK); + + ret = TRUE; + } else { + // Not dragging + + // Do snapping + 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 = this->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( drag->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 + bool over_line = false; + + if (drag->lines) { + for (GSList *l = drag->lines; l != NULL; l = l->next) { + over_line |= sp_mesh_context_is_over_line (this, (SPItem*) l->data, Geom::Point(event->motion.x, event->motion.y)); + } + } + + if (this->cursor_addnode && !over_line) { + this->cursor_shape = cursor_gradient_xpm; + this->sp_event_context_update_cursor(); + this->cursor_addnode = false; + } else if (!this->cursor_addnode && over_line) { + this->cursor_shape = cursor_gradient_add_xpm; + this->sp_event_context_update_cursor(); + this->cursor_addnode = true; + } + } + break; + + case GDK_BUTTON_RELEASE: + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_BUTTON_RELEASE" << std::endl; +#endif + + this->xp = this->yp = 0; + + if ( event->button.button == 1 && !this->space_panning ) { + // Check if over line + bool over_line = false; + SPCtrlLine *line = NULL; + + if (drag->lines) { + for (GSList *l = drag->lines; (l != NULL) && (!over_line); l = l->next) { + line = (SPCtrlLine*) l->data; + over_line = sp_mesh_context_is_over_line (this, (SPItem*) line, Geom::Point(event->motion.x, event->motion.y)); + + if (over_line) { + break; + } + } + } + + if ( (event->button.state & GDK_CONTROL_MASK) && (event->button.state & GDK_MOD1_MASK ) ) { + if (over_line && line) { + sp_mesh_context_split_near_point(this, line->item, this->mousepoint_doc, 0); + ret = TRUE; + } + } else { + dragging = false; + + // unless clicked with Ctrl (to enable Ctrl+doubleclick). + if (event->button.state & GDK_CONTROL_MASK) { + ret = TRUE; + break; + } + + if (!this->within_tolerance) { + // we've been dragging, either do nothing (grdrag handles that), + // or rubberband-select if we have rubberband + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + + if (r->is_started() && !this->within_tolerance) { + // this was a rubberband drag + if (r->getMode() == RUBBERBAND_MODE_RECT) { + Geom::OptRect const b = r->getRectangle(); + drag->selectRect(*b); + } + } + } else if (this->item_to_select) { + if (over_line && line) { + // 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 { + selection->set(this->item_to_select); + } + } + } else { + // click in an empty space; do the same as Esc + if (drag->selected) { + drag->deselectAll(); + } else { + selection->clear(); + } + } + + this->item_to_select = NULL; + ret = TRUE; + } + + Inkscape::Rubberband::get(desktop)->stop(); + } + break; + + case GDK_KEY_PRESS: + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_KEY_PRESS" << std::endl; +#endif + + // FIXME: tip + switch (get_group0_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) && drag->isNonEmpty()) { + drag->selectAll(); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (drag->selected) { + drag->deselectAll(); + } else { + selection->clear(); + } + + ret = TRUE; + //TODO: make dragging escapable by Esc + break; + + case GDK_KEY_Left: // move handle left + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + drag->selected_move_screen(mul*-10, 0); // shift + } else { + drag->selected_move_screen(mul*-1, 0); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + drag->selected_move(mul*-10*nudge, 0); // shift + } else { + drag->selected_move(mul*-nudge, 0); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Up: // move handle up + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + drag->selected_move_screen(0, mul*10); // shift + } else { + drag->selected_move_screen(0, mul*1); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + drag->selected_move(0, mul*10*nudge); // shift + } else { + drag->selected_move(0, mul*nudge); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Right: // move handle right + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + drag->selected_move_screen(mul*10, 0); // shift + } else { + drag->selected_move_screen(mul*1, 0); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + drag->selected_move(mul*10*nudge, 0); // shift + } else { + drag->selected_move(mul*nudge, 0); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Down: // move handle down + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + drag->selected_move_screen(0, mul*-10); // shift + } else { + drag->selected_move_screen(0, mul*-1); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + drag->selected_move(0, mul*-10*nudge); // shift + } else { + drag->selected_move(0, mul*-nudge); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Insert: + case GDK_KEY_KP_Insert: + // with any modifiers: + //sp_gradient_context_add_stops_between_selected_stops (rc); + std::cout << "Inserting stops between selected stops not implemented yet" << std::endl; + ret = TRUE; + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + if ( drag->selected ) { + std::cout << "Deleting mesh stops not implemented yet" << std::endl; + ret = TRUE; + } + break; + + // Mesh Operations -------------------------------------------- + + case GDK_KEY_b: // Toggle mesh side between lineto and curveto. + case GDK_KEY_B: + if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) { + sp_mesh_context_corner_operation ( this, 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) && drag->isNonEmpty() && drag->hasSelection()) { + sp_mesh_context_corner_operation ( this, 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) && drag->isNonEmpty() && drag->hasSelection()) { + sp_mesh_context_corner_operation ( this, MG_CORNER_TENSOR_TOGGLE ); + ret = TRUE; + } + break; + + case GDK_KEY_j: // Smooth corner color + case GDK_KEY_J: + if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) { + sp_mesh_context_corner_operation ( this, MG_CORNER_COLOR_SMOOTH ); + ret = TRUE; + } + break; + + case GDK_KEY_k: // Pick corner color + case GDK_KEY_K: + if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) { + sp_mesh_context_corner_operation ( this, MG_CORNER_COLOR_PICK ); + ret = TRUE; + } + break; + + default: + break; + } + + break; + + case GDK_KEY_RELEASE: + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_KEY_RELEASE" << std::endl; +#endif + switch (get_group0_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; +} + +static void sp_mesh_drag(MeshTool &rc, Geom::Point const /*pt*/, guint /*state*/, guint32 /*etime*/) { + SPDesktop *desktop = SP_EVENT_CONTEXT(&rc)->desktop; + Inkscape::Selection *selection = sp_desktop_selection(desktop); + SPDocument *document = sp_desktop_document(desktop); + ToolBase *ec = SP_EVENT_CONTEXT(&rc); + + if (!selection->isEmpty()) { + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int type = SP_GRADIENT_TYPE_MESH; + Inkscape::PaintTarget fill_or_stroke = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + + SPGradient *vector; + if (ec->item_to_select) { + // pick color from the object where drag started + vector = sp_gradient_vector_for_object(document, desktop, ec->item_to_select, fill_or_stroke); + } else { + // Starting from empty space: + // Sort items so that the topmost comes last + GSList *items = g_slist_copy ((GSList *) selection->itemList()); + items = g_slist_sort(items, (GCompareFunc) sp_item_repr_compare_position); + // take topmost + vector = sp_gradient_vector_for_object(document, desktop, SP_ITEM(g_slist_last(items)->data), fill_or_stroke); + g_slist_free (items); + } + + // 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"); + + for (GSList const *i = selection->itemList(); i != NULL; i = i->next) { + + //FIXME: see above + sp_repr_css_change_recursive(SP_OBJECT(i->data)->getRepr(), css, "style"); + + sp_item_set_gradient(SP_ITEM(i->data), vector, (SPGradientType) type, fill_or_stroke); + + // We don't need to do anything. Mesh is already sized appropriately. + + SP_OBJECT(i->data)->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + // if (ec->_grdrag) { + // ec->_grdrag->updateDraggers(); + // // prevent regenerating draggers by selection modified signal, which sometimes + // // comes too late and thus destroys the knot which we will now grab: + // ec->_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 + // ec->_grdrag->grabKnot (SP_ITEM(selection->itemList()->data), + // 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 = g_slist_length((GSList *) selection->itemList()); + rc.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 { + sp_desktop_message_stack(desktop)->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 000000000..d952c9010 --- /dev/null +++ b/src/ui/tools/mesh-tool.h @@ -0,0 +1,77 @@ +#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 + */ + +#include <stddef.h> +#include <sigc++/sigc++.h> +#include "ui/tools/tool-base.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 { +namespace UI { +namespace Tools { + +class MeshTool : public ToolBase { +public: + MeshTool(); + virtual ~MeshTool(); + + Geom::Point origin; + + bool cursor_addnode; + + bool node_added; + + Geom::Point mousepoint_doc; // stores mousepoint when over_line in doc coords + + sigc::connection *selcon; + sigc::connection *subselcon; + + static const std::string prefsPath; + + virtual void setup(); + virtual bool root_handler(GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +private: + void selection_changed(Inkscape::Selection* sel); +}; + +void sp_mesh_context_select_next(ToolBase *event_context); +void sp_mesh_context_select_prev(ToolBase *event_context); + +} +} +} + +#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/tool/node-tool.cpp b/src/ui/tools/node-tool.cpp index 2eeab7c1b..8a950b528 100644 --- a/src/ui/tool/node-tool.cpp +++ b/src/ui/tools/node-tool.cpp @@ -10,7 +10,7 @@ * Released under GNU GPL, read the file 'COPYING' for more information */ -#include "curve-drag-point.h" +#include "ui/tool/curve-drag-point.h" #include <glib/gi18n.h> #include "desktop.h" #include "desktop-handles.h" @@ -30,7 +30,7 @@ #include "sp-path.h" #include "sp-text.h" #include "ui/control-manager.h" -#include "ui/tool/node-tool.h" +#include "ui/tools/node-tool.h" #include "ui/tool/control-point-selection.h" #include "ui/tool/event-utils.h" #include "ui/tool/manipulator.h" diff --git a/src/ui/tool/node-tool.h b/src/ui/tools/node-tool.h index a670256bb..4d15ab70e 100644 --- a/src/ui/tool/node-tool.h +++ b/src/ui/tools/node-tool.h @@ -13,7 +13,7 @@ #include <boost/ptr_container/ptr_map.hpp> #include <glib.h> -#include "event-context.h" +#include "ui/tools/tool-base.h" namespace Inkscape { namespace Display { diff --git a/src/ui/tools/pen-tool.cpp b/src/ui/tools/pen-tool.cpp new file mode 100644 index 000000000..4c573a244 --- /dev/null +++ b/src/ui/tools/pen-tool.cpp @@ -0,0 +1,1404 @@ +/** \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, read the file 'COPYING' for more information + */ + +#include <gdk/gdkkeysyms.h> +#include <cstring> +#include <string> + +#include "ui/tools/pen-tool.h" +#include "sp-namedview.h" +#include "desktop.h" +#include "desktop-handles.h" +#include "selection.h" +#include "selection-chemistry.h" +#include "draw-anchor.h" +#include "message-stack.h" +#include "message-context.h" +#include "preferences.h" +#include "sp-path.h" +#include "display/sp-canvas.h" +#include "display/curve.h" +#include "pixmaps/cursor-pen.xpm" +#include "display/canvas-bpath.h" +#include "display/sp-ctrlline.h" +#include "display/sodipodi-ctrl.h" +#include <glibmm/i18n.h> +#include "macros.h" +#include "context-fns.h" +#include "tools-switch.h" +#include "ui/control-manager.h" +#include "tool-factory.h" + +using Inkscape::ControlManager; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void spdc_pen_set_initial_point(PenTool *pc, Geom::Point const p); +static void spdc_pen_set_subsequent_point(PenTool *const pc, Geom::Point const p, bool statusbar, guint status = 0); +static void spdc_pen_set_ctrl(PenTool *pc, Geom::Point const p, guint state); +static void spdc_pen_finish_segment(PenTool *pc, Geom::Point p, guint state); + +static void spdc_pen_finish(PenTool *pc, gboolean closed); + +static gint pen_handle_button_press(PenTool *const pc, GdkEventButton const &bevent); +static gint pen_handle_motion_notify(PenTool *const pc, GdkEventMotion const &mevent); +static gint pen_handle_button_release(PenTool *const pc, GdkEventButton const &revent); +static gint pen_handle_2button_press(PenTool *const pc, GdkEventButton const &bevent); +static gint pen_handle_key_press(PenTool *const pc, GdkEvent *event); +static void spdc_reset_colors(PenTool *pc); + +static void pen_disable_events(PenTool *const pc); +static void pen_enable_events(PenTool *const pc); + +static Geom::Point pen_drag_origin_w(0, 0); +static bool pen_within_tolerance = false; + +static int pen_next_paraxial_direction(const PenTool *const pc, Geom::Point const &pt, Geom::Point const &origin, guint state); +static void pen_set_to_nearest_horiz_vert(const PenTool *const pc, Geom::Point &pt, guint const state, bool snap); + +static int pen_last_paraxial_dir = 0; // last used direction in horizontal/vertical mode; 0 = horizontal, 1 = vertical + +namespace { + ToolBase* createPenContext() { + return new PenTool(); + } + + bool penContextRegistered = ToolFactory::instance().registerObject("/tools/freehand/pen", createPenContext); +} + +const std::string& PenTool::getPrefsPath() { + return PenTool::prefsPath; +} + +const std::string PenTool::prefsPath = "/tools/freehand/pen"; + +PenTool::PenTool() : FreehandBase() { + this->polylines_only = false; + this->polylines_paraxial = false; + this->expecting_clicks_for_LPE = 0; + + this->cursor_shape = cursor_pen_xpm; + this->hot_x = 4; + this->hot_y = 4; + + this->npoints = 0; + this->mode = MODE_CLICK; + this->state = POINT; + + this->c0 = NULL; + this->c1 = NULL; + this->cl0 = NULL; + this->cl1 = NULL; + + this->events_disabled = 0; + + this->num_clicks = 0; + this->waiting_LPE = NULL; + this->waiting_item = NULL; +} + +PenTool::~PenTool() { + if (this->c0) { + sp_canvas_item_destroy(this->c0); + this->c0 = NULL; + } + if (this->c1) { + sp_canvas_item_destroy(this->c1); + this->c1 = NULL; + } + if (this->cl0) { + sp_canvas_item_destroy(this->cl0); + this->cl0 = NULL; + } + if (this->cl1) { + sp_canvas_item_destroy(this->cl1); + this->cl1 = NULL; + } + + if (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 sp_pen_context_set_polyline_mode(PenTool *const pc) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/tools/freehand/pen/freehand-mode", 0); + pc->polylines_only = (mode == 2 || mode == 3); + pc->polylines_paraxial = (mode == 3); +} + +/** + * Callback to initialize PenTool object. + */ +void PenTool::setup() { + FreehandBase::setup(); + + ControlManager &mgr = ControlManager::getManager(); + + // Pen indicators + this->c0 = mgr.createControl(sp_desktop_controls(SP_EVENT_CONTEXT_DESKTOP(this)), Inkscape::CTRL_TYPE_ADJ_HANDLE); + mgr.track(this->c0); + + this->c1 = mgr.createControl(sp_desktop_controls(SP_EVENT_CONTEXT_DESKTOP(this)), Inkscape::CTRL_TYPE_ADJ_HANDLE); + mgr.track(this->c1); + + this->cl0 = mgr.createControlLine(sp_desktop_controls(SP_EVENT_CONTEXT_DESKTOP(this))); + this->cl1 = mgr.createControlLine(sp_desktop_controls(SP_EVENT_CONTEXT_DESKTOP(this))); + + sp_canvas_item_hide(this->c0); + sp_canvas_item_hide(this->c1); + sp_canvas_item_hide(this->cl0); + sp_canvas_item_hide(this->cl1); + + sp_event_context_read(this, "mode"); + + this->anchor_statusbar = false; + + sp_pen_context_set_polyline_mode(this); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/freehand/pen/selcue")) { + this->enableSelectionCue(); + } +} + +static void pen_cancel (PenTool *const pc) +{ + pc->num_clicks = 0; + pc->state = PenTool::STOP; + spdc_reset_colors(pc); + sp_canvas_item_hide(pc->c0); + sp_canvas_item_hide(pc->c1); + sp_canvas_item_hide(pc->cl0); + sp_canvas_item_hide(pc->cl1); + pc->message_context->clear(); + pc->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled")); + + pc->desktop->canvas->endForcedFullRedraws(); +} + +/** + * Finalization callback. + */ +void PenTool::finish() { + sp_event_context_discard_delayed_snap_event(this); + + if (this->npoints != 0) { + pen_cancel(this); + } + + FreehandBase::finish(); +} + +/** + * 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; + } + } +} + +/** + * Snaps new node relative to the previous node. + */ +static void spdc_endpoint_snap(PenTool const *const pc, Geom::Point &p, guint const state) +{ + if ((state & GDK_CONTROL_MASK) && !pc->polylines_paraxial) { //CTRL enables angular snapping + if (pc->npoints > 0) { + spdc_endpoint_snap_rotation(pc, p, pc->p[0], 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 ((pc->npoints > 0) && pc->polylines_paraxial) { + // snap constrained + pen_set_to_nearest_horiz_vert(pc, p, state, true); + } else { + // snap freely + boost::optional<Geom::Point> origin = pc->npoints > 0 ? pc->p[0] : boost::optional<Geom::Point>(); + spdc_endpoint_snap_free(pc, p, origin, state); // pass the origin, to allow for perpendicular / tangential snapping + } + } +} + +/** + * Snaps new node's handle relative to the new node. + */ +static void spdc_endpoint_snap_handle(PenTool const *const pc, Geom::Point &p, guint const state) +{ + g_return_if_fail(( pc->npoints == 2 || + pc->npoints == 5 )); + + if ((state & GDK_CONTROL_MASK)) { //CTRL enables angular snapping + spdc_endpoint_snap_rotation(pc, p, pc->p[pc->npoints - 2], state); + } else { + if (!(state & GDK_SHIFT_MASK)) { //SHIFT disables all snapping, except the angular snapping above + boost::optional<Geom::Point> origin = pc->p[pc->npoints - 2]; + spdc_endpoint_snap_free(pc, p, origin, state); + } + } +} + +bool PenTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = pen_handle_button_press(this, event->button); + break; + case GDK_BUTTON_RELEASE: + ret = pen_handle_button_release(this, 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) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = pen_handle_button_press(this, event->button); + break; + + case GDK_MOTION_NOTIFY: + ret = pen_handle_motion_notify(this, event->motion); + break; + + case GDK_BUTTON_RELEASE: + ret = pen_handle_button_release(this, event->button); + break; + + case GDK_2BUTTON_PRESS: + ret = pen_handle_2button_press(this, event->button); + break; + + case GDK_KEY_PRESS: + ret = pen_handle_key_press(this, event); + break; + + default: + break; + } + + if (!ret) { + ret = FreehandBase::root_handler(event); + } + + return ret; +} + +/** + * Handle mouse button press event. + */ +static gint pen_handle_button_press(PenTool *const pc, GdkEventButton const &bevent) +{ + if (pc->events_disabled) { + // skip event processing if events are disabled + return FALSE; + } + + FreehandBase * const dc = SP_DRAW_CONTEXT(pc); + SPDesktop * const desktop = SP_EVENT_CONTEXT_DESKTOP(dc); + Geom::Point const event_w(bevent.x, bevent.y); + Geom::Point event_dt(desktop->w2d(event_w)); + ToolBase *event_context = SP_EVENT_CONTEXT(pc); + + gint ret = FALSE; + if (bevent.button == 1 && !event_context->space_panning + // make sure this is not the last click for a waiting LPE (otherwise we want to finish the path) + && pc->expecting_clicks_for_LPE != 1) { + + if (Inkscape::have_viable_layer(desktop, dc->message_context) == false) { + return TRUE; + } + + if (!pc->grab ) { + // Grab mouse, so release will not pass unnoticed + pc->grab = SP_CANVAS_ITEM(desktop->acetate); + sp_canvas_item_grab(pc->grab, ( GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK ), + NULL, bevent.time); + } + + pen_drag_origin_w = event_w; + pen_within_tolerance = true; + + // Test whether we hit any anchor. + SPDrawAnchor * const anchor = spdc_test_inside(pc, event_w); + + switch (pc->mode) { + case PenTool::MODE_CLICK: + // In click mode we add point on release + switch (pc->state) { + case PenTool::POINT: + case PenTool::CONTROL: + case PenTool::CLOSE: + break; + case PenTool::STOP: + // This is allowed, if we just canceled curve + pc->state = PenTool::POINT; + break; + default: + break; + } + break; + case PenTool::MODE_DRAG: + switch (pc->state) { + case PenTool::STOP: + // This is allowed, if we just canceled curve + case PenTool::POINT: + if (pc->npoints == 0) { + + Geom::Point p; + if ((bevent.state & GDK_CONTROL_MASK) && (pc->polylines_only || pc->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(event_context, 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 + pc->sa = anchor; + if (anchor && !sp_pen_context_has_waiting_LPE(pc)) { + // 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 = sp_desktop_selection(desktop); + if (!(bevent.state & GDK_SHIFT_MASK) || sp_pen_context_has_waiting_LPE(pc)) { + // if we have a waiting LPE, we need a fresh path to be created + // so don't append to an existing one + selection->clear(); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path")); + } else if (selection->singleItem() && SP_IS_PATH(selection->singleItem())) { + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path")); + } + + // Create green anchor + p = event_dt; + spdc_endpoint_snap(pc, p, bevent.state); + pc->green_anchor = sp_draw_anchor_new(pc, pc->green_curve, TRUE, p); + } + spdc_pen_set_initial_point(pc, p); + } else { + + // Set end anchor + pc->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 + pc->state = PenTool::CLOSE; + + if (pc->green_anchor && pc->green_anchor->active) { + // we clicked on the current curve start, so close it even if + // we drag a handle away from it + dc->green_closed = TRUE; + } + ret = TRUE; + break; + + } else { + p = event_dt; + spdc_endpoint_snap(pc, p, bevent.state); // Snap node only if not hitting anchor. + spdc_pen_set_subsequent_point(pc, p, true); + } + } + + pc->state = pc->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 (pc->expecting_clicks_for_LPE == 1 && pc->npoints != 0) { + // when the last click for a waiting LPE occurs we want to finish the path + spdc_pen_finish_segment(pc, event_dt, bevent.state); + if (pc->green_closed) { + // finishing at the start anchor, close curve + spdc_pen_finish(pc, TRUE); + } else { + // finishing at some other anchor, finish curve but not close + spdc_pen_finish(pc, FALSE); + } + + ret = TRUE; + } else if (bevent.button == 3 && pc->npoints != 0) { + // right click - finish path + spdc_pen_finish(pc, FALSE); + ret = TRUE; + } + + if (pc->expecting_clicks_for_LPE > 0) { + --pc->expecting_clicks_for_LPE; + } + + return ret; +} + +/** + * Handle motion_notify event. + */ +static gint pen_handle_motion_notify(PenTool *const pc, GdkEventMotion const &mevent) +{ + gint ret = FALSE; + + ToolBase *event_context = SP_EVENT_CONTEXT(pc); + SPDesktop * const dt = SP_EVENT_CONTEXT_DESKTOP(event_context); + + if (event_context->space_panning || mevent.state & GDK_BUTTON2_MASK || mevent.state & GDK_BUTTON3_MASK) { + // allow scrolling + return FALSE; + } + + if (pc->events_disabled) { + // skip motion events if pen events are disabled + return FALSE; + } + + Geom::Point const event_w(mevent.x, + mevent.y); + if (pen_within_tolerance) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint const tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + 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 = dt->w2d(event_w); + + // Test, whether we hit any anchor + SPDrawAnchor *anchor = spdc_test_inside(pc, event_w); + + switch (pc->mode) { + case PenTool::MODE_CLICK: + switch (pc->state) { + case PenTool::POINT: + if ( pc->npoints != 0 ) { + // Only set point, if we are already appending + spdc_endpoint_snap(pc, p, mevent.state); + spdc_pen_set_subsequent_point(pc, p, true); + ret = TRUE; + } else if (!sp_event_context_knot_mouseover(pc)) { + SnapManager &m = dt->namedview->snap_manager; + m.setup(dt); + 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 + spdc_endpoint_snap(pc, p, mevent.state); + spdc_pen_set_ctrl(pc, p, mevent.state); + ret = TRUE; + break; + case PenTool::STOP: + // This is perfectly valid + break; + default: + break; + } + break; + case PenTool::MODE_DRAG: + switch (pc->state) { + case PenTool::POINT: + if ( pc->npoints > 0 ) { + // Only set point, if we are already appending + + if (!anchor) { // Snap node only if not hitting anchor + spdc_endpoint_snap(pc, p, mevent.state); + spdc_pen_set_subsequent_point(pc, p, true, mevent.state); + } else { + spdc_pen_set_subsequent_point(pc, anchor->dp, false, mevent.state); + } + + if (anchor && !pc->anchor_statusbar) { + pc->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to close and finish the path.")); + pc->anchor_statusbar = true; + } else if (!anchor && pc->anchor_statusbar) { + pc->message_context->clear(); + pc->anchor_statusbar = false; + } + + ret = TRUE; + } else { + if (anchor && !pc->anchor_statusbar) { + pc->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to continue the path from this point.")); + pc->anchor_statusbar = true; + } else if (!anchor && pc->anchor_statusbar) { + pc->message_context->clear(); + pc->anchor_statusbar = false; + } + if (!sp_event_context_knot_mouseover(pc)) { + SnapManager &m = dt->namedview->snap_manager; + m.setup(dt); + 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 + spdc_endpoint_snap_handle(pc, p, mevent.state); + + if (!pc->polylines_only) { + spdc_pen_set_ctrl(pc, p, mevent.state); + } else { + spdc_pen_set_ctrl(pc, pc->p[1], mevent.state); + } + gobble_motion_events(GDK_BUTTON1_MASK); + ret = TRUE; + break; + case PenTool::STOP: + // This is perfectly valid + break; + default: + if (!sp_event_context_knot_mouseover(pc)) { + SnapManager &m = dt->namedview->snap_manager; + m.setup(dt); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + } + break; + default: + break; + } + return ret; +} + +/** + * Handle mouse button release event. + */ +static gint pen_handle_button_release(PenTool *const pc, GdkEventButton const &revent) +{ + if (pc->events_disabled) { + // skip event processing if events are disabled + return FALSE; + } + + gint ret = FALSE; + ToolBase *event_context = SP_EVENT_CONTEXT(pc); + if ( revent.button == 1 && !event_context->space_panning) { + + FreehandBase *dc = SP_DRAW_CONTEXT (pc); + + Geom::Point const event_w(revent.x, + revent.y); + // Find desktop coordinates + Geom::Point p = pc->desktop->w2d(event_w); + + // Test whether we hit any anchor. + SPDrawAnchor *anchor = spdc_test_inside(pc, event_w); + + switch (pc->mode) { + case PenTool::MODE_CLICK: + switch (pc->state) { + case PenTool::POINT: + if ( pc->npoints == 0 ) { + // Start new thread only with button release + if (anchor) { + p = anchor->dp; + } + pc->sa = anchor; + spdc_pen_set_initial_point(pc, p); + } else { + // Set end anchor here + pc->ea = anchor; + if (anchor) { + p = anchor->dp; + } + } + pc->state = PenTool::CONTROL; + ret = TRUE; + break; + case PenTool::CONTROL: + // End current segment + spdc_endpoint_snap(pc, p, revent.state); + spdc_pen_finish_segment(pc, p, revent.state); + pc->state = PenTool::POINT; + ret = TRUE; + break; + case PenTool::CLOSE: + // End current segment + if (!anchor) { // Snap node only if not hitting anchor + spdc_endpoint_snap(pc, p, revent.state); + } + spdc_pen_finish_segment(pc, p, revent.state); + spdc_pen_finish(pc, TRUE); + pc->state = PenTool::POINT; + ret = TRUE; + break; + case PenTool::STOP: + // This is allowed, if we just canceled curve + pc->state = PenTool::POINT; + ret = TRUE; + break; + default: + break; + } + break; + case PenTool::MODE_DRAG: + switch (pc->state) { + case PenTool::POINT: + case PenTool::CONTROL: + spdc_endpoint_snap(pc, p, revent.state); + spdc_pen_finish_segment(pc, p, revent.state); + break; + case PenTool::CLOSE: + spdc_endpoint_snap(pc, p, revent.state); + spdc_pen_finish_segment(pc, p, revent.state); + if (pc->green_closed) { + // finishing at the start anchor, close curve + spdc_pen_finish(pc, TRUE); + } else { + // finishing at some other anchor, finish curve but not close + spdc_pen_finish(pc, FALSE); + } + break; + case PenTool::STOP: + // This is allowed, if we just cancelled curve + break; + default: + break; + } + pc->state = PenTool::POINT; + ret = TRUE; + break; + default: + break; + } + + if (pc->grab) { + // Release grab now + sp_canvas_item_ungrab(pc->grab, revent.time); + pc->grab = NULL; + } + + ret = TRUE; + + dc->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 (pc->expecting_clicks_for_LPE == 0 && sp_pen_context_has_waiting_LPE(pc)) { + sp_pen_context_set_polyline_mode(pc); + + ToolBase *ec = SP_EVENT_CONTEXT(pc); + Inkscape::Selection *selection = sp_desktop_selection (ec->desktop); + + if (pc->waiting_LPE) { + // we have an already created LPE waiting for a path + pc->waiting_LPE->acceptParamPath(SP_PATH(selection->singleItem())); + selection->add(SP_OBJECT(pc->waiting_item)); + pc->waiting_LPE = NULL; + } 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; +} + +static gint pen_handle_2button_press(PenTool *const pc, GdkEventButton const &bevent) +{ + gint ret = FALSE; + // only end on LMB double click. Otherwise horizontal scrolling causes ending of the path + if (pc->npoints != 0 && bevent.button == 1) { + spdc_pen_finish(pc, FALSE); + ret = TRUE; + } + return ret; +} + +static void pen_redraw_all (PenTool *const pc) +{ + // green + if (pc->green_bpaths) { + // remove old piecewise green canvasitems + while (pc->green_bpaths) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(pc->green_bpaths->data)); + pc->green_bpaths = g_slist_remove(pc->green_bpaths, pc->green_bpaths->data); + } + // one canvas bpath for all of green_curve + SPCanvasItem *cshape = sp_canvas_bpath_new(sp_desktop_sketch(pc->desktop), pc->green_curve); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cshape), pc->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(cshape), 0, SP_WIND_RULE_NONZERO); + + pc->green_bpaths = g_slist_prepend(pc->green_bpaths, cshape); + } + + if (pc->green_anchor) + SP_CTRL(pc->green_anchor->ctrl)->moveto(pc->green_anchor->dp); + + pc->red_curve->reset(); + pc->red_curve->moveto(pc->p[0]); + pc->red_curve->curveto(pc->p[1], pc->p[2], pc->p[3]); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), pc->red_curve); + + // handles + if (pc->p[0] != pc->p[1]) { + SP_CTRL(pc->c1)->moveto(pc->p[1]); + pc->cl1->setCoords(pc->p[0], pc->p[1]); + sp_canvas_item_show(pc->c1); + sp_canvas_item_show(pc->cl1); + } else { + sp_canvas_item_hide(pc->c1); + sp_canvas_item_hide(pc->cl1); + } + + Geom::Curve const * last_seg = pc->green_curve->last_segment(); + if (last_seg) { + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>( last_seg ); + if ( cubic && + (*cubic)[2] != pc->p[0] ) + { + Geom::Point p2 = (*cubic)[2]; + SP_CTRL(pc->c0)->moveto(p2); + pc->cl0->setCoords(p2, pc->p[0]); + sp_canvas_item_show(pc->c0); + sp_canvas_item_show(pc->cl0); + } else { + sp_canvas_item_hide(pc->c0); + sp_canvas_item_hide(pc->cl0); + } + } +} + +static void pen_lastpoint_move (PenTool *const pc, gdouble x, gdouble y) +{ + if (pc->npoints != 5) + return; + + // green + if (!pc->green_curve->is_empty()) { + pc->green_curve->last_point_additive_move( Geom::Point(x,y) ); + } else { + // start anchor too + if (pc->green_anchor) { + pc->green_anchor->dp += Geom::Point(x, y); + } + } + + // red + pc->p[0] += Geom::Point(x, y); + pc->p[1] += Geom::Point(x, y); + pen_redraw_all(pc); +} + +static void pen_lastpoint_move_screen (PenTool *const pc, gdouble x, gdouble y) +{ + pen_lastpoint_move (pc, x / pc->desktop->current_zoom(), y / pc->desktop->current_zoom()); +} + +static void pen_lastpoint_tocurve (PenTool *const pc) +{ + if (pc->npoints != 5) + return; + + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>( pc->green_curve->last_segment() ); + if ( cubic ) { + pc->p[1] = pc->p[0] + (Geom::Point)( (*cubic)[3] - (*cubic)[2] ); + } else { + pc->p[1] = pc->p[0] + (1./3)*(pc->p[3] - pc->p[0]); + } + + pen_redraw_all(pc); +} + +static void pen_lastpoint_toline (PenTool *const pc) +{ + if (pc->npoints != 5) + return; + + pc->p[1] = pc->p[0]; + + pen_redraw_all(pc); +} + + +static gint pen_handle_key_press(PenTool *const pc, GdkEvent *event) +{ + + gint ret = FALSE; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px + + switch (get_group0_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)) pen_lastpoint_move_screen(pc, -10, 0); // shift + else pen_lastpoint_move_screen(pc, -1, 0); // no shift + } + else { // no alt + if (MOD__SHIFT(event)) pen_lastpoint_move(pc, -10*nudge, 0); // shift + else pen_lastpoint_move(pc, -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)) pen_lastpoint_move_screen(pc, 0, 10); // shift + else pen_lastpoint_move_screen(pc, 0, 1); // no shift + } + else { // no alt + if (MOD__SHIFT(event)) pen_lastpoint_move(pc, 0, 10*nudge); // shift + else pen_lastpoint_move(pc, 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)) pen_lastpoint_move_screen(pc, 10, 0); // shift + else pen_lastpoint_move_screen(pc, 1, 0); // no shift + } + else { // no alt + if (MOD__SHIFT(event)) pen_lastpoint_move(pc, 10*nudge, 0); // shift + else pen_lastpoint_move(pc, 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)) pen_lastpoint_move_screen(pc, 0, -10); // shift + else pen_lastpoint_move_screen(pc, 0, -1); // no shift + } + else { // no alt + if (MOD__SHIFT(event)) pen_lastpoint_move(pc, 0, -10*nudge); // shift + else pen_lastpoint_move(pc, 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)) { + pen_lastpoint_tocurve(pc); + ret = TRUE; + } + break; + case GDK_KEY_L: + case GDK_KEY_l: + if (MOD__SHIFT_ONLY(event)) { + pen_lastpoint_toline(pc); + ret = TRUE; + } + break; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (pc->npoints != 0) { + spdc_pen_finish(pc, FALSE); + ret = TRUE; + } + break; + case GDK_KEY_Escape: + if (pc->npoints != 0) { + // if drawing, cancel, otherwise pass it up for deselecting + pen_cancel (pc); + ret = TRUE; + } + break; + case GDK_KEY_z: + case GDK_KEY_Z: + if (MOD__CTRL_ONLY(event) && pc->npoints != 0) { + // if drawing, cancel, otherwise pass it up for undo + pen_cancel (pc); + ret = TRUE; + } + break; + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + sp_selection_to_guides(SP_EVENT_CONTEXT(pc)->desktop); + ret = true; + } + break; + case GDK_KEY_BackSpace: + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + if ( pc->green_curve->is_empty() || (pc->green_curve->last_segment() == NULL) ) { + if (!pc->red_curve->is_empty()) { + pen_cancel (pc); + ret = TRUE; + } else { + // do nothing; this event should be handled upstream + } + } else { + // Reset red curve + pc->red_curve->reset(); + // Destroy topmost green bpath + if (pc->green_bpaths) { + if (pc->green_bpaths->data) + sp_canvas_item_destroy(SP_CANVAS_ITEM(pc->green_bpaths->data)); + pc->green_bpaths = g_slist_remove(pc->green_bpaths, pc->green_bpaths->data); + } + // Get last segment + if ( pc->green_curve->is_empty() ) { + g_warning("pen_handle_key_press, case GDK_KP_Delete: Green curve is empty"); + break; + } + // The code below assumes that pc->green_curve has only ONE path ! + Geom::Curve const * crv = pc->green_curve->last_segment(); + pc->p[0] = crv->initialPoint(); + if ( Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>(crv)) { + pc->p[1] = (*cubic)[1]; + } else { + pc->p[1] = pc->p[0]; + } + Geom::Point const pt(( pc->npoints < 4 + ? (Geom::Point)(crv->finalPoint()) + : pc->p[3] )); + pc->npoints = 2; + pc->green_curve->backspace(); + sp_canvas_item_hide(pc->c0); + sp_canvas_item_hide(pc->c1); + sp_canvas_item_hide(pc->cl0); + sp_canvas_item_hide(pc->cl1); + pc->state = PenTool::POINT; + spdc_pen_set_subsequent_point(pc, pt, true); + pen_last_paraxial_dir = !pen_last_paraxial_dir; + ret = TRUE; + } + break; + default: + break; + } + return ret; +} + +static void spdc_reset_colors(PenTool *pc) +{ + // Red + pc->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), NULL); + // Blue + pc->blue_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->blue_bpath), NULL); + // Green + while (pc->green_bpaths) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(pc->green_bpaths->data)); + pc->green_bpaths = g_slist_remove(pc->green_bpaths, pc->green_bpaths->data); + } + pc->green_curve->reset(); + if (pc->green_anchor) { + pc->green_anchor = sp_draw_anchor_destroy(pc->green_anchor); + } + pc->sa = NULL; + pc->ea = NULL; + pc->npoints = 0; + pc->red_curve_is_valid = false; +} + + +static void spdc_pen_set_initial_point(PenTool *const pc, Geom::Point const p) +{ + g_assert( pc->npoints == 0 ); + + pc->p[0] = p; + pc->p[1] = p; + pc->npoints = 2; + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), NULL); + + pc->desktop->canvas->forceFullRedrawAfterInterruptions(5); +} + +/** + * 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"). + */ +static void spdc_pen_set_angle_distance_status_message(PenTool *const pc, Geom::Point const p, int pc_point_to_compare, gchar const *message) +{ + g_assert(pc != NULL); + g_assert((pc_point_to_compare == 0) || (pc_point_to_compare == 3)); // exclude control handles + g_assert(message != NULL); + + SPDesktop *desktop = SP_EVENT_CONTEXT(pc)->desktop; + Geom::Point rel = p - pc->p[pc_point_to_compare]; + Inkscape::Util::Quantity q = Inkscape::Util::Quantity(Geom::L2(rel), "px"); + GString *dist = g_string_new(q.string(desktop->namedview->doc_units).c_str()); + double angle = atan2(rel[Geom::Y], rel[Geom::X]) * 180 / M_PI; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/compassangledisplay/value", 0) != 0) { + angle = 90 - angle; + if (angle < 0) { + angle += 360; + } + } + + pc->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, message, angle, dist->str); + g_string_free(dist, FALSE); +} + +static void spdc_pen_set_subsequent_point(PenTool *const pc, Geom::Point const p, bool statusbar, guint status) +{ + g_assert( pc->npoints != 0 ); + // todo: Check callers to see whether 2 <= npoints is guaranteed. + + pc->p[2] = p; + pc->p[3] = p; + pc->p[4] = p; + pc->npoints = 5; + pc->red_curve->reset(); + bool is_curve; + pc->red_curve->moveto(pc->p[0]); + if (pc->polylines_paraxial && !statusbar) { + // we are drawing horizontal/vertical lines and hit an anchor; + Geom::Point const origin = pc->p[0]; + // if the previous point and the anchor are not aligned either horizontally or vertically... + if ((abs(p[Geom::X] - origin[Geom::X]) > 1e-9) && (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; + pen_set_to_nearest_horiz_vert(pc, intermed, status, false); + pc->red_curve->lineto(intermed); + } + pc->red_curve->lineto(p); + is_curve = false; + } else { + // one of the 'regular' modes + if (pc->p[1] != pc->p[0]) { + pc->red_curve->curveto(pc->p[1], p, p); + is_curve = true; + } else { + pc->red_curve->lineto(p); + is_curve = false; + } + } + + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), pc->red_curve); + + if (statusbar) { + gchar *message = is_curve ? + _("<b>Curve segment</b>: angle %3.2f°, distance %s; with <b>Ctrl</b> to snap angle, <b>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> to finish the path"); + spdc_pen_set_angle_distance_status_message(pc, p, 0, message); + } +} + +static void spdc_pen_set_ctrl(PenTool *const pc, Geom::Point const p, guint const state) +{ + sp_canvas_item_show(pc->c1); + sp_canvas_item_show(pc->cl1); + + if ( pc->npoints == 2 ) { + pc->p[1] = p; + sp_canvas_item_hide(pc->c0); + sp_canvas_item_hide(pc->cl0); + SP_CTRL(pc->c1)->moveto(pc->p[1]); + pc->cl1->setCoords(pc->p[0], pc->p[1]); + + spdc_pen_set_angle_distance_status_message(pc, p, 0, _("<b>Curve handle</b>: angle %3.2f°, length %s; with <b>Ctrl</b> to snap angle")); + } else if ( pc->npoints == 5 ) { + pc->p[4] = p; + sp_canvas_item_show(pc->c0); + sp_canvas_item_show(pc->cl0); + bool is_symm = false; + if ( ( ( pc->mode == PenTool::MODE_CLICK ) && ( state & GDK_CONTROL_MASK ) ) || + ( ( pc->mode == PenTool::MODE_DRAG ) && !( state & GDK_SHIFT_MASK ) ) ) { + Geom::Point delta = p - pc->p[3]; + pc->p[2] = pc->p[3] - delta; + is_symm = true; + pc->red_curve->reset(); + pc->red_curve->moveto(pc->p[0]); + pc->red_curve->curveto(pc->p[1], pc->p[2], pc->p[3]); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), pc->red_curve); + } + SP_CTRL(pc->c0)->moveto(pc->p[2]); + pc->cl0 ->setCoords(pc->p[3], pc->p[2]); + SP_CTRL(pc->c1)->moveto(pc->p[4]); + pc->cl1->setCoords(pc->p[3], pc->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"); + spdc_pen_set_angle_distance_status_message(pc, p, 3, message); + } else { + g_warning("Something bad happened - npoints is %d", pc->npoints); + } +} + +static void spdc_pen_finish_segment(PenTool *const pc, Geom::Point const p, guint const state) +{ + if (pc->polylines_paraxial) { + pen_last_paraxial_dir = pen_next_paraxial_direction(pc, p, pc->p[0], state); + } + + ++pc->num_clicks; + + if (!pc->red_curve->is_empty()) { + pc->green_curve->append_continuous(pc->red_curve, 0.0625); + SPCurve *curve = pc->red_curve->copy(); + /// \todo fixme: + SPCanvasItem *cshape = sp_canvas_bpath_new(sp_desktop_sketch(pc->desktop), curve); + curve->unref(); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cshape), pc->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + pc->green_bpaths = g_slist_prepend(pc->green_bpaths, cshape); + + pc->p[0] = pc->p[3]; + pc->p[1] = pc->p[4]; + pc->npoints = 2; + + pc->red_curve->reset(); + } +} + +static void spdc_pen_finish(PenTool *const pc, gboolean const closed) +{ + if (pc->expecting_clicks_for_LPE > 1) { + // don't let the path be finished before we have collected the required number of mouse clicks + return; + } + + pc->num_clicks = 0; + + pen_disable_events(pc); + + SPDesktop *const desktop = pc->desktop; + pc->message_context->clear(); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Drawing finished")); + + pc->red_curve->reset(); + spdc_concat_colors_and_flush(pc, closed); + pc->sa = NULL; + pc->ea = NULL; + + pc->npoints = 0; + pc->state = PenTool::POINT; + + sp_canvas_item_hide(pc->c0); + sp_canvas_item_hide(pc->c1); + sp_canvas_item_hide(pc->cl0); + sp_canvas_item_hide(pc->cl1); + + if (pc->green_anchor) { + pc->green_anchor = sp_draw_anchor_destroy(pc->green_anchor); + } + + + pc->desktop->canvas->endForcedFullRedraws(); + + pen_enable_events(pc); +} + +static void pen_disable_events(PenTool *const pc) { + pc->events_disabled++; +} + +static void pen_enable_events(PenTool *const pc) { + g_return_if_fail(pc->events_disabled != 0); + + pc->events_disabled--; +} + +void sp_pen_context_wait_for_LPE_mouse_clicks(PenTool *pc, Inkscape::LivePathEffect::EffectType effect_type, + unsigned int num_clicks, bool use_polylines) +{ + if (effect_type == Inkscape::LivePathEffect::INVALID_LPE) + return; + + pc->waiting_LPE_type = effect_type; + pc->expecting_clicks_for_LPE = num_clicks; + pc->polylines_only = use_polylines; + pc->polylines_paraxial = false; // TODO: think if this is correct for all cases +} + +void sp_pen_context_cancel_waiting_for_LPE(PenTool *pc) +{ + pc->waiting_LPE_type = Inkscape::LivePathEffect::INVALID_LPE; + pc->expecting_clicks_for_LPE = 0; + sp_pen_context_set_polyline_mode(pc); +} + +static int pen_next_paraxial_direction(const PenTool *const pc, + 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 (pc->num_clicks == 0) { + + if (pc->green_curve->is_empty()) { + // first mouse click + double dist_h = fabs(pt[Geom::X] - origin[Geom::X]); + double dist_v = fabs(pt[Geom::Y] - origin[Geom::Y]); + int ret = (dist_h < dist_v) ? 1 : 0; // 0 = horizontal, 1 = vertical + pen_last_paraxial_dir = (state & GDK_SHIFT_MASK) ? 1 - ret : ret; + return pen_last_paraxial_dir; + } else { + // subsequent mouse click + return (state & GDK_SHIFT_MASK) ? pen_last_paraxial_dir : 1 - pen_last_paraxial_dir; + } +} + +void pen_set_to_nearest_horiz_vert(const PenTool *const pc, Geom::Point &pt, guint const state, bool snap) +{ + Geom::Point const origin = pc->p[0]; + + int next_dir = pen_next_paraxial_direction(pc, pt, origin, state); + + if (!snap) { + if (next_dir == 0) { + // line is forced to be horizontal + pt[Geom::Y] = origin[Geom::Y]; + } else { + // line is forced to be vertical + pt[Geom::X] = origin[Geom::X]; + } + } else { + // Create a horizontal or vertical constraint line + Inkscape::Snapper::SnapConstraint cl(origin, next_dir ? Geom::Point(0, 1) : Geom::Point(1, 0)); + + // Snap along the constraint line; if we didn't snap then still the constraint will be applied + SnapManager &m = pc->desktop->namedview->snap_manager; + + Inkscape::Selection *selection = sp_desktop_selection (pc->desktop); + // 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(pc->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 000000000..4452dbd68 --- /dev/null +++ b/src/ui/tools/pen-tool.h @@ -0,0 +1,105 @@ +#ifndef SEEN_PEN_CONTEXT_H +#define SEEN_PEN_CONTEXT_H + +/** \file + * PenTool: a context for pen tool events. + */ + +#include "ui/tools/freehand-base.h" +#include "live_effects/effect.h" + +#define SP_PEN_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PenTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_PEN_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::PenTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +struct SPCtrlLine; + +namespace Inkscape { +namespace UI { +namespace Tools { + +/** + * PenTool: a context for pen tool events. + */ +class PenTool : public FreehandBase { +public: + PenTool(); + virtual ~PenTool(); + + enum Mode { + MODE_CLICK, + MODE_DRAG + }; + + enum State { + POINT, + CONTROL, + CLOSE, + STOP + }; + + Geom::Point p[5]; + + /** \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; + + Mode mode; + State state; + + bool polylines_only; + bool polylines_paraxial; + int num_clicks; + + unsigned int expecting_clicks_for_LPE; // if positive, finish the path after this many clicks + Inkscape::LivePathEffect::Effect *waiting_LPE; // if NULL, waiting_LPE_type in SPDrawContext is taken into account + SPLPEItem *waiting_item; + + SPCanvasItem *c0; + SPCanvasItem *c1; + + SPCtrlLine *cl0; + SPCtrlLine *cl1; + + unsigned int events_disabled : 1; + + static const std::string prefsPath; + + virtual const std::string& getPrefsPath(); + +protected: + virtual void setup(); + virtual void finish(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); + virtual bool item_handler(SPItem* item, GdkEvent* event); +}; + +inline bool sp_pen_context_has_waiting_LPE(PenTool *pc) { + // note: waiting_LPE_type is defined in SPDrawContext + return (pc->waiting_LPE != NULL || + pc->waiting_LPE_type != Inkscape::LivePathEffect::INVALID_LPE); +} + +void sp_pen_context_set_polyline_mode(PenTool *const pc); +void sp_pen_context_wait_for_LPE_mouse_clicks(PenTool *pc, Inkscape::LivePathEffect::EffectType effect_type, + unsigned int num_clicks, bool use_polylines = true); +void sp_pen_context_cancel_waiting_for_LPE(PenTool *pc); +void sp_pen_context_put_into_waiting_mode(SPDesktop *desktop, Inkscape::LivePathEffect::EffectType effect_type, + unsigned int num_clicks, bool use_polylines = true); + +} +} +} + +#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 000000000..7edf5d8ff --- /dev/null +++ b/src/ui/tools/pencil-tool.cpp @@ -0,0 +1,916 @@ +/** \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, read the file 'COPYING' for more information + */ + +#include <gdk/gdkkeysyms.h> + +#include "ui/tools/pencil-tool.h" +#include "desktop.h" +#include "desktop-handles.h" +#include "selection.h" +#include "selection-chemistry.h" +#include "draw-anchor.h" +#include "message-stack.h" +#include "message-context.h" +#include "modifier-fns.h" +#include "sp-path.h" +#include "preferences.h" +#include "snap.h" +#include "pixmaps/cursor-pencil.xpm" +#include <2geom/sbasis-to-bezier.h> +#include <2geom/bezier-utils.h> +#include "display/canvas-bpath.h" +#include <glibmm/i18n.h> +#include "context-fns.h" +#include "sp-namedview.h" +#include "xml/repr.h" +#include "document.h" +#include "desktop-style.h" +#include "macros.h" +#include "display/sp-canvas.h" +#include "display/curve.h" +#include "livarot/Path.h" +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +static gint pencil_handle_button_press(PencilTool *const pc, GdkEventButton const &bevent); +static gint pencil_handle_motion_notify(PencilTool *const pc, GdkEventMotion const &mevent); +static gint pencil_handle_button_release(PencilTool *const pc, GdkEventButton const &revent); +static gint pencil_handle_key_press(PencilTool *const pc, guint const keyval, guint const state); +static gint pencil_handle_key_release(PencilTool *const pc, guint const keyval, guint const state); + +static void spdc_set_startpoint(PencilTool *pc, Geom::Point const &p); +static void spdc_set_endpoint(PencilTool *pc, Geom::Point const &p); +static void spdc_finish_endpoint(PencilTool *pc); +static void spdc_add_freehand_point(PencilTool *pc, Geom::Point const &p, guint state); +static void fit_and_split(PencilTool *pc); +static void interpolate(PencilTool *pc); +static void sketch_interpolate(PencilTool *pc); + +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; } + +namespace { + ToolBase* createPencilContext() { + return new PencilTool(); + } + + bool pencilContextRegistered = ToolFactory::instance().registerObject("/tools/freehand/pencil", createPencilContext); +} + +const std::string& PencilTool::getPrefsPath() { + return PencilTool::prefsPath; +} + +const std::string PencilTool::prefsPath = "/tools/freehand/pencil"; + +PencilTool::PencilTool() : + FreehandBase(), + p(), + npoints(0), + state(SP_PENCIL_CONTEXT_IDLE), + req_tangent(0,0), + is_drawing(false), + ps(), + sketch_interpolation(Geom::Piecewise<Geom::D2<Geom::SBasis> >())// since PencilTool is not properly constructed... +{ + this->cursor_shape = cursor_pencil_xpm; + this->hot_x = 4; + this->hot_y = 4; + this->sketch_n = 0; +} + +void PencilTool::setup() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/freehand/pencil/selcue")) { + this->enableSelectionCue(); + } + + FreehandBase::setup(); + + this->is_drawing = false; + this->anchor_statusbar = false; +} + +PencilTool::~PencilTool() { +} + +/** Snaps new node relative to the previous node. */ +static void +spdc_endpoint_snap(PencilTool const *pc, Geom::Point &p, guint const state) +{ + if ((state & GDK_CONTROL_MASK)) { //CTRL enables constrained snapping + if (pc->npoints > 0) { + spdc_endpoint_snap_rotation(pc, p, pc->p[0], state); + } + } else { + if (!(state & GDK_SHIFT_MASK)) { //SHIFT disables all snapping, except the angular snapping above + //After all, the user explicitely asked for angular snapping by + //pressing CTRL + boost::optional<Geom::Point> origin = pc->npoints > 0 ? pc->p[0] : boost::optional<Geom::Point>(); + spdc_endpoint_snap_free(pc, p, origin, state); + } + } +} + +/** + * Callback for handling all pencil context events. + */ +bool PencilTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = pencil_handle_button_press(this, event->button); + break; + + case GDK_MOTION_NOTIFY: + ret = pencil_handle_motion_notify(this, event->motion); + break; + + case GDK_BUTTON_RELEASE: + ret = pencil_handle_button_release(this, event->button); + break; + + case GDK_KEY_PRESS: + ret = pencil_handle_key_press(this, get_group0_keyval (&event->key), event->key.state); + break; + + case GDK_KEY_RELEASE: + ret = pencil_handle_key_release(this, get_group0_keyval (&event->key), event->key.state); + break; + + default: + break; + } + + if (!ret) { + ret = FreehandBase::root_handler(event); + } + + return ret; +} + +static gint +pencil_handle_button_press(PencilTool *const pc, GdkEventButton const &bevent) +{ + gint ret = FALSE; + ToolBase *event_context = SP_EVENT_CONTEXT(pc); + if ( bevent.button == 1 && !event_context->space_panning) { + + FreehandBase *dc = SP_DRAW_CONTEXT (pc); + SPDesktop *desktop = SP_EVENT_CONTEXT_DESKTOP(dc); + Inkscape::Selection *selection = sp_desktop_selection(desktop); + + if (Inkscape::have_viable_layer(desktop, dc->message_context) == false) { + return TRUE; + } + + if (!pc->grab) { + /* Grab mouse, so release will not pass unnoticed */ + pc->grab = SP_CANVAS_ITEM(desktop->acetate); + sp_canvas_item_grab(pc->grab, ( GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK ), + NULL, bevent.time); + } + + Geom::Point const button_w(bevent.x, bevent.y); + + /* Find desktop coordinates */ + Geom::Point p = pc->desktop->w2d(button_w); + + /* Test whether we hit any anchor. */ + SPDrawAnchor *anchor = spdc_test_inside(pc, button_w); + + pencil_drag_origin_w = Geom::Point(bevent.x,bevent.y); + pencil_within_tolerance = true; + + switch (pc->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); + if (!(bevent.state & GDK_SHIFT_MASK)) { + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + } + spdc_create_single_dot(event_context, p, "/tools/freehand/pencil", bevent.state); + m.unSetup(); + ret = true; + break; + } + if (anchor) { + p = anchor->dp; + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Continuing selected path")); + } else { + m.setup(desktop); + if (!(bevent.state & GDK_SHIFT_MASK)) { + // This is the first click of a new curve; deselect item so that + // this curve is not combined with it (unless it is drawn from its + // anchor, which is handled by the sibling branch above) + selection->clear(); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path")); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + } else if (selection->singleItem() && SP_IS_PATH(selection->singleItem())) { + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path")); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + } + m.unSetup(); + } + pc->sa = anchor; + spdc_set_startpoint(pc, p); + ret = TRUE; + break; + } + + pc->is_drawing = true; + } + return ret; +} + +static gint +pencil_handle_motion_notify(PencilTool *const pc, GdkEventMotion const &mevent) +{ + SPDesktop *const dt = pc->desktop; + + 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 + pc->is_drawing = false; + return TRUE; + } + gint ret = FALSE; + + ToolBase *event_context = SP_EVENT_CONTEXT(pc); + if (event_context->space_panning || mevent.state & GDK_BUTTON2_MASK || mevent.state & GDK_BUTTON3_MASK) { + // allow scrolling + return FALSE; + } + + if ( ( mevent.state & GDK_BUTTON1_MASK ) && !pc->grab && pc->is_drawing) { + /* Grab mouse, so release will not pass unnoticed */ + pc->grab = SP_CANVAS_ITEM(dt->acetate); + sp_canvas_item_grab(pc->grab, ( GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK ), + NULL, mevent.time); + } + + /* Find desktop coordinates */ + Geom::Point p = dt->w2d(Geom::Point(mevent.x, mevent.y)); + + /* Test whether we hit any anchor. */ + SPDrawAnchor *anchor = spdc_test_inside(pc, Geom::Point(mevent.x, mevent.y)); + + if (pencil_within_tolerance) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + 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; + + switch (pc->state) { + case SP_PENCIL_CONTEXT_ADDLINE: + /* Set red endpoint */ + if (anchor) { + p = anchor->dp; + } else { + Geom::Point ptnr(p); + spdc_endpoint_snap(pc, ptnr, mevent.state); + p = ptnr; + } + spdc_set_endpoint(pc, p); + ret = TRUE; + break; + default: + /* We may be idle or already freehand */ + if ( mevent.state & GDK_BUTTON1_MASK && pc->is_drawing ) { + if (pc->state == SP_PENCIL_CONTEXT_IDLE) { + sp_event_context_discard_delayed_snap_event(event_context); + } + pc->state = SP_PENCIL_CONTEXT_FREEHAND; + + if ( !pc->sa && !pc->green_anchor ) { + /* Create green anchor */ + pc->green_anchor = sp_draw_anchor_new(pc, pc->green_curve, TRUE, pc->p[0]); + } + if (anchor) { + p = anchor->dp; + } + + if ( pc->npoints != 0) { // buttonpress may have happened before we entered draw context! + if (pc->ps.empty()) { + // Only in freehand mode we have to add the first point also to pc->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 + // wheter we're going into freehand mode or not + pc->ps.push_back(pc->p[0]); + } + spdc_add_freehand_point(pc, p, mevent.state); + ret = TRUE; + } + + if (anchor && !pc->anchor_statusbar) { + pc->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Release</b> here to close and finish the path.")); + pc->anchor_statusbar = true; + } else if (!anchor && pc->anchor_statusbar) { + pc->message_context->clear(); + pc->anchor_statusbar = false; + } else if (!anchor) { + pc->message_context->set(Inkscape::NORMAL_MESSAGE, _("Drawing a freehand path")); + } + + } else { + if (anchor && !pc->anchor_statusbar) { + pc->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drag</b> to continue the path from this point.")); + pc->anchor_statusbar = true; + } else if (!anchor && pc->anchor_statusbar) { + pc->message_context->clear(); + pc->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 (!sp_event_context_knot_mouseover(pc)) { + SnapManager &m = dt->namedview->snap_manager; + m.setup(dt); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + } + return ret; +} + +static gint +pencil_handle_button_release(PencilTool *const pc, GdkEventButton const &revent) +{ + gint ret = FALSE; + + ToolBase *event_context = SP_EVENT_CONTEXT(pc); + if ( revent.button == 1 && pc->is_drawing && !event_context->space_panning) { + SPDesktop *const dt = pc->desktop; + + pc->is_drawing = false; + + /* Find desktop coordinates */ + Geom::Point p = dt->w2d(Geom::Point(revent.x, revent.y)); + + /* Test whether we hit any anchor. */ + SPDrawAnchor *anchor = spdc_test_inside(pc, Geom::Point(revent.x, + revent.y)); + + switch (pc->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)) { + // Ctrl+click creates a single point so only set context in ADDLINE mode when Ctrl isn't pressed + pc->state = SP_PENCIL_CONTEXT_ADDLINE; + } + ret = TRUE; + break; + case SP_PENCIL_CONTEXT_ADDLINE: + /* Finish segment now */ + if (anchor) { + p = anchor->dp; + } else { + spdc_endpoint_snap(pc, p, revent.state); + } + pc->ea = anchor; + spdc_set_endpoint(pc, p); + spdc_finish_endpoint(pc); + pc->state = SP_PENCIL_CONTEXT_IDLE; + sp_event_context_discard_delayed_snap_event(event_context); + ret = TRUE; + break; + case SP_PENCIL_CONTEXT_FREEHAND: + if (revent.state & GDK_MOD1_MASK) { + /* sketch mode: interpolate the sketched path and improve the current output path with the new interpolation. don't finish sketch */ + + sketch_interpolate(pc); + + if (pc->green_anchor) { + pc->green_anchor = sp_draw_anchor_destroy(pc->green_anchor); + } + + pc->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; + spdc_endpoint_snap(pc, p_end, revent.state); + if (p_end != p) { + // then we must have snapped! + spdc_add_freehand_point(pc, p_end, revent.state); + } + } + + pc->ea = anchor; + /* Write curves to object */ + + dt->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing freehand")); + + interpolate(pc); + spdc_concat_colors_and_flush(pc, FALSE); + pc->sa = NULL; + pc->ea = NULL; + if (pc->green_anchor) { + pc->green_anchor = sp_draw_anchor_destroy(pc->green_anchor); + } + pc->state = SP_PENCIL_CONTEXT_IDLE; + // reset sketch mode too + pc->sketch_n = 0; + } + ret = TRUE; + break; + case SP_PENCIL_CONTEXT_SKETCH: + default: + break; + } + + if (pc->grab) { + /* Release grab now */ + sp_canvas_item_ungrab(pc->grab, revent.time); + pc->grab = NULL; + } + + ret = TRUE; + } + return ret; +} + +static void +pencil_cancel (PencilTool *const pc) +{ + if (pc->grab) { + /* Release grab now */ + sp_canvas_item_ungrab(pc->grab, 0); + pc->grab = NULL; + } + + pc->is_drawing = false; + pc->state = SP_PENCIL_CONTEXT_IDLE; + sp_event_context_discard_delayed_snap_event(SP_EVENT_CONTEXT(pc)); + + pc->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), NULL); + while (pc->green_bpaths) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(pc->green_bpaths->data)); + pc->green_bpaths = g_slist_remove(pc->green_bpaths, pc->green_bpaths->data); + } + pc->green_curve->reset(); + if (pc->green_anchor) { + pc->green_anchor = sp_draw_anchor_destroy(pc->green_anchor); + } + + pc->message_context->clear(); + pc->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled")); + + pc->desktop->canvas->endForcedFullRedraws(); +} + +static gint +pencil_handle_key_press(PencilTool *const pc, guint const keyval, guint const state) +{ + gint ret = FALSE; + switch (keyval) { + 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(state)) { + ret = TRUE; + } + break; + case GDK_KEY_Escape: + if (pc->npoints != 0) { + // if drawing, cancel, otherwise pass it up for deselecting + if (pc->state != SP_PENCIL_CONTEXT_IDLE) { + pencil_cancel (pc); + ret = TRUE; + } + } + break; + case GDK_KEY_z: + case GDK_KEY_Z: + if (mod_ctrl_only(state) && pc->npoints != 0) { + // if drawing, cancel, otherwise pass it up for undo + if (pc->state != SP_PENCIL_CONTEXT_IDLE) { + pencil_cancel (pc); + ret = TRUE; + } + } + break; + case GDK_KEY_g: + case GDK_KEY_G: + if (mod_shift_only(state)) { + sp_selection_to_guides(SP_EVENT_CONTEXT(pc)->desktop); + ret = true; + } + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Meta_L: + case GDK_KEY_Meta_R: + if (pc->state == SP_PENCIL_CONTEXT_IDLE) { + pc->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; +} + +static gint +pencil_handle_key_release(PencilTool *const pc, guint const keyval, guint const /*state*/) +{ + gint ret = FALSE; + switch (keyval) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Meta_L: + case GDK_KEY_Meta_R: + if (pc->state == SP_PENCIL_CONTEXT_SKETCH) { + spdc_concat_colors_and_flush(pc, FALSE); + pc->sketch_n = 0; + pc->sa = NULL; + pc->ea = NULL; + if (pc->green_anchor) { + pc->green_anchor = sp_draw_anchor_destroy(pc->green_anchor); + } + pc->state = SP_PENCIL_CONTEXT_IDLE; + sp_event_context_discard_delayed_snap_event(SP_EVENT_CONTEXT(pc)); + pc->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing freehand sketch")); + ret = TRUE; + } + break; + default: + break; + } + return ret; +} + +/** + * Reset points and set new starting point. + */ +static void +spdc_set_startpoint(PencilTool *const pc, Geom::Point const &p) +{ + pc->npoints = 0; + pc->red_curve_is_valid = false; + if (in_svg_plane(p)) { + pc->p[pc->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. + */ +static void +spdc_set_endpoint(PencilTool *const pc, Geom::Point const &p) +{ + if (pc->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( pc->npoints > 0 ); + + pc->red_curve->reset(); + if ( ( p == pc->p[0] ) + || !in_svg_plane(p) ) + { + pc->npoints = 1; + } else { + pc->p[1] = p; + pc->npoints = 2; + + pc->red_curve->moveto(pc->p[0]); + pc->red_curve->lineto(pc->p[1]); + pc->red_curve_is_valid = true; + + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), pc->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. + */ +static void +spdc_finish_endpoint(PencilTool *const pc) +{ + if ( ( pc->red_curve->is_empty() ) + || ( *(pc->red_curve->first_point()) == *(pc->red_curve->second_point()) ) ) + { + pc->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), NULL); + } else { + /* Write curves to object. */ + spdc_concat_colors_and_flush(pc, FALSE); + pc->sa = NULL; + pc->ea = NULL; + } +} + + +static void +spdc_add_freehand_point(PencilTool *pc, Geom::Point const &p, guint /*state*/) +{ + g_assert( pc->npoints > 0 ); + g_return_if_fail(unsigned(pc->npoints) < G_N_ELEMENTS(pc->p)); + + if ( ( p != pc->p[ pc->npoints - 1 ] ) + && in_svg_plane(p) ) + { + pc->ps.push_back(p); + pc->p[pc->npoints++] = p; + fit_and_split(pc); + } +} + +static inline double +square(double const x) +{ + return x * x; +} + +static void +interpolate(PencilTool *pc) +{ + if ( pc->ps.size() <= 1 ) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double const tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0) * 0.4; + double const tolerance_sq = 0.02 * square( pc->desktop->w2d().descrim() * + tol) * exp(0.2*tol - 2); + + g_assert(is_zero(pc->req_tangent) + || is_unit_vector(pc->req_tangent)); + Geom::Point const tHatEnd(0, 0); + + guint n_points = pc->ps.size(); + pc->green_curve->reset(); + pc->red_curve->reset(); + pc->red_curve_is_valid = false; + + Geom::Point * b = g_new(Geom::Point, 4*n_points); + Geom::Point * points = g_new(Geom::Point, 4*n_points); + for (unsigned int i = 0; i < pc->ps.size(); i++) { + points[i] = pc->ps[i]; + } + + // worst case gives us a segment per point + int max_segs = 4*n_points; + + int const n_segs = Geom::bezier_fit_cubic_r(b, points, n_points, + tolerance_sq, max_segs); + + if ( n_segs > 0) + { + /* Fit and draw and reset state */ + pc->green_curve->moveto(b[0]); + for (int c = 0; c < n_segs; c++) { + pc->green_curve->curveto(b[4*c+1], b[4*c+2], b[4*c+3]); + } + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), pc->green_curve); + + /* Fit and draw and copy last point */ + g_assert(!pc->green_curve->is_empty()); + + /* Set up direction of next curve. */ + { + Geom::Curve const * last_seg = pc->green_curve->last_segment(); + g_assert( last_seg ); // Relevance: validity of (*last_seg) + pc->p[0] = last_seg->finalPoint(); + pc->npoints = 1; + Geom::Curve *last_seg_reverse = last_seg->reverse(); + Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) ); + delete last_seg_reverse; + pc->req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) ) + ? Geom::Point(0, 0) + : Geom::unit_vector(req_vec) ); + } + } + g_free(b); + g_free(points); + pc->ps.clear(); +} + + +/* interpolates the sketched curve and tweaks the current sketch interpolation*/ +static void +sketch_interpolate(PencilTool *pc) +{ + if ( pc->ps.size() <= 1 ) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double const tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0) * 0.4; + double const tolerance_sq = 0.02 * square( pc->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(pc->req_tangent) + || is_unit_vector(pc->req_tangent)); + Geom::Point const tHatEnd(0, 0); + + guint n_points = pc->ps.size(); + pc->red_curve->reset(); + pc->red_curve_is_valid = false; + + Geom::Point * b = g_new(Geom::Point, 4*n_points); + Geom::Point * points = g_new(Geom::Point, 4*n_points); + for (unsigned i = 0; i < pc->ps.size(); i++) { + points[i] = pc->ps[i]; + } + + // worst case gives us a segment per point + int max_segs = 4*n_points; + + int const n_segs = Geom::bezier_fit_cubic_r(b, points, 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 ( pc->sketch_n > 0 ) { + double t =0.; + if (average_all_sketches) { + // Average = (sum of all) / n + // = (sum of all + new one) / n+1 + // = ((old average)*n + new one) / n+1 + t = pc->sketch_n / (pc->sketch_n + 1.); + } else { + t = 0.5; + } + pc->sketch_interpolation = Geom::lerp(t, fit_pwd2, pc->sketch_interpolation); + // simplify path, to eliminate small segments + Path *path = new Path; + path->LoadPathVector(Geom::path_from_piecewise(pc->sketch_interpolation, 0.01)); + path->Simplify(0.5); + Geom::PathVector *pathv = path->MakePathVector(); + pc->sketch_interpolation = (*pathv)[0].toPwSb(); + delete path; + delete pathv; + } else { + pc->sketch_interpolation = fit_pwd2; + } + pc->sketch_n++; + + pc->green_curve->reset(); + pc->green_curve->set_pathvector(Geom::path_from_piecewise(pc->sketch_interpolation, 0.01)); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), pc->green_curve); + + /* Fit and draw and copy last point */ + g_assert(!pc->green_curve->is_empty()); + + /* Set up direction of next curve. */ + { + Geom::Curve const * last_seg = pc->green_curve->last_segment(); + g_assert( last_seg ); // Relevance: validity of (*last_seg) + pc->p[0] = last_seg->finalPoint(); + pc->npoints = 1; + Geom::Curve *last_seg_reverse = last_seg->reverse(); + Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) ); + delete last_seg_reverse; + pc->req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) ) + ? Geom::Point(0, 0) + : Geom::unit_vector(req_vec) ); + } + } + g_free(b); + g_free(points); + pc->ps.clear(); +} + +static void +fit_and_split(PencilTool *pc) +{ + g_assert( pc->npoints > 1 ); + + double const tolerance_sq = 0; + + Geom::Point b[4]; + g_assert(is_zero(pc->req_tangent) + || is_unit_vector(pc->req_tangent)); + Geom::Point const tHatEnd(0, 0); + int const n_segs = Geom::bezier_fit_cubic_full(b, NULL, pc->p, pc->npoints, + pc->req_tangent, tHatEnd, + tolerance_sq, 1); + if ( n_segs > 0 + && unsigned(pc->npoints) < G_N_ELEMENTS(pc->p) ) + { + /* Fit and draw and reset state */ + pc->red_curve->reset(); + pc->red_curve->moveto(b[0]); + pc->red_curve->curveto(b[1], b[2], b[3]); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(pc->red_bpath), pc->red_curve); + pc->red_curve_is_valid = true; + } else { + /* Fit and draw and copy last point */ + + g_assert(!pc->red_curve->is_empty()); + + /* Set up direction of next curve. */ + { + Geom::Curve const * last_seg = pc->red_curve->last_segment(); + g_assert( last_seg ); // Relevance: validity of (*last_seg) + pc->p[0] = last_seg->finalPoint(); + pc->npoints = 1; + Geom::Curve *last_seg_reverse = last_seg->reverse(); + Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) ); + delete last_seg_reverse; + pc->req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) ) + ? Geom::Point(0, 0) + : Geom::unit_vector(req_vec) ); + } + + + pc->green_curve->append_continuous(pc->red_curve, 0.0625); + SPCurve *curve = pc->red_curve->copy(); + + /// \todo fixme: + SPCanvasItem *cshape = sp_canvas_bpath_new(sp_desktop_sketch(pc->desktop), curve); + curve->unref(); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cshape), pc->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + pc->green_bpaths = g_slist_prepend(pc->green_bpaths, cshape); + + pc->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 000000000..6ced9eb56 --- /dev/null +++ b/src/ui/tools/pencil-tool.h @@ -0,0 +1,68 @@ +#ifndef SEEN_PENCIL_CONTEXT_H +#define SEEN_PENCIL_CONTEXT_H + +/** \file + * PencilTool: a context for pencil tool events + */ + +#include "ui/tools/freehand-base.h" + +#define SP_PENCIL_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PencilTool*>((ToolBase*)obj)) +#define SP_IS_PENCIL_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::PencilTool*>((const 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(); + virtual ~PencilTool(); + + Geom::Point p[16]; + gint npoints; + PencilState state; + Geom::Point req_tangent; + + bool is_drawing; + + std::vector<Geom::Point> ps; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > sketch_interpolation; // the current proposal from the sketched paths + unsigned sketch_n; // number of sketches done + + static const std::string prefsPath; + + virtual const std::string& getPrefsPath(); + +protected: + virtual void setup(); + virtual bool root_handler(GdkEvent* event); +}; + +} +} +} + +#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 000000000..263fdea84 --- /dev/null +++ b/src/ui/tools/rect-tool.cpp @@ -0,0 +1,527 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#include "config.h" + +#include <gdk/gdkkeysyms.h> +#include <cstring> +#include <string> + +#include "macros.h" +#include "display/sp-canvas.h" +#include "sp-rect.h" +#include "document.h" +#include "document-undo.h" +#include "sp-namedview.h" +#include "selection.h" +#include "selection-chemistry.h" +#include "desktop-handles.h" +#include "snap.h" +#include "desktop.h" +#include "desktop-style.h" +#include "message-context.h" +#include "pixmaps/cursor-rect.xpm" +#include "ui/tools/rect-tool.h" +#include <glibmm/i18n.h> +#include "xml/repr.h" +#include "xml/node-event-vector.h" +#include "preferences.h" +#include "context-fns.h" +#include "shape-editor.h" +#include "verbs.h" +#include "display/sp-canvas-item.h" + +using Inkscape::DocumentUndo; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createRectContext() { + return new RectTool(); + } + + bool rectContextRegistered = ToolFactory::instance().registerObject("/tools/shapes/rect", createRectContext); +} + +const std::string& RectTool::getPrefsPath() { + return RectTool::prefsPath; +} + +const std::string RectTool::prefsPath = "/tools/shapes/rect"; + +RectTool::RectTool() : ToolBase() { + this->cursor_shape = cursor_rect_xpm; + this->hot_x = 4; + this->hot_y = 4; + this->xp = 0; + this->yp = 0; + this->tolerance = 0; + this->within_tolerance = false; + this->item_to_select = NULL; + + this->rect = NULL; + + this->rx = 0.0; + this->ry = 0.0; +} + +void RectTool::finish() { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(this->desktop->acetate), GDK_CURRENT_TIME); + + this->finishItem(); + this->sel_changed_connection.disconnect(); + + ToolBase::finish(); +} + +RectTool::~RectTool() { + this->enableGrDrag(false); + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = NULL; + + /* 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(SH_KNOTHOLDER); + this->shape_editor->set_item(selection->singleItem(), SH_KNOTHOLDER); +} + +void RectTool::setup() { + ToolBase::setup(); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = sp_desktop_selection(this->desktop)->singleItem(); + if (item) { + this->shape_editor->set_item(item, SH_KNOTHOLDER); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = sp_desktop_selection(this->desktop)->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(); + } +} + +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->space_panning) { + Inkscape::setup_for_drag_start(desktop, this, event); + ret = TRUE; + } + 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; + + SPDesktop *desktop = this->desktop; + Inkscape::Selection *selection = sp_desktop_selection (desktop); + + 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 && !this->space_panning) { + 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; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + ( GDK_KEY_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_POINTER_MOTION_HINT_MASK | + GDK_BUTTON_PRESS_MASK ), + NULL, event->button.time); + + ret = TRUE; + } + break; + case GDK_MOTION_NOTIFY: + if ( dragging + && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) + { + 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 (!sp_event_context_knot_mouseover(this)) { + 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 && !this->space_panning) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the rect + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = NULL; + ret = TRUE; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), + event->button.time); + } + break; + case GDK_KEY_PRESS: + switch (get_group0_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"), + NULL); + } + 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_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-rect"); + ret = TRUE; + } + break; + + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + sp_selection_to_guides(desktop); + ret = true; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_space: + if (dragging) { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), + event->button.time); + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + 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_group0_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) { + SPDesktop *desktop = this->desktop; + + if (!this->rect) { + if (Inkscape::have_viable_layer(desktop, this->message_context) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:rect"); + + // Set style + sp_desktop_apply_style_tool (desktop, repr, "/tools/shapes/rect", false); + + this->rect = SP_RECT(desktop->currentLayer()->appendChildRepr(repr)); + Inkscape::GC::release(repr); + + this->rect->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + this->rect->updateRepr(); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + 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"); + GString *xs = g_string_new(rdimx_q.string(desktop->namedview->doc_units).c_str()); + GString *ys = g_string_new(rdimy_q.string(desktop->namedview->doc_units).c_str()); + + 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->str, ys->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->str, ys->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->str, ys->str); + } + } + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Rectangle</b>: %s × %s; with <b>Ctrl</b> to make square or integer-ratio rectangle; with <b>Shift</b> to draw around the starting point"), xs->str, ys->str); + } + + g_string_free(xs, FALSE); + g_string_free(ys, FALSE); +} + +void RectTool::finishItem() { + this->message_context->clear(); + + if (this->rect != NULL) { + 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->getRepr(), this->rect->transform, NULL, true); + + this->desktop->canvas->endForcedFullRedraws(); + + sp_desktop_selection(this->desktop)->set(this->rect); + + DocumentUndo::done(sp_desktop_document(this->desktop), SP_VERB_CONTEXT_RECT, _("Create rectangle")); + + this->rect = NULL; + } +} + +void RectTool::cancel(){ + sp_desktop_selection(this->desktop)->clear(); + sp_canvas_item_ungrab(SP_CANVAS_ITEM(this->desktop->acetate), 0); + + if (this->rect != NULL) { + this->rect->deleteObject(); + this->rect = NULL; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = NULL; + + this->desktop->canvas->endForcedFullRedraws(); + + DocumentUndo::cancel(sp_desktop_document(this->desktop)); +} + +} +} +} + +/* + 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 000000000..a50fd7b24 --- /dev/null +++ b/src/ui/tools/rect-tool.h @@ -0,0 +1,65 @@ +#ifndef __SP_RECT_CONTEXT_H__ +#define __SP_RECT_CONTEXT_H__ + +/* + * Rectangle 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 + */ + +#include <stddef.h> +#include <sigc++/sigc++.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +#include "sp-rect.h" + +#define SP_RECT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::RectTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_RECT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::RectTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class RectTool : public ToolBase { +public: + RectTool(); + virtual ~RectTool(); + + static const std::string prefsPath; + + virtual void setup(); + virtual void finish(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); + virtual bool item_handler(SPItem* item, GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +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 000000000..498882417 --- /dev/null +++ b/src/ui/tools/select-tool.cpp @@ -0,0 +1,1252 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif +#include <cstring> +#include <string> +#include <gdk/gdkkeysyms.h> +#include "macros.h" +#include "rubberband.h" +#include "document.h" +#include "document-undo.h" +#include "selection.h" +#include "sp-cursor.h" +#include "style.h" +#include "pixmaps/cursor-select-m.xpm" +#include "pixmaps/cursor-select-d.xpm" +#include "pixmaps/handles.xpm" +#include <glibmm/i18n.h> + +#include "ui/tools/select-tool.h" +#include "selection-chemistry.h" +#ifdef WITH_DBUS +#include "extension/dbus/document-interface.h" +#endif +#include "desktop.h" +#include "desktop-handles.h" +#include "sp-root.h" +#include "preferences.h" +#include "tools-switch.h" +#include "message-stack.h" +#include "selection-describer.h" +#include "seltrans.h" +#include "box3d.h" +#include "display/sp-canvas.h" +#include "display/sp-canvas-item.h" +#include "display/drawing-item.h" +#include "tool-factory.h" + +using Inkscape::DocumentUndo; + +GdkPixbuf *handles[13]; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static GdkCursor *CursorSelectMouseover = NULL; +static GdkCursor *CursorSelectDragging = NULL; + +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 + +namespace { + ToolBase* createSelectContext() { + return new SelectTool(); + } + + bool selectContextRegistered = ToolFactory::instance().registerObject("/tools/select", createSelectContext); +} + +const std::string& SelectTool::getPrefsPath() { + return SelectTool::prefsPath; +} + +const std::string SelectTool::prefsPath = "/tools/select"; + + +//Creates rotated variations for handles +static void +sp_load_handles(int start, int count, char const **xpm) { + handles[start] = gdk_pixbuf_new_from_xpm_data((gchar const **)xpm); + for(int i = start + 1; i < start + count; i++) { + // We use either the original at *start or previous loop item to rotate + handles[i] = gdk_pixbuf_rotate_simple(handles[i-1], GDK_PIXBUF_ROTATE_CLOCKWISE); + } +} + +SelectTool::SelectTool() : ToolBase() { + this->grabbed = 0; + this->item = 0; + + this->dragging = FALSE; + this->moved = FALSE; + this->button_press_shift = false; + this->button_press_ctrl = false; + this->button_press_alt = false; + this->cycling_items = NULL; + this->cycling_items_cmp = NULL; + this->cycling_items_selected_before = NULL; + this->cycling_cur_item = NULL; + this->cycling_wrap = true; + this->_seltrans = NULL; + this->_describer = NULL; + + + // cursors in select context + CursorSelectMouseover = sp_cursor_new_from_xpm(cursor_select_m_xpm , 1, 1); + CursorSelectDragging = sp_cursor_new_from_xpm(cursor_select_d_xpm , 1, 1); + + // selection handles + sp_load_handles(0, 2, handle_scale_xpm); + sp_load_handles(2, 2, handle_stretch_xpm); + sp_load_handles(4, 4, handle_rotate_xpm); + sp_load_handles(8, 4, handle_skew_xpm); + sp_load_handles(12, 1, handle_center_xpm); +} + +//static gint xp = 0, yp = 0; // where drag started +//static gint tolerance = 0; +//static bool within_tolerance = false; +static bool is_cycling = false; +static bool moved_while_cycling = false; +ToolBase *prev_event_context = NULL; + + +SelectTool::~SelectTool() { + this->enableGrDrag(false); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, GDK_CURRENT_TIME); + this->grabbed = NULL; + } + + delete this->_seltrans; + this->_seltrans = NULL; + + delete this->_describer; + this->_describer = NULL; + + if (CursorSelectDragging) { +#if GTK_CHECK_VERSION(3,0,0) + g_object_unref(CursorSelectDragging); +#else + gdk_cursor_unref (CursorSelectDragging); +#endif + CursorSelectDragging = NULL; + } + + if (CursorSelectMouseover) { +#if GTK_CHECK_VERSION(3,0,0) + g_object_unref(CursorSelectMouseover); +#else + gdk_cursor_unref (CursorSelectMouseover); +#endif + CursorSelectMouseover = NULL; + } +} + +void SelectTool::setup() { + ToolBase::setup(); + + this->_describer = new Inkscape::SelectionDescriber( + desktop->selection, + desktop->messageStack(), + _("Click selection to toggle scale/rotation handles"), + _("No objects selected. Click, Shift+click, Alt+scroll mouse on top of objects, or drag around objects to select.") + ); + + 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(); + } +} + +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; + sp_event_context_discard_delayed_snap_event(this); + drag_escaped = 1; + + if (this->item) { + // only undo if the item is still valid + if (this->item->document) { + DocumentUndo::undo(sp_desktop_document(desktop)); + } + + sp_object_unref( this->item, NULL); + } else if (this->button_press_ctrl) { + // NOTE: This is a workaround to a bug. + // When the ctrl key is held, sc->item is not defined + // so in this case (only), we skip the object doc check + DocumentUndo::undo(sp_desktop_document(desktop)); + } + this->item = NULL; + + SP_EVENT_CONTEXT(this)->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; + SP_EVENT_CONTEXT(this)->defaultMessageContext()->clear(); + SP_EVENT_CONTEXT(this)->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. + */ + SPObject *const current_layer = desktop->currentLayer(); + if (current_layer) { + SPObject *const parent = current_layer->parent; + if ( parent + && ( parent->parent + || !( SP_IS_GROUP(current_layer) + && ( SPGroup::LAYER + == SP_GROUP(current_layer)->layerMode() ) ) ) ) + { + desktop->setCurrentLayer(parent); + if (SP_IS_GROUP(current_layer) && SPGroup::LAYER != SP_GROUP(current_layer)->layerMode()) + sp_desktop_selection(desktop)->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 == NULL) { + this->sp_select_context_abort(); + } + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + /* 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_shift = (event->button.state & GDK_SHIFT_MASK) ? true : false; + this->button_press_ctrl = (event->button.state & GDK_CONTROL_MASK) ? true : false; + this->button_press_alt = (event->button.state & GDK_MOD1_MASK) ? true : false; + + if (event->button.state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK)) { + // 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 + } else { + GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (sp_desktop_canvas(desktop))); + + this->dragging = TRUE; + this->moved = FALSE; + + gdk_window_set_cursor(window, CursorSelectDragging); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + + // remember the clicked item in this->item: + if (this->item) { + sp_object_unref(this->item, NULL); + this->item = NULL; + } + + this->item = sp_event_context_find_item (desktop, + Geom::Point(event->button.x, event->button.y), event->button.state & GDK_MOD1_MASK, FALSE); + sp_object_ref(this->item, NULL); + + rb_escaped = drag_escaped = 0; + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, event->button.time); + this->grabbed = NULL; + } + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->drawing), + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK, + NULL, event->button.time); + + this->grabbed = SP_CANVAS_ITEM(desktop->drawing); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + + 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_ENTER_NOTIFY: { + if (!desktop->isWaitingCursor() && !this->dragging) { + GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (sp_desktop_canvas(desktop))); + + gdk_window_set_cursor(window, CursorSelectMouseover); + } + break; + } + case GDK_LEAVE_NOTIFY: + if (!desktop->isWaitingCursor() && !this->dragging) { + GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (sp_desktop_canvas(desktop))); + + gdk_window_set_cursor(window, this->cursor); + } + break; + + case GDK_KEY_PRESS: + if (get_group0_keyval (&event->key) == GDK_KEY_space) { + if (this->dragging && this->grabbed) { + /* stamping mode: show content mode moving */ + _seltrans->stamp(); + ret = TRUE; + } + } else if (get_group0_keyval (&event->key) == GDK_KEY_Tab) { + if (this->dragging && this->grabbed) { + _seltrans->getNextClosestPoint(false); + ret = TRUE; + } + } else if (get_group0_keyval (&event->key) == GDK_KEY_ISO_Left_Tab) { + if (this->dragging && this->grabbed) { + _seltrans->getNextClosestPoint(true); + ret = TRUE; + } + } + 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, bool shift_pressed) { + if (!this->cycling_cur_item) { + return; + } + + Inkscape::DrawingItem *arenaitem; + SPItem *item = SP_ITEM(this->cycling_cur_item->data); + + // Deactivate current item + if (!g_list_find(this->cycling_items_selected_before, item) && selection->includes(item)) { + selection->remove(item); + } + + arenaitem = item->get_arenaitem(desktop->dkey); + arenaitem->setOpacity(0.3); + + // Find next item and activate it + GList *next; + if (scroll_event->direction == GDK_SCROLL_UP) { + next = this->cycling_cur_item->next; + if (next == NULL && this->cycling_wrap) + next = this->cycling_items; + } else { + next = this->cycling_cur_item->prev; + if (next == NULL && this->cycling_wrap) + next = g_list_last(this->cycling_items); + } + + if (next) { + this->cycling_cur_item = next; + item = SP_ITEM(this->cycling_cur_item->data); + } + + arenaitem = item->get_arenaitem(desktop->dkey); + arenaitem->setOpacity(1.0); + + if (shift_pressed) { + selection->add(item); + } else { + selection->set(item); + } +} + + +static void +sp_select_context_reset_opacities(ToolBase *event_context) +{ + // SPDesktop *desktop = event_context->desktop; + SelectTool *sc = SP_SELECT_CONTEXT(event_context); + Inkscape::DrawingItem *arenaitem; + for (GList *l = sc->cycling_items; l != NULL; l = g_list_next(l)) { + arenaitem = SP_ITEM(l->data)->get_arenaitem(event_context->desktop->dkey); + arenaitem->setOpacity(SP_SCALE24_TO_FLOAT(SP_ITEM(l->data)->style->opacity.value)); + } + g_list_free(sc->cycling_items); + g_list_free(sc->cycling_items_selected_before); + g_list_free(sc->cycling_items_cmp); + sc->cycling_items = NULL; + sc->cycling_items_selected_before = NULL; + sc->cycling_cur_item = NULL; + sc->cycling_items_cmp = NULL; +} + +bool SelectTool::root_handler(GdkEvent* event) { + SPItem *item = NULL; + SPItem *item_at_point = NULL, *group_at_point = NULL, *item_in_group = NULL; + gint ret = FALSE; + + Inkscape::Selection *selection = sp_desktop_selection(desktop); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // make sure we still have valid objects to move around + if (this->item && this->item->document == NULL) { + this->sp_select_context_abort(); + } + + switch (event->type) { + case GDK_2BUTTON_PRESS: + if (event->button.button == 1) { + if (!selection->isEmpty()) { + SPItem *clicked_item = static_cast<SPItem *>(selection->itemList()->data); + + if (SP_IS_GROUP(clicked_item) && !SP_IS_BOX3D(clicked_item)) { // enter group if it's not a 3D box + desktop->setCurrentLayer(reinterpret_cast<SPObject *>(clicked_item)); + sp_desktop_selection(desktop)->clear(); + this->dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + desktop->canvas->endForcedFullRedraws(); + } else { // switch tool + Geom::Point const button_pt(event->button.x, event->button.y); + Geom::Point const p(desktop->w2d(button_pt)); + tools_switch_by_item (desktop, clicked_item, p); + } + } else { + sp_select_context_up_one_layer(desktop); + } + + ret = TRUE; + } + break; + + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + // 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 (event->button.state & GDK_MOD1_MASK) { + Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH); + } + + Inkscape::Rubberband::get(desktop)->start(desktop, p); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, event->button.time); + this->grabbed = NULL; + } + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK, + NULL, event->button.time); + + this->grabbed = SP_CANVAS_ITEM(desktop->acetate); + + // remember what modifiers were on before button press + this->button_press_shift = (event->button.state & GDK_SHIFT_MASK) ? true : false; + this->button_press_ctrl = (event->button.state & GDK_CONTROL_MASK) ? true : false; + this->button_press_alt = (event->button.state & GDK_MOD1_MASK) ? true : false; + + 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 (is_cycling) + { + moved_while_cycling = true; + prev_event_context = this; + } + + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + if ((event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + 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 (this->button_press_ctrl || (this->button_press_alt && !this->button_press_shift && !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; + + GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (sp_desktop_canvas(desktop))); + + gdk_window_set_cursor(window, CursorSelectDragging); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + 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 + Inkscape::Rubberband::get(desktop)->stop(); + this->defaultMessageContext()->clear(); + + item_at_point = desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), FALSE); + + if (!item_at_point) { // if no item at this point, try at the click point (bug 1012200) + item_at_point = desktop->getItemAtPoint(Geom::Point(xp, yp), FALSE); + } + + if (item_at_point || this->moved || this->button_press_alt) { + // 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)); + + if (SP_IS_LAYER(selection->single())) { + group_at_point = SP_GROUP(selection->single()); + } + + // 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 = NULL; + } + + // 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)) + && !this->button_press_alt) { + // 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()) { + _seltrans->moveTo(p, event->button.state); + } + + desktop->scroll_to_point(p); + gobble_motion_events(GDK_BUTTON1_MASK); + ret = TRUE; + } else { + this->dragging = FALSE; + sp_event_context_discard_delayed_snap_event(this); + desktop->canvas->endForcedFullRedraws(); + } + } else { + if (Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->move(p); + + if (Inkscape::Rubberband::get(desktop)->getMode() == RUBBERBAND_MODE_TOUCHPATH) { + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw over</b> objects to select them; release <b>Alt</b> to switch to rubberband selection")); + } else { + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Drag around</b> objects to select them; press <b>Alt</b> to switch to touch selection")); + } + + gobble_motion_events(GDK_BUTTON1_MASK); + } + } + } + break; + } + case GDK_BUTTON_RELEASE: + xp = yp = 0; + + if ((event->button.button == 1) && (this->grabbed) && !this->space_panning) { + if (this->dragging) { + GdkWindow* window; + + if (this->moved) { + // item has been moved + _seltrans->ungrab(); + this->moved = FALSE; +#ifdef WITH_DBUS + dbus_send_ping(desktop, this->item); +#endif + } else if (this->item && !drag_escaped) { + // item has not been moved -> simply a click, do selecting + if (!selection->isEmpty()) { + if (event->button.state & GDK_SHIFT_MASK) { + // with shift, toggle selection + _seltrans->resetState(); + selection->toggle(this->item); + } else { + SPObject* single = selection->single(); + // without shift, increase state (i.e. toggle scale/rotation handles) + if (selection->includes(this->item)) { + _seltrans->increaseState(); + } else if (SP_IS_LAYER(single) && 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; + window = gtk_widget_get_window (GTK_WIDGET (sp_desktop_canvas(desktop))); + + gdk_window_set_cursor(window, CursorSelectMouseover); + sp_event_context_discard_delayed_snap_event(this); + desktop->canvas->endForcedFullRedraws(); + + if (this->item) { + sp_object_unref( this->item, NULL); + } + + this->item = NULL; + } else { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + + if (r->is_started() && !within_tolerance) { + // this was a rubberband drag + GSList *items = NULL; + + if (r->getMode() == RUBBERBAND_MODE_RECT) { + Geom::OptRect const b = r->getRectangle(); + items = sp_desktop_document(desktop)->getItemsInBox(desktop->dkey, *b); + } else if (r->getMode() == RUBBERBAND_MODE_TOUCHPATH) { + items = sp_desktop_document(desktop)->getItemsAtPoints(desktop->dkey, r->getPoints()); + } + + _seltrans->resetState(); + r->stop(); + this->defaultMessageContext()->clear(); + + if (event->button.state & GDK_SHIFT_MASK) { + // with shift, add to selection + selection->addList (items); + } else { + // without shift, simply select anew + selection->setList (items); + } + + g_slist_free (items); + } else { // it was just a click, or a too small rubberband + r->stop(); + + if (this->button_press_shift && !rb_escaped && !drag_escaped) { + // this was a shift+click or alt+shift+click, select what was clicked upon + this->button_press_shift = false; + + if (this->button_press_ctrl) { + // go into groups, honoring Alt + item = sp_event_context_find_item (desktop, + Geom::Point(event->button.x, event->button.y), event->button.state & GDK_MOD1_MASK, TRUE); + this->button_press_ctrl = FALSE; + } else { + // don't go into groups, honoring Alt + item = sp_event_context_find_item (desktop, + Geom::Point(event->button.x, event->button.y), event->button.state & GDK_MOD1_MASK, FALSE); + } + + if (item) { + selection->toggle(item); + item = NULL; + } + + } else if ((this->button_press_ctrl || this->button_press_alt) && !rb_escaped && !drag_escaped) { // ctrl+click, alt+click + item = sp_event_context_find_item (desktop, + Geom::Point(event->button.x, event->button.y), this->button_press_alt, this->button_press_ctrl); + + this->button_press_ctrl = FALSE; + this->button_press_alt = FALSE; + + if (item) { + if (selection->includes(item)) { + _seltrans->increaseState(); + } else { + _seltrans->resetState(); + selection->set(item); + } + + item = NULL; + } + } else { // click without shift, simply deselect, unless with Alt or something was cancelled + if (!selection->isEmpty()) { + if (!(rb_escaped) && !(drag_escaped) && !(event->button.state & GDK_MOD1_MASK)) { + selection->clear(); + } + + rb_escaped = 0; + ret = TRUE; + } + } + } + + ret = TRUE; + } + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, event->button.time); + this->grabbed = NULL; + } + + desktop->updateNow(); + } + + if (event->button.button == 1) { + Inkscape::Rubberband::get(desktop)->stop(); // might have been started in another tool! + } + + this->button_press_shift = false; + this->button_press_ctrl = false; + this->button_press_alt = false; + break; + + case GDK_SCROLL: { + GdkEventScroll *scroll_event = (GdkEventScroll*) event; + + if (scroll_event->state & GDK_MOD1_MASK) { // alt modified pressed + if (moved_while_cycling) + { + moved_while_cycling = false; + sp_select_context_reset_opacities(prev_event_context); + prev_event_context = NULL; + } + + is_cycling = true; + + bool shift_pressed = scroll_event->state & GDK_SHIFT_MASK; + + /* Rebuild list of items underneath the mouse pointer */ + Geom::Point p = desktop->d2w(desktop->point()); + SPItem *item = desktop->getItemAtPoint(p, true, NULL); + + // Save pointer to current cycle-item so that we can find it again later, in the freshly built list + SPItem *tmp_cur_item = this->cycling_cur_item ? SP_ITEM(this->cycling_cur_item->data) : NULL; + g_list_free(this->cycling_items); + this->cycling_items = NULL; + this->cycling_cur_item = NULL; + + while(item != NULL) { + this->cycling_items = g_list_append(this->cycling_items, item); + item = desktop->getItemAtPoint(p, true, item); + } + + /* Compare current item list with item list during previous scroll ... */ + GList *l1, *l2; + bool item_lists_differ = false; + + // Note that we can do an 'or' comparison in the loop because it is safe to call g_list_next with a NULL pointer. + for (l1 = this->cycling_items, l2 = this->cycling_items_cmp; l1 != NULL || l2 != NULL; l1 = g_list_next(l1), l2 = g_list_next(l2)) { + if ((l1 !=NULL && l2 == NULL) || (l1 == NULL && l2 != NULL) || (l1->data != l2->data)) { + item_lists_differ = true; + break; + } + } + + /* If list of items under mouse pointer hasn't changed ... */ + if (!item_lists_differ) { + // ... find current item in the freshly built list and continue cycling ... + // TODO: This wouldn't be necessary if cycling_cur_item pointed to an element of cycling_items_cmp instead + this->cycling_cur_item = g_list_find(this->cycling_items, tmp_cur_item); + g_assert(this->cycling_cur_item != NULL || this->cycling_items == NULL); + } else { + // ... otherwise reset opacities for outdated items ... + Inkscape::DrawingItem *arenaitem; + + for(GList *l = this->cycling_items_cmp; l != NULL; l = l->next) { + arenaitem = SP_ITEM(l->data)->get_arenaitem(desktop->dkey); + arenaitem->setOpacity(1.0); + //if (!shift_pressed && !g_list_find(this->cycling_items_selected_before, SP_ITEM(l->data)) && selection->includes(SP_ITEM(l->data))) + if (!g_list_find(this->cycling_items_selected_before, SP_ITEM(l->data)) && selection->includes(SP_ITEM(l->data))) { + selection->remove(SP_ITEM(l->data)); + } + } + + // ... clear the lists ... + g_list_free(this->cycling_items_cmp); + g_list_free(this->cycling_items_selected_before); + + this->cycling_items_cmp = NULL; + this->cycling_items_selected_before = NULL; + this->cycling_cur_item = NULL; + + // ... and rebuild them with the new items. + this->cycling_items_cmp = g_list_copy(this->cycling_items); + SPItem *item; + + for(GList *l = this->cycling_items; l != NULL; l = l->next) { + item = SP_ITEM(l->data); + arenaitem = item->get_arenaitem(desktop->dkey); + arenaitem->setOpacity(0.3); + + if (selection->includes(item)) { + // already selected items are stored separately, too + this->cycling_items_selected_before = g_list_append(this->cycling_items_selected_before, item); + } + } + + // set the current item to the bottommost one so that the cycling step below re-starts at the top + this->cycling_cur_item = g_list_last(this->cycling_items); + } + + 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, shift_pressed); + + ret = TRUE; + + GtkWindow *w =GTK_WINDOW(gtk_widget_get_toplevel( GTK_WIDGET(desktop->canvas) )); + if (w) + { + gtk_window_present(w); + gtk_widget_grab_focus (GTK_WIDGET(desktop->canvas)); + } + } + break; + } + + case GDK_KEY_PRESS: // keybindings for select context + { + { + guint keyval = get_group0_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 (!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 (alt) { + 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 { + sp_event_show_modifier_tip (this->defaultMessageContext(), event, + _("<b>Ctrl</b>: click to select in groups; drag to move hor/vert"), + _("<b>Shift</b>: click to toggle select; drag for rubberband selection"), + _("<b>Alt</b>: click to select under; scroll mouse-wheel to cycle-select; drag to move selected or select by touch")); + + // if Alt and nonempty selection, show moving cursor ("move selected"): + if (alt && !selection->isEmpty() && !desktop->isWaitingCursor()) { + GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (sp_desktop_canvas(desktop))); + + gdk_window_set_cursor(window, CursorSelectDragging); + } + //*/ + break; + } + } + + gdouble const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px + gdouble const offset = prefs->getDoubleLimited("/options/defaultscale/value", 2, 0, 1000, "px"); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + switch (get_group0_keyval (&event->key)) { + case GDK_KEY_Left: // move selection left + case GDK_KEY_KP_Left: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events( get_group0_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + sp_selection_move_screen(sp_desktop_selection(desktop), mul*-10, 0); // shift + } else { + sp_selection_move_screen(sp_desktop_selection(desktop), mul*-1, 0); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + sp_selection_move(sp_desktop_selection(desktop), mul*-10*nudge, 0); // shift + } else { + sp_selection_move(sp_desktop_selection(desktop), mul*-nudge, 0); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Up: // move selection up + case GDK_KEY_KP_Up: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + sp_selection_move_screen(sp_desktop_selection(desktop), 0, mul*10); // shift + } else { + sp_selection_move_screen(sp_desktop_selection(desktop), 0, mul*1); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + sp_selection_move(sp_desktop_selection(desktop), 0, mul*10*nudge); // shift + } else { + sp_selection_move(sp_desktop_selection(desktop), 0, mul*nudge); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Right: // move selection right + case GDK_KEY_KP_Right: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + sp_selection_move_screen(sp_desktop_selection(desktop), mul*10, 0); // shift + } else { + sp_selection_move_screen(sp_desktop_selection(desktop), mul*1, 0); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + sp_selection_move(sp_desktop_selection(desktop), mul*10*nudge, 0); // shift + } else { + sp_selection_move(sp_desktop_selection(desktop), mul*nudge, 0); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Down: // move selection down + case GDK_KEY_KP_Down: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + sp_selection_move_screen(sp_desktop_selection(desktop), 0, mul*-10); // shift + } else { + sp_selection_move_screen(sp_desktop_selection(desktop), 0, mul*-1); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + sp_selection_move(sp_desktop_selection(desktop), 0, mul*-10*nudge); // shift + } else { + sp_selection_move(sp_desktop_selection(desktop), 0, mul*-nudge); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (!this->sp_select_context_abort()) { + selection->clear(); + } + + ret = TRUE; + break; + + case GDK_KEY_a: + case GDK_KEY_A: + if (MOD__CTRL_ONLY(event)) { + sp_edit_select_all(desktop); + ret = TRUE; + } + break; + + case GDK_KEY_space: + /* stamping mode: show outline mode moving */ + /* FIXME: Is next condition ok? (lauris) */ + if (this->dragging && this->grabbed) { + _seltrans->stamp(); + ret = TRUE; + } + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx"); + ret = TRUE; + } + break; + + case GDK_KEY_bracketleft: + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + sp_selection_rotate_screen(selection, mul*1); + } else if (MOD__CTRL(event)) { + sp_selection_rotate(selection, 90); + } else if (snaps) { + sp_selection_rotate(selection, 180.0/snaps); + } + + ret = TRUE; + break; + + case GDK_KEY_bracketright: + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + sp_selection_rotate_screen(selection, -1*mul); + } else if (MOD__CTRL(event)) { + sp_selection_rotate(selection, -90); + } else if (snaps) { + sp_selection_rotate(selection, -180.0/snaps); + } + + ret = TRUE; + break; + + case GDK_KEY_less: + case GDK_KEY_comma: + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + sp_selection_scale_screen(selection, -2*mul); + } else if (MOD__CTRL(event)) { + sp_selection_scale_times(selection, 0.5); + } else { + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + sp_selection_scale(selection, -offset*mul); + } + + ret = TRUE; + break; + + case GDK_KEY_greater: + case GDK_KEY_period: + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + sp_selection_scale_screen(selection, 2*mul); + } else if (MOD__CTRL(event)) { + sp_selection_scale_times(selection, 2); + } else { + gint mul = 1 + gobble_key_events(get_group0_keyval(&event->key), 0); // with any mask + sp_selection_scale(selection, offset*mul); + } + + ret = TRUE; + break; + + case GDK_KEY_Return: + if (MOD__CTRL_ONLY(event)) { + if (selection->singleItem()) { + SPItem *clicked_item = selection->singleItem(); + + if ( SP_IS_GROUP(clicked_item) || SP_IS_BOX3D(clicked_item)) { // enter group or a 3D box + desktop->setCurrentLayer(reinterpret_cast<SPObject *>(clicked_item)); + sp_desktop_selection(desktop)->clear(); + } else { + this->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)) { + sp_selection_to_guides(desktop); + ret = true; + } + break; + + default: + break; + } + break; + } + case GDK_KEY_RELEASE: { + guint keyval = get_group0_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 (Inkscape::Rubberband::get(desktop)->is_started()) { + // if Alt then change cursor to moving cursor: + if (alt) { + Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_RECT); + } + } else { + if (alt) { // TODO: Should we have a variable like is_cycling or is it harmless to run this piece of code each time? + // quit cycle-selection and reset opacities + if (is_cycling) + { + sp_select_context_reset_opacities(this); + is_cycling = false; + } + + } + } + + } + // set cursor to default. + if (!desktop->isWaitingCursor()) { + // Do we need to reset the cursor here on key release ? + //GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (sp_desktop_canvas(desktop))); + //gdk_window_set_cursor(window, event_context->cursor); + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/tools/select-tool.h b/src/ui/tools/select-tool.h new file mode 100644 index 000000000..b26fc03bc --- /dev/null +++ b/src/ui/tools/select-tool.h @@ -0,0 +1,73 @@ +#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, read the file 'COPYING' for more information + */ + +#include "ui/tools/tool-base.h" +#include <gtk/gtk.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) + +struct SPCanvasItem; + +namespace Inkscape { + class MessageContext; + class SelTrans; + class SelectionDescriber; +} + +namespace Inkscape { +namespace UI { +namespace Tools { + +class SelectTool : public ToolBase { +public: + SelectTool(); + virtual ~SelectTool(); + + guint dragging : 1; + guint moved : 1; + bool button_press_shift; + bool button_press_ctrl; + bool button_press_alt; + + GList *cycling_items; + GList *cycling_items_cmp; + GList *cycling_items_selected_before; + GList *cycling_cur_item; + bool cycling_wrap; + + SPItem *item; + SPCanvasItem *grabbed; + Inkscape::SelTrans *_seltrans; + Inkscape::SelectionDescriber *_describer; + + static const std::string prefsPath; + + virtual void setup(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); + virtual bool item_handler(SPItem* item, GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +private: + bool sp_select_context_abort(); + void sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event, bool shift_pressed); +}; + +} +} +} + +#endif diff --git a/src/ui/tools/spiral-tool.cpp b/src/ui/tools/spiral-tool.cpp new file mode 100644 index 000000000..3199ee06e --- /dev/null +++ b/src/ui/tools/spiral-tool.cpp @@ -0,0 +1,467 @@ +/* + * 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 + */ + +#include "config.h" + +#include <gdk/gdkkeysyms.h> +#include <cstring> +#include <string> + +#include "macros.h" +#include "display/sp-canvas.h" +#include "sp-spiral.h" +#include "document.h" +#include "document-undo.h" +#include "sp-namedview.h" +#include "selection.h" +#include "desktop-handles.h" +#include "snap.h" +#include "desktop.h" +#include "desktop-style.h" +#include "message-context.h" +#include "pixmaps/cursor-spiral.xpm" +#include "ui/tools/spiral-tool.h" +#include <glibmm/i18n.h> +#include "xml/repr.h" +#include "xml/node-event-vector.h" +#include "preferences.h" +#include "context-fns.h" +#include "shape-editor.h" +#include "verbs.h" +#include "display/sp-canvas-item.h" + +using Inkscape::DocumentUndo; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createSpiralContext() { + return new SpiralTool(); + } + + bool spiralContextRegistered = ToolFactory::instance().registerObject("/tools/shapes/spiral", createSpiralContext); +} + +const std::string& SpiralTool::getPrefsPath() { + return SpiralTool::prefsPath; +} + +const std::string SpiralTool::prefsPath = "/tools/shapes/spiral"; + +SpiralTool::SpiralTool() : ToolBase() { + this->cursor_shape = cursor_spiral_xpm; + this->hot_x = 4; + this->hot_y = 4; + this->xp = 0; + this->yp = 0; + this->tolerance = 0; + this->within_tolerance = false; + this->item_to_select = NULL; + + this->spiral = NULL; + + this->revo = 3.0; + this->exp = 1.0; + this->t0 = 0.0; +} + +void SpiralTool::finish() { + SPDesktop *desktop = this->desktop; + + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), GDK_CURRENT_TIME); + + this->finishItem(); + this->sel_changed_connection.disconnect(); + + ToolBase::finish(); +} + +SpiralTool::~SpiralTool() { + this->enableGrDrag(false); + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = NULL; + + /* 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(SH_KNOTHOLDER); + this->shape_editor->set_item(selection->singleItem(), SH_KNOTHOLDER); +} + +void SpiralTool::setup() { + ToolBase::setup(); + + sp_event_context_read(this, "expansion"); + sp_event_context_read(this, "revolution"); + sp_event_context_read(this, "t0"); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = sp_desktop_selection(this->desktop)->singleItem(); + if (item) { + this->shape_editor->set_item(item, SH_KNOTHOLDER); + } + + Inkscape::Selection *selection = sp_desktop_selection(this->desktop); + 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(); + } +} + +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; + + SPDesktop *desktop = this->desktop; + Inkscape::Selection *selection = sp_desktop_selection (desktop); + + 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 && !this->space_panning) { + dragging = TRUE; + + this->center = Inkscape::setup_for_drag_start(desktop, this, event); + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + ( GDK_KEY_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_POINTER_MOTION_HINT_MASK | + GDK_BUTTON_PRESS_MASK ), + NULL, event->button.time); + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + 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(this->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 (!sp_event_context_knot_mouseover(this)) { + 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 && !this->space_panning) { + dragging = FALSE; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the spiral + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = NULL; + ret = TRUE; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), event->button.time); + } + break; + + case GDK_KEY_PRESS: + switch (get_group0_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"), + NULL, + _("<b>Alt</b>: lock spiral radius")); + 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_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-spiral"); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_space: + if (dragging) { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), + event->button.time); + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the spiral + this->finish(); + } + // 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_group0_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) { + SPDesktop *desktop = SP_EVENT_CONTEXT(this)->desktop; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + if (!this->spiral) { + if (Inkscape::have_viable_layer(desktop, this->message_context) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = SP_EVENT_CONTEXT_DOCUMENT(this)->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "spiral"); + + // Set style + sp_desktop_apply_style_tool(desktop, repr, "/tools/shapes/spiral", false); + + this->spiral = SP_SPIRAL(desktop->currentLayer()->appendChildRepr(repr)); + Inkscape::GC::release(repr); + this->spiral->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + this->spiral->updateRepr(); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + 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); + + gdouble arg = Geom::atan2(delta) - 2.0*M_PI*this->spiral->revo; + + if (state & GDK_CONTROL_MASK) { + arg = sp_round(arg, M_PI/snaps); + } + + /* 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"); + GString *rads = g_string_new(q.string(desktop->namedview->doc_units).c_str()); + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Spiral</b>: radius %s, angle %5g°; with <b>Ctrl</b> to snap angle"), + rads->str, sp_round((arg + 2.0*M_PI*this->spiral->revo)*180/M_PI, 0.0001)); + g_string_free(rads, FALSE); +} + +void SpiralTool::finishItem() { + this->message_context->clear(); + + if (this->spiral != NULL) { + 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); + spiral->doWriteTransform(spiral->getRepr(), spiral->transform, NULL, true); + + this->desktop->canvas->endForcedFullRedraws(); + + sp_desktop_selection(this->desktop)->set(this->spiral); + DocumentUndo::done(sp_desktop_document(this->desktop), SP_VERB_CONTEXT_SPIRAL, _("Create spiral")); + + this->spiral = NULL; + } +} + +void SpiralTool::cancel() { + sp_desktop_selection(this->desktop)->clear(); + sp_canvas_item_ungrab(SP_CANVAS_ITEM(this->desktop->acetate), 0); + + if (this->spiral != NULL) { + this->spiral->deleteObject(); + this->spiral = NULL; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = NULL; + + this->desktop->canvas->endForcedFullRedraws(); + + DocumentUndo::cancel(sp_desktop_document(this->desktop)); +} + +} +} +} + +/* + 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 000000000..416aeb7e4 --- /dev/null +++ b/src/ui/tools/spiral-tool.h @@ -0,0 +1,66 @@ +#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 + */ + +#include <gtk/gtk.h> +#include <stddef.h> +#include <sigc++/sigc++.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +#include "sp-spiral.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) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class SpiralTool : public ToolBase { +public: + SpiralTool(); + virtual ~SpiralTool(); + + static const std::string prefsPath; + + virtual void setup(); + virtual void finish(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +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 000000000..0ded1e44b --- /dev/null +++ b/src/ui/tools/spray-tool.cpp @@ -0,0 +1,890 @@ +/* + * 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 + * + * Copyright (C) 2009 authors + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include "config.h" + +#include <numeric> + +#include "ui/dialog/dialog-manager.h" + +#include "svg/svg.h" + +#include <glib.h> +#include "macros.h" +#include "document.h" +#include "document-undo.h" +#include "selection.h" +#include "desktop.h" +#include "desktop-events.h" +#include "desktop-handles.h" +#include "message-context.h" +#include "pixmaps/cursor-spray.xpm" +#include <boost/optional.hpp> +#include "xml/repr.h" +#include "context-fns.h" +#include "sp-item.h" +#include "inkscape.h" + +#include "splivarot.h" +#include "sp-item-group.h" +#include "sp-shape.h" +#include "sp-path.h" +#include "path-chemistry.h" + +#include "sp-text.h" +#include "sp-flowtext.h" +#include "display/sp-canvas.h" +#include "display/canvas-bpath.h" +#include "display/canvas-arena.h" +#include "display/curve.h" +#include "livarot/Shape.h" +#include <2geom/circle.h> +#include <2geom/transforms.h> +#include "preferences.h" +#include "style.h" +#include "box3d.h" +#include "sp-item-transform.h" +#include "filter-chemistry.h" + +#include "ui/tools/spray-tool.h" +#include "helper/action.h" +#include "verbs.h" + +#include <iostream> + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +using Inkscape::DocumentUndo; +using namespace std; + +#define DDC_RED_RGBA 0xff0000ff +#define DYNA_MIN_WIDTH 1.0e-6 + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createSprayContext() { + return new SprayTool(); + } + + bool sprayContextRegistered = ToolFactory::instance().registerObject("/tools/spray", createSprayContext); +} + +const std::string& SprayTool::getPrefsPath() { + return SprayTool::prefsPath; +} + +const std::string SprayTool::prefsPath = "/tools/spray"; + +/** + * 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 = Geom::Affine(s).inverse() * Geom::Affine(rotation) * Geom::Affine(s); + // Rotate item. + item->set_i2d_affine(item->i2dt_affine() * (Geom::Affine)affine); + // Use each item's own transform writer, consistent with sp_selection_apply_affine() + item->doWriteTransform(item->getRepr(), 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->getRepr(), item->transform); +} + +SprayTool::SprayTool() : ToolBase() { + this->usetilt = 0; + this->dilate_area = 0; + this->usetext = false; + this->population = 0; + this->is_drawing = false; + this->mode = 0; + this->usepressure = 0; + + this->cursor_shape = cursor_spray_xpm; + this->hot_x = 4; + this->hot_y = 4; + + /* attributes */ + this->dragging = FALSE; + this->distrib = 1; + this->width = 0.2; + this->force = 0.2; + this->ratio = 0; + this->tilt = 0; + this->mean = 0.2; + this->rotation_variation = 0; + this->standard_deviation = 0.2; + this->scale = 1; + this->scale_variation = 1; + this->pressure = TC_DEFAULT_PRESSURE; + + this->is_dilating = false; + this->has_dilated = false; +} + +SprayTool::~SprayTool() { + this->enableGrDrag(false); + this->style_set_connection.disconnect(); + + if (this->dilate_area) { + sp_canvas_item_destroy(this->dilate_area); + this->dilate_area = NULL; + } +} + +static bool is_transform_modes(gint mode) +{ + return (mode == SPRAY_MODE_COPY || + mode == SPRAY_MODE_CLONE || + mode == SPRAY_MODE_SINGLE_PATH || + mode == SPRAY_OPTION); +} + +void SprayTool::update_cursor(bool /*with_shift*/) { + guint num = 0; + gchar *sel_message = NULL; + + if (!desktop->selection->isEmpty()) { + num = g_slist_length(const_cast<GSList *>(desktop->selection->itemList())); + 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; + } + + this->sp_event_context_update_cursor(); + g_free(sel_message); +} + +void SprayTool::setup() { + ToolBase::setup(); + + { + /* TODO: have a look at sp_dyna_draw_context_setup where the same is done.. generalize? at least make it an arcto! */ + Geom::PathVector path; + Geom::Circle(0, 0, 1).getPath(path); + + SPCurve *c = new SPCurve(path); + + this->dilate_area = sp_canvas_bpath_new(sp_desktop_controls(this->desktop), c); + c->unref(); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->dilate_area), 0x00000000,(SPWindRule)0); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->dilate_area), 0xff9900ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_hide(this->dilate_area); + } + + this->is_drawing = false; + + 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, "force"); + sp_event_context_read(this, "mean"); + sp_event_context_read(this, "standard_deviation"); + sp_event_context_read(this, "usepressure"); + sp_event_context_read(this, "Scale"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/spray/selcue")) { + this->enableSelectionCue(); + } + if (prefs->getBool("/tools/spray/gradientdrag")) { + this->enableGrDrag(); + } +} + +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 == "usepressure") { + this->usepressure = 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 == "force") { + this->force = CLAMP(val.getDouble(1.0), 0, 1.0); + } +} + +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_dilate_radius(SprayTool *tc) +{ + return 250 * tc->width/SP_EVENT_CONTEXT(tc)->desktop->current_zoom(); +} + +static double get_path_force(SprayTool *tc) +{ + double force = 8 * (tc->usepressure? tc->pressure : TC_DEFAULT_PRESSURE) + /sqrt(SP_EVENT_CONTEXT(tc)->desktop->current_zoom()); + if (force > 3) { + force += 4 * (force - 3); + } + return force * tc->force; +} + +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_move_force(SprayTool *tc) +{ + double force = (tc->usepressure? tc->pressure : TC_DEFAULT_PRESSURE); + return force * tc->force; +} + +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 bool sp_spray_recursive(SPDesktop *desktop, + Inkscape::Selection *selection, + SPItem *item, + Geom::Point p, + Geom::Point /*vector*/, + gint mode, + double radius, + double /*force*/, + double population, + double &scale, + double scale_variation, + bool /*reverse*/, + double mean, + double standard_deviation, + double ratio, + double tilt, + double rotation_variation, + gint _distrib) +{ + bool did = false; + + if (SP_IS_BOX3D(item) ) { + // convert 3D boxes to ordinary groups before spraying their shapes + item = box3d_convert_to_group(SP_BOX3D(item)); + selection->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 ); + double dr; double dp; + random_position( dr, dp, mean, standard_deviation, _distrib ); + dr=dr*radius; + + if (mode == SPRAY_MODE_COPY) { + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + SPItem *item_copied; + if(_fid <= population) + { + // duplicate + 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(); + Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc); + parent->appendChild(copy); + + SPObject *new_obj = doc->getObjectByRepr(copy); + item_copied = SP_ITEM(new_obj); //convertion object->item + 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)); + //Move the cursor p + 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()); + sp_item_move_rel(item_copied, Geom::Translate(move[Geom::X], -move[Geom::Y])); + did = true; + } + } + } else if (mode == SPRAY_MODE_SINGLE_PATH) { + + SPItem *father = NULL; //initial Object + SPItem *item_copied = NULL; //Projected Object + SPItem *unionResult = NULL; //previous union + SPItem *son = NULL; //father copy + + int i=1; + for (GSList *items = g_slist_copy(const_cast<GSList *>(selection->itemList())); + items != NULL; + items = items->next) { + + SPItem *item1 = SP_ITEM(items->data); + if (i == 1) { + father = item1; + } + if (i == 2) { + unionResult = item1; + } + i++; + } + SPDocument *doc = father->document; + Inkscape::XML::Document* xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *old_repr = father->getRepr(); + Inkscape::XML::Node *parent = old_repr->parent(); + + Geom::OptRect a = father->documentVisualBounds(); + if (a) { + if (i == 2) { + Inkscape::XML::Node *copy1 = old_repr->duplicate(xml_doc); + parent->appendChild(copy1); + SPObject *new_obj1 = doc->getObjectByRepr(copy1); + son = SP_ITEM(new_obj1); // conversion object->item + unionResult = son; + Inkscape::GC::release(copy1); + } + + if (_fid <= population) { // Rules the population of objects sprayed + // duplicates the father + Inkscape::XML::Node *copy2 = old_repr->duplicate(xml_doc); + parent->appendChild(copy2); + SPObject *new_obj2 = doc->getObjectByRepr(copy2); + item_copied = SP_ITEM(new_obj2); + + // 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=father->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)); + sp_item_move_rel(item_copied, Geom::Translate(move[Geom::X], -move[Geom::Y])); + + // union and duplication + selection->clear(); + selection->add(item_copied); + selection->add(unionResult); + sp_selected_path_union_skip_undo(selection, selection->desktop()); + selection->add(father); + Inkscape::GC::release(copy2); + did = true; + } + } + } else if (mode == SPRAY_MODE_CLONE) { + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + if(_fid <= population) { + SPItem *item_copied; + 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(); + + // Creation of the clone + Inkscape::XML::Node *clone = xml_doc->createElement("svg:use"); + // Ad the clone to the list of the father's sons + parent->appendChild(clone); + // Generates the link between father and son attributes + gchar *href_str = g_strdup_printf("#%s", old_repr->attribute("id")); + clone->setAttribute("xlink:href", href_str, false); + g_free(href_str); + + SPObject *clone_object = doc->getObjectByRepr(clone); + // conversion object->item + item_copied = SP_ITEM(clone_object); + 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)); + 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()); + sp_item_move_rel(item_copied, Geom::Translate(move[Geom::X], -move[Geom::Y])); + + 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) +{ + Inkscape::Selection *selection = sp_desktop_selection(SP_EVENT_CONTEXT(tc)->desktop); + SPDesktop *desktop = SP_EVENT_CONTEXT(tc)->desktop; + + if (selection->isEmpty()) { + return false; + } + + bool did = false; + double radius = get_dilate_radius(tc); + double path_force = get_path_force(tc); + if (radius == 0 || path_force == 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_force = get_move_force(tc); + double move_mean = get_move_mean(tc); + double move_standard_deviation = get_move_standard_deviation(tc); + + for (GSList *items = g_slist_copy(const_cast<GSList *>(selection->itemList())); + items != NULL; + items = items->next) { + + SPItem *item = SP_ITEM(items->data); + + if (is_transform_modes(tc->mode)) { + if (sp_spray_recursive(desktop, selection, item, p, vector, tc->mode, radius, move_force, tc->population, tc->scale, tc->scale_variation, reverse, move_mean, move_standard_deviation, tc->ratio, tc->tilt, tc->rotation_variation, tc->distrib)) + did = true; + } else { + if (sp_spray_recursive(desktop, selection, item, p, vector, tc->mode, radius, path_force, tc->population, tc->scale, tc->scale_variation, reverse, path_mean, path_standard_deviation, tc->ratio, tc->tilt, tc->rotation_variation, tc->distrib)) + did = true; + } + } + + 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)) ); + sp_canvas_item_affine_absolute(tc->dilate_area, (sm* Geom::Rotate(tc->tilt))* Geom::Translate(SP_EVENT_CONTEXT(tc)->desktop->point())); + sp_canvas_item_show(tc->dilate_area); +} + +static void sp_spray_switch_mode(SprayTool *tc, gint mode, bool with_shift) +{ + // select the button mode + SP_EVENT_CONTEXT(tc)->desktop->setToolboxSelectOneValue("spray_tool_mode", mode); + // 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: + sp_canvas_item_show(this->dilate_area); + break; + case GDK_LEAVE_NOTIFY: + sp_canvas_item_hide(this->dilate_area); + break; + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + if (Inkscape::have_viable_layer(desktop, this->message_context) == false) { + return TRUE; + } + + 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); + + desktop->canvas->forceFullRedrawAfterInterruptions(3); + this->is_drawing = true; + this->is_dilating = true; + this->has_dilated = false; + + if(this->is_dilating && event->button.button == 1 && !this->space_panning) { + sp_spray_dilate(this, motion_w, desktop->dt2doc(motion_dt), 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)) ); + sp_canvas_item_affine_absolute(this->dilate_area, (sm*Geom::Rotate(this->tilt))*Geom::Translate(desktop->w2d(motion_w))); + sp_canvas_item_show(this->dilate_area); + + guint num = 0; + if (!desktop->selection->isEmpty()) { + num = g_slist_length(const_cast<GSList *>(desktop->selection->itemList())); + } + 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();; + Geom::Point motion_doc(desktop->dt2doc(scroll_dt)); + switch (event->scroll.direction) { + case GDK_SCROLL_DOWN: + case GDK_SCROLL_UP: { + if (Inkscape::have_viable_layer(desktop, this->message_context) == false) { + return TRUE; + } + this->last_push = desktop->dt2doc(scroll_dt); + sp_spray_extinput(this, event); + desktop->canvas->forceFullRedrawAfterInterruptions(3); + this->is_drawing = true; + this->is_dilating = true; + this->has_dilated = false; + if(this->is_dilating && !this->space_panning) { + 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)); + + desktop->canvas->endForcedFullRedraws(); + this->is_drawing = false; + + if (this->is_dilating && event->button.button == 1 && !this->space_panning) { + 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(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_SPRAY, _("Spray with copies")); + break; + case SPRAY_MODE_CLONE: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_SPRAY, _("Spray with clones")); + break; + case SPRAY_MODE_SINGLE_PATH: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_SPRAY, _("Spray in single path")); + break; + } + } + break; + } + + case GDK_KEY_PRESS: + switch (get_group0_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; + 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; + 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("altx-spray", 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("altx-spray", 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("altx-spray", 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("altx-spray", 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("altx-spray"); + 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_group0_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 000000000..e7362fd50 --- /dev/null +++ b/src/ui/tools/spray-tool.h @@ -0,0 +1,121 @@ +#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 + * + * Copyright (C) 2009 authors + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <2geom/point.h> +#include "ui/tools/tool-base.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 { + 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_OPTION, +}; + +class SprayTool : public ToolBase { +public: + SprayTool(); + virtual ~SprayTool(); + + //ToolBase event_context; + //Inkscape::UI::Dialog::Dialog *dialog_option;//Attribut de type SprayOptionClass, localisé dans scr/ui/dialog + /* extended input data */ + gdouble pressure; + + /* attributes */ + guint dragging : 1; /* mouse state: mouse is dragging */ + guint usepressure : 1; + guint usetilt : 1; + bool usetext ; + + double width; + double ratio; + double tilt; + double rotation_variation; + double force; + 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; + SPCanvasItem *dilate_area; + + sigc::connection style_set_connection; + + static const std::string prefsPath; + + virtual void setup(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); + + virtual const std::string& getPrefsPath(); + + + void update_cursor(bool /*with_shift*/); +}; + +} +} +} + +#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 000000000..b5d8c4418 --- /dev/null +++ b/src/ui/tools/star-tool.cpp @@ -0,0 +1,494 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <cstring> +#include <string> + +#include <gdk/gdkkeysyms.h> + +#include "macros.h" +#include "display/sp-canvas.h" +#include "sp-star.h" +#include "document.h" +#include "document-undo.h" +#include "sp-namedview.h" +#include "selection.h" +#include "desktop-handles.h" +#include "snap.h" +#include "desktop.h" +#include "desktop-style.h" +#include "message-context.h" +#include "pixmaps/cursor-star.xpm" +#include <glibmm/i18n.h> +#include "preferences.h" +#include "xml/repr.h" +#include "xml/node-event-vector.h" +#include "context-fns.h" +#include "shape-editor.h" +#include "verbs.h" +#include "display/sp-canvas-item.h" + +#include "ui/tools/star-tool.h" + +using Inkscape::DocumentUndo; + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createStarContext() { + return new StarTool(); + } + + bool starContextRegistered = ToolFactory::instance().registerObject("/tools/shapes/star", createStarContext); +} + +const std::string& StarTool::getPrefsPath() { + return StarTool::prefsPath; +} + +const std::string StarTool::prefsPath = "/tools/shapes/star"; + +StarTool::StarTool() : ToolBase() { + this->randomized = 0; + this->rounded = 0; + + this->cursor_shape = cursor_star_xpm; + this->hot_x = 4; + this->hot_y = 4; + this->xp = 0; + this->yp = 0; + this->tolerance = 0; + this->within_tolerance = false; + this->item_to_select = NULL; + //this->tool_url = "/tools/shapes/star"; + + this->star = NULL; + + this->magnitude = 5; + this->proportion = 0.5; + this->isflatsided = false; +} + +void StarTool::finish() { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), GDK_CURRENT_TIME); + + this->finishItem(); + this->sel_changed_connection.disconnect(); + + ToolBase::finish(); +} + +StarTool::~StarTool() { + this->enableGrDrag(false); + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = NULL; + + /* 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 != NULL); + + this->shape_editor->unset_item(SH_KNOTHOLDER); + this->shape_editor->set_item(selection->singleItem(), SH_KNOTHOLDER); +} + +void StarTool::setup() { + ToolBase::setup(); + + sp_event_context_read(this, "magnitude"); + sp_event_context_read(this, "proportion"); + sp_event_context_read(this, "isflatsided"); + sp_event_context_read(this, "rounded"); + sp_event_context_read(this, "randomized"); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = sp_desktop_selection(this->desktop)->singleItem(); + if (item) { + this->shape_editor->set_item(item, SH_KNOTHOLDER); + } + + Inkscape::Selection *selection = sp_desktop_selection(this->desktop); + + 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(); + } +} + +void StarTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "magnitude") { + this->magnitude = CLAMP(val.getInt(5), 3, 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; + + SPDesktop *desktop = this->desktop; + Inkscape::Selection *selection = sp_desktop_selection (desktop); + 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 && !this->space_panning) { + dragging = true; + + this->center = Inkscape::setup_for_drag_start(desktop, this, event); + + /* Snap center */ + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop, true); + m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_POINTER_MOTION_HINT_MASK | + GDK_BUTTON_PRESS_MASK, + NULL, event->button.time); + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + 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 (!sp_event_context_knot_mouseover(this)) { + 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 && !this->space_panning) { + dragging = false; + + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the star + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = NULL; + ret = TRUE; + sp_canvas_item_ungrab(SP_CANVAS_ITEM (desktop->acetate), event->button.time); + } + break; + + case GDK_KEY_PRESS: + switch (get_group0_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"), + NULL, + NULL); + 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_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; + sp_event_context_discard_delayed_snap_event(this); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_space: + if (dragging) { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), event->button.time); + + dragging = false; + + sp_event_context_discard_delayed_snap_event(this); + + 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_group0_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) +{ + SPDesktop *desktop = this->desktop; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + if (!this->star) { + if (Inkscape::have_viable_layer(desktop, this->message_context) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "star"); + + // Set style + sp_desktop_apply_style_tool(desktop, repr, "/tools/shapes/star", false); + + this->star = SP_STAR(desktop->currentLayer()->appendChildRepr(repr)); + + Inkscape::GC::release(repr); + this->star->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + this->star->updateRepr(); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + /* 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 */ + arg1 = sp_round(arg1, M_PI / snaps); + } + + 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"); + GString *rads = g_string_new(q.string(desktop->namedview->doc_units).c_str()); + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + ( this->isflatsided? + _("<b>Polygon</b>: radius %s, angle %5g°; with <b>Ctrl</b> to snap angle") + : _("<b>Star</b>: radius %s, angle %5g°; with <b>Ctrl</b> to snap angle") ), + rads->str, sp_round((arg1) * 180 / M_PI, 0.0001)); + + g_string_free(rads, FALSE); +} + +void StarTool::finishItem() { + this->message_context->clear(); + + if (this->star != NULL) { + 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); + this->star->doWriteTransform(this->star->getRepr(), this->star->transform, NULL, true); + + desktop->canvas->endForcedFullRedraws(); + + sp_desktop_selection(desktop)->set(this->star); + DocumentUndo::done(sp_desktop_document(desktop), SP_VERB_CONTEXT_STAR, + _("Create star")); + + this->star = NULL; + } +} + +void StarTool::cancel() { + sp_desktop_selection(desktop)->clear(); + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), 0); + + if (this->star != NULL) { + this->star->deleteObject(); + this->star = NULL; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = NULL; + + desktop->canvas->endForcedFullRedraws(); + + DocumentUndo::cancel(sp_desktop_document(desktop)); +} + +} +} +} + +/* + 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 000000000..da3774e68 --- /dev/null +++ b/src/ui/tools/star-tool.h @@ -0,0 +1,74 @@ +#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, read the file 'COPYING' for more information + */ + +#include <stddef.h> +#include <sigc++/sigc++.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +#include "sp-star.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +class StarTool : public ToolBase { +public: + StarTool(); + virtual ~StarTool(); + + static const std::string prefsPath; + + virtual void setup(); + virtual void finish(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +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 000000000..0a8b35110 --- /dev/null +++ b/src/ui/tools/text-tool.cpp @@ -0,0 +1,1765 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <gtkmm/clipboard.h> +#include <display/sp-ctrlline.h> +#include <display/sodipodi-ctrlrect.h> +#include <display/sp-ctrlquadr.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> +#include <sstream> + +#include "context-fns.h" +#include "desktop-handles.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "macros.h" +#include "message-context.h" +#include "message-stack.h" +#include "pixmaps/cursor-text-insert.xpm" +#include "pixmaps/cursor-text.xpm" +#include "preferences.h" +#include "rubberband.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "shape-editor.h" +#include "sp-flowtext.h" +#include "sp-namedview.h" +#include "sp-text.h" +#include "style.h" +#include "ui/tools/text-tool.h" +#include "text-editing.h" +#include "ui/control-manager.h" +#include "verbs.h" +#include "xml/node-event-vector.h" +#include "xml/repr.h" +#include <gtk/gtk.h> +#include "tool-factory.h" + +using Inkscape::ControlManager; +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void sp_text_context_selection_changed(Inkscape::Selection *selection, TextTool *tc); +static void sp_text_context_selection_modified(Inkscape::Selection *selection, guint flags, TextTool *tc); +static bool sp_text_context_style_set(SPCSSAttr const *css, TextTool *tc); +static int sp_text_context_style_query(SPStyle *style, int property, TextTool *tc); + +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); + +namespace { + ToolBase* createTextContext() { + return new TextTool(); + } + + bool textContextRegistered = ToolFactory::instance().registerObject("/tools/text", createTextContext); +} + +const std::string& TextTool::getPrefsPath() { + return TextTool::prefsPath; +} + +const std::string TextTool::prefsPath = "/tools/text"; + + +TextTool::TextTool() : ToolBase() { + this->preedit_string = 0; + this->unipos = 0; + + this->cursor_shape = cursor_text_xpm; + this->hot_x = 7; + this->hot_y = 7; + + this->xp = 0; + this->yp = 0; + this->tolerance = 0; + this->within_tolerance = false; + + this->imc = NULL; + + this->text = NULL; + this->pdoc = Geom::Point(0, 0); + + this->unimode = false; + + this->cursor = NULL; + this->indicator = NULL; + this->frame = NULL; + this->grabbed = NULL; + this->timeout = 0; + this->show = FALSE; + this->phase = 0; + this->nascent_object = 0; + this->over_text = 0; + this->dragging = 0; + this->creating = 0; +} + +TextTool::~TextTool() { + delete this->shape_editor; + this->shape_editor = NULL; + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, GDK_CURRENT_TIME); + this->grabbed = NULL; + } + + Inkscape::Rubberband::get(this->desktop)->stop(); +} + +void TextTool::setup() { + GtkSettings* settings = gtk_settings_get_default(); + gint timeout = 0; + g_object_get( settings, "gtk-cursor-blink-time", &timeout, NULL ); + + if (timeout < 0) { + timeout = 200; + } else { + timeout /= 2; + } + + this->cursor = ControlManager::getManager().createControlLine(sp_desktop_controls(desktop), Geom::Point(100, 0), Geom::Point(100, 100)); + this->cursor->setRgba32(0x000000ff); + sp_canvas_item_hide(this->cursor); + + this->indicator = sp_canvas_item_new(sp_desktop_controls(desktop), SP_TYPE_CTRLRECT, NULL); + SP_CTRLRECT(this->indicator)->setRectangle(Geom::Rect(Geom::Point(0, 0), Geom::Point(100, 100))); + SP_CTRLRECT(this->indicator)->setColor(0x0000ff7f, false, 0); + sp_canvas_item_hide(this->indicator); + + this->frame = sp_canvas_item_new(sp_desktop_controls(desktop), SP_TYPE_CTRLRECT, NULL); + SP_CTRLRECT(this->frame)->setRectangle(Geom::Rect(Geom::Point(0, 0), Geom::Point(100, 100))); + SP_CTRLRECT(this->frame)->setColor(0x0000ff7f, false, 0); + sp_canvas_item_hide(this->frame); + + 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(sp_desktop_canvas(desktop)); + + /* 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, NULL, this); + } + } + + ToolBase::setup(); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = sp_desktop_selection(this->desktop)->singleItem(); + if (item && SP_IS_FLOWTEXT(item) && SP_FLOWTEXT(item)->has_internal_frame()) { + this->shape_editor->set_item(item, SH_KNOTHOLDER); + } + + this->sel_changed_connection = sp_desktop_selection(desktop)->connectChanged( + sigc::bind(sigc::ptr_fun(&sp_text_context_selection_changed), this) + ); + this->sel_modified_connection = sp_desktop_selection(desktop)->connectModified( + sigc::bind(sigc::ptr_fun(&sp_text_context_selection_modified), this) + ); + this->style_set_connection = desktop->connectSetStyle( + sigc::bind(sigc::ptr_fun(&sp_text_context_style_set), this) + ); + this->style_query_connection = desktop->connectQueryStyle( + sigc::bind(sigc::ptr_fun(&sp_text_context_style_query), this) + ); + + sp_text_context_selection_changed(sp_desktop_selection(desktop), this); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/text/selcue")) { + this->enableSelectionCue(); + } + if (prefs->getBool("/tools/text/gradientdrag")) { + this->enableGrDrag(); + } +} + +void TextTool::finish() { + if (this->desktop) { + sp_signal_disconnect_by_data(sp_desktop_canvas(this->desktop), 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 = NULL; + } + + if (this->timeout) { + g_source_remove(this->timeout); + this->timeout = 0; + } + + if (this->cursor) { + sp_canvas_item_destroy(this->cursor); + this->cursor = NULL; + } + + if (this->indicator) { + sp_canvas_item_destroy(this->indicator); + this->indicator = NULL; + } + + if (this->frame) { + sp_canvas_item_destroy(this->frame); + this->frame = NULL; + } + + for (std::vector<SPCanvasItem*>::iterator it = this->text_selection_quads.begin() ; + it != this->text_selection_quads.end() ; ++it) { + sp_canvas_item_hide(*it); + sp_canvas_item_destroy(*it); + } + + this->text_selection_quads.clear(); +} + +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->space_panning) { + // find out clicked item, disregarding groups + item_ungrouped = desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE); + if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) { + sp_desktop_selection(desktop)->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) { + 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->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->space_panning) { + this->dragging = 0; + sp_event_context_discard_delayed_snap_event(this); + ret = TRUE; + } + break; + case GDK_MOTION_NOTIFY: + if ((event->motion.state & GDK_BUTTON1_MASK) && this->dragging && !this->space_panning) { + 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); + ret = TRUE; + break; + } + // find out item under mouse, disregarding groups + item_ungrouped = desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE); + if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) { + + Inkscape::Text::Layout const *layout = te_get_layout(item_ungrouped); + if (layout->inputTruncated()) { + SP_CTRLRECT(this->indicator)->setColor(0xff0000ff, false, 0); + } else { + SP_CTRLRECT(this->indicator)->setColor(0x0000ff7f, false, 0); + } + Geom::OptRect ibbox = item_ungrouped->desktopVisualBounds(); + if (ibbox) { + SP_CTRLRECT(this->indicator)->setRectangle(*ibbox); + } + sp_canvas_item_show(this->indicator); + + this->cursor_shape = cursor_text_insert_xpm; + this->hot_x = 7; + this->hot_y = 10; + this->sp_event_context_update_cursor(); + sp_text_context_update_text_selection(this); + + if (SP_IS_TEXT (item_ungrouped)) { + desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> to edit the text, <b>drag</b> to select part of the text.")); + } else { + desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> to edit the flowed text, <b>drag</b> to select part of the text.")); + } + + this->over_text = true; + + ret = TRUE; + } + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::item_handler(item, event); + } + + return ret; +} + +static void sp_text_context_setup_text(TextTool *tc) +{ + ToolBase *ec = SP_EVENT_CONTEXT(tc); + + /* Create <text> */ + Inkscape::XML::Document *xml_doc = SP_EVENT_CONTEXT_DESKTOP(ec)->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(SP_EVENT_CONTEXT_DESKTOP(ec), rtext, "/tools/text", true); + + sp_repr_set_svg_double(rtext, "x", tc->pdoc[Geom::X]); + sp_repr_set_svg_double(rtext, "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, NULL); + Inkscape::GC::release(rtspan); + + /* Create TEXT */ + Inkscape::XML::Node *rstring = xml_doc->createTextNode(""); + rtspan->addChild(rstring, NULL); + Inkscape::GC::release(rstring); + SPItem *text_item = SP_ITEM(ec->desktop->currentLayer()->appendChildRepr(rtext)); + /* fixme: Is selection::changed really immediate? */ + /* yes, it's immediate .. why does it matter? */ + sp_desktop_selection(ec->desktop)->set(text_item); + Inkscape::GC::release(rtext); + text_item->transform = SP_ITEM(ec->desktop->currentLayer())->i2doc_affine().inverse(); + + text_item->updateRepr(); + text_item->doWriteTransform(text_item->getRepr(), text_item->transform, NULL, true); + DocumentUndo::done(sp_desktop_document(ec->desktop), SP_VERB_CONTEXT_TEXT, + _("Create 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->desktop->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 = 0; // 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(sp_desktop_document(tc->desktop), SP_VERB_DIALOG_TRANSFORM, + _("Insert Unicode character")); + } +} + +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) { + sp_canvas_item_hide(this->indicator); + + sp_text_context_validate_cursor_iterators(this); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + + if (Inkscape::have_viable_layer(desktop, desktop->messageStack()) == 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); + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK, + NULL, event->button.time); + this->grabbed = SP_CANVAS_ITEM(desktop->acetate); + this->creating = 1; + + /* Processed */ + return TRUE; + } + break; + case GDK_MOTION_NOTIFY: + if (this->over_text) { + this->over_text = 0; + // update cursor and statusbar: we are not over a text object now + this->cursor_shape = cursor_text_xpm; + this->hot_x = 7; + this->hot_y = 7; + this->sp_event_context_update_cursor(); + desktop->event_context->defaultMessageContext()->clear(); + } + + if (this->creating && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + 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"); + GString *xs = g_string_new(x_q.string(desktop->namedview->doc_units).c_str()); + GString *ys = g_string_new(y_q.string(desktop->namedview->doc_units).c_str()); + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Flowed text frame</b>: %s × %s"), xs->str, ys->str); + + g_string_free(xs, FALSE); + g_string_free(ys, FALSE); + + } else if (!sp_event_context_knot_mouseover(this)) { + 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(); + } + break; + case GDK_BUTTON_RELEASE: + if (event->button.button == 1 && !this->space_panning) { + sp_event_context_discard_delayed_snap_event(this); + + 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(); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, GDK_CURRENT_TIME); + this->grabbed = NULL; + } + + Inkscape::Rubberband::get(desktop)->stop(); + + if (this->creating && this->within_tolerance) { + /* Button 1, set X & Y & new item */ + sp_desktop_selection(desktop)->clear(); + this->pdoc = desktop->dt2doc(p1); + this->show = TRUE; + this->phase = 1; + this->nascent_object = 1; // new object was just created + + /* Cursor */ + sp_canvas_item_show(this->cursor); + // 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); + this->cursor->setCoords(p1, p1 + Geom::Point(0, cursor_height)); + if (this->imc) { + GdkRectangle im_cursor; + Geom::Point const top_left = SP_EVENT_CONTEXT(this)->desktop->get_display_area().corner(3); + Geom::Point const cursor_size(0, cursor_height); + Geom::Point const im_position = SP_EVENT_CONTEXT(this)->desktop->d2w(p1 + cursor_size - top_left); + im_cursor.x = (int) floor(im_position[Geom::X]); + im_cursor.y = (int) floor(im_position[Geom::Y]); + im_cursor.width = 0; + im_cursor.height = (int) -floor(SP_EVENT_CONTEXT(this)->desktop->d2w(cursor_size)[Geom::Y]); + 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) + SPItem *ft = create_flowtext_with_internal_frame (desktop, this->p0, p1); + /* Set style */ + sp_desktop_apply_style_tool(desktop, ft->getRepr(), "/tools/text", true); + sp_desktop_selection(desktop)->set(ft); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Flowed text is created.")); + DocumentUndo::done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT, + _("Create flowed 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; + return TRUE; + } + break; + case GDK_KEY_PRESS: { + guint const group0_keyval = get_group0_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 + + if (this->unimode || !this->imc + || (MOD__CTRL(event) && MOD__SHIFT(event)) // input methods tend to steal this for unimode, + // but we have our own so make sure they don't swallow it + || !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: { + if (g_ascii_isxdigit(group0_keyval)) { + g_return_val_if_fail(this->unipos < sizeof(this->uni) - 1, TRUE); + this->uni[this->unipos++] = group0_keyval; + 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(); + 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 ("altx-text"); + 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 = 0; // 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(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT, + _("Insert no-break space")); + 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(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT, + _("Make bold")); + 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(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT, + _("Make italic")); + 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 = 0; // we don't need it anymore, having created a real <text> + } + + 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(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT, + _("New line")); + 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(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT, + _("Backspace")); + } + 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(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT, + _("Delete")); + } + 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_group0_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(sp_desktop_document(desktop), "kern:left", SP_VERB_CONTEXT_TEXT, + _("Kern to the left")); + } 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_group0_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(sp_desktop_document(desktop), "kern:right", SP_VERB_CONTEXT_TEXT, + _("Kern to the right")); + } 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_group0_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(sp_desktop_document(desktop), "kern:up", SP_VERB_CONTEXT_TEXT, + _("Kern up")); + } 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_group0_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(sp_desktop_document(desktop), "kern:down", SP_VERB_CONTEXT_TEXT, + _("Kern down")); + } 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 = 0; + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, GDK_CURRENT_TIME); + this->grabbed = NULL; + } + Inkscape::Rubberband::get(desktop)->stop(); + } else { + sp_desktop_selection(desktop)->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(sp_desktop_document(desktop), "textrot:ccw", SP_VERB_CONTEXT_TEXT, + _("Rotate counterclockwise")); + 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(sp_desktop_document(desktop), "textrot:cw", SP_VERB_CONTEXT_TEXT, + _("Rotate clockwise")); + 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(sp_desktop_document(desktop), "linespacing:dec", SP_VERB_CONTEXT_TEXT, + _("Contract line spacing")); + } 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(sp_desktop_document(desktop), "letterspacing:dec", SP_VERB_CONTEXT_TEXT, + _("Contract letter spacing")); + } + 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(sp_desktop_document(desktop), "linespacing:inc", SP_VERB_CONTEXT_TEXT, + _("Expand line spacing")); + } 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(sp_desktop_document(desktop), "letterspacing:inc", SP_VERB_CONTEXT_TEXT, + _("Expand letter spacing"));\ + } + 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 = 0; + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, GDK_CURRENT_TIME); + this->grabbed = NULL; + } + Inkscape::Rubberband::get(desktop)->stop(); + } + } else if ((group0_keyval == GDK_KEY_x || group0_keyval == GDK_KEY_X) && MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-text"); + 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()) { + // 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 = 0; // 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) { + 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; + } + 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; + } + DocumentUndo::done(sp_desktop_document(ec->desktop), SP_VERB_CONTEXT_TEXT, + _("Paste 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 == NULL) + 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 NULL; + TextTool const *tc = SP_TEXT_CONTEXT(ec); + if (tc->text == NULL) + return NULL; + + 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 NULL; +} + +/** + 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 == NULL) + 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. + */ +static void +sp_text_context_selection_changed(Inkscape::Selection *selection, TextTool *tc) +{ + g_assert(selection != NULL); + + ToolBase *ec = SP_EVENT_CONTEXT(tc); + + ec->shape_editor->unset_item(SH_KNOTHOLDER); + SPItem *item = selection->singleItem(); + if (item && SP_IS_FLOWTEXT(item) && SP_FLOWTEXT(item)->has_internal_frame()) { + ec->shape_editor->set_item(item, SH_KNOTHOLDER); + } + + if (tc->text && (item != tc->text)) { + sp_text_context_forget_text(tc); + } + tc->text = NULL; + + if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + tc->text = item; + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + if (layout) + tc->text_sel_start = tc->text_sel_end = layout->end(); + } else { + tc->text = NULL; + } + + // 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(tc, false); + sp_text_context_update_text_selection(tc); +} + +static void +sp_text_context_selection_modified(Inkscape::Selection */*selection*/, guint /*flags*/, TextTool *tc) +{ + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); +} + +static bool sp_text_context_style_set(SPCSSAttr const *css, TextTool *tc) +{ + if (tc->text == NULL) + return false; + if (tc->text_sel_start == tc->text_sel_end) + return false; // will get picked up by the parent and applied to the whole text object + + sp_te_apply_style(tc->text, tc->text_sel_start, tc->text_sel_end, css); + DocumentUndo::done(sp_desktop_document(tc->desktop), SP_VERB_CONTEXT_TEXT, + _("Set text style")); + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); + + return true; +} + +static int +sp_text_context_style_query(SPStyle *style, int property, TextTool *tc) +{ + if (tc->text == NULL) { + return QUERY_STYLE_NOTHING; + } + const Inkscape::Text::Layout *layout = te_get_layout(tc->text); + if (layout == NULL) { + return QUERY_STYLE_NOTHING; + } + sp_text_context_validate_cursor_iterators(tc); + + GSList *styles_list = NULL; + + Inkscape::Text::Layout::iterator begin_it, end_it; + if (tc->text_sel_start < tc->text_sel_end) { + begin_it = tc->text_sel_start; + end_it = tc->text_sel_end; + } else { + begin_it = tc->text_sel_end; + end_it = tc->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 const *pos_obj = 0; + void *rawptr = 0; + layout->getSourceOfCharacter(it, &rawptr); + if (!rawptr || !SP_IS_OBJECT(rawptr)) { + continue; + } + pos_obj = SP_OBJECT(rawptr); + while (SP_IS_STRING(pos_obj) && pos_obj->parent) { + pos_obj = pos_obj->parent; // SPStrings don't have style + } + styles_list = g_slist_prepend(styles_list, (gpointer)pos_obj); + } + + int result = sp_desktop_query_style_from_list (styles_list, style, property); + + g_slist_free(styles_list); + return result; +} + +static void sp_text_context_validate_cursor_iterators(TextTool *tc) +{ + if (tc->text == NULL) + 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->desktop) return; + + 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) { + Geom::Point const center = SP_EVENT_CONTEXT(tc)->desktop->get_display_area().midpoint(); + 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 + SP_EVENT_CONTEXT(tc)->desktop->scroll_to_point(d0, 1.0); + else + SP_EVENT_CONTEXT(tc)->desktop->scroll_to_point(d1, 1.0); + } + + sp_canvas_item_show(tc->cursor); + tc->cursor->setCoords(d0, d1); + + /* 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 = SP_EVENT_CONTEXT(tc)->desktop->get_display_area().corner(3); + Geom::Point const im_d0 = SP_EVENT_CONTEXT(tc)->desktop->d2w(d0 - top_left); + Geom::Point const im_d1 = SP_EVENT_CONTEXT(tc)->desktop->d2w(d1 - top_left); + im_cursor.x = (int) floor(im_d0[Geom::X]); + im_cursor.y = (int) floor(im_d1[Geom::Y]); + im_cursor.width = (int) floor(im_d1[Geom::X]) - im_cursor.x; + im_cursor.height = (int) floor(im_d0[Geom::Y]) - im_cursor.y; + gtk_im_context_set_cursor_location(tc->imc, &im_cursor); + } + + tc->show = TRUE; + tc->phase = 1; + + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + int const nChars = layout->iteratorToCharIndex(layout->end()); + char const *trunc = ""; + bool truncated = false; + if (layout->inputTruncated()) { + truncated = true; + trunc = _(" [truncated]"); + } + if (SP_IS_FLOWTEXT(tc->text)) { + SPItem *frame = SP_FLOWTEXT(tc->text)->get_frame (NULL); // first frame only + if (frame) { + if (truncated) { + SP_CTRLRECT(tc->frame)->setColor(0xff0000ff, false, 0); + } else { + SP_CTRLRECT(tc->frame)->setColor(0x0000ff7f, false, 0); + } + sp_canvas_item_show(tc->frame); + Geom::OptRect frame_bbox = frame->desktopVisualBounds(); + if (frame_bbox) { + SP_CTRLRECT(tc->frame)->setRectangle(*frame_bbox); + } + } + + SP_EVENT_CONTEXT(tc)->message_context->setF(Inkscape::NORMAL_MESSAGE, 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), nChars, trunc); + } else { + SP_EVENT_CONTEXT(tc)->message_context->setF(Inkscape::NORMAL_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), nChars, trunc); + } + + } else { + sp_canvas_item_hide(tc->cursor); + sp_canvas_item_hide(tc->frame); + tc->show = FALSE; + if (!tc->nascent_object) { + SP_EVENT_CONTEXT(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 + } + } + + SP_EVENT_CONTEXT(tc)->desktop->emitToolSubselectionChanged((gpointer)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->desktop) return; + + for (std::vector<SPCanvasItem*>::iterator it = tc->text_selection_quads.begin() ; it != tc->text_selection_quads.end() ; it++) { + sp_canvas_item_hide(*it); + sp_canvas_item_destroy(*it); + } + tc->text_selection_quads.clear(); + + std::vector<Geom::Point> quads; + if (tc->text != NULL) + 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) { + SPCanvasItem *quad_canvasitem; + quad_canvasitem = sp_canvas_item_new(sp_desktop_controls(tc->desktop), SP_TYPE_CTRLQUADR, NULL); + // FIXME: make the color settable in prefs + // for now, use semitrasparent blue, as cairo cannot do inversion :( + sp_ctrlquadr_set_rgba32(SP_CTRLQUADR(quad_canvasitem), 0x00777777); + sp_ctrlquadr_set_coords(SP_CTRLQUADR(quad_canvasitem), quads[i], quads[i+1], quads[i+2], quads[i+3]); + sp_canvas_item_show(quad_canvasitem); + tc->text_selection_quads.push_back(quad_canvasitem); + } +} + +static gint sp_text_context_timeout(TextTool *tc) +{ + if (tc->show) { + sp_canvas_item_show(tc->cursor); + if (tc->phase) { + tc->phase = 0; + tc->cursor->setRgba32(0x000000ff); + } else { + tc->phase = 1; + tc->cursor->setRgba32(0xffffffff); + } + } + + 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 = NULL; + +/* FIXME: this automatic deletion when nothing is inputted crashes the XML edittor and also crashes when duplicating an empty flowtext. + So don't create an empty flowtext in the first place? Create it when first character is typed. + */ +/* + if ((SP_IS_TEXT(ti) || SP_IS_FLOWTEXT(ti)) && sp_te_input_is_empty(ti)) { + Inkscape::XML::Node *text_repr = ti->getRepr(); + // the repr may already have been unparented + // if we were called e.g. as the result of + // an undo or the element being removed from + // the XML editor + if ( text_repr && text_repr->parent() ) { + sp_repr_unparent(text_repr); + SPDocumentUndo::done(sp_desktop_document(tc->desktop), SP_VERB_CONTEXT_TEXT, + _("Remove empty 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 = 0; // 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, SP_VERB_CONTEXT_TEXT, + _("Type text")); +} + +void sp_text_context_place_cursor (TextTool *tc, SPObject *text, Inkscape::Text::Layout::iterator where) +{ + SP_EVENT_CONTEXT_DESKTOP (tc)->selection->set (text); + tc->text_sel_start = tc->text_sel_end = where; + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); +} + +void sp_text_context_place_cursor_at (TextTool *tc, SPObject *text, Geom::Point const p) +{ + SP_EVENT_CONTEXT_DESKTOP (tc)->selection->set (text); + sp_text_context_place_cursor (tc, text, sp_te_get_position_by_coords(tc->text, p)); +} + +Inkscape::Text::Layout::iterator *sp_text_context_get_cursor_position(TextTool *tc, SPObject *text) +{ + if (text != tc->text) + return NULL; + 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 000000000..ef8a67984 --- /dev/null +++ b/src/ui/tools/text-tool.h @@ -0,0 +1,101 @@ +#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, read the file 'COPYING' for more information + */ + +/* #include <gdk/gdkic.h> */ +#include <stddef.h> +#include <sigc++/sigc++.h> +#include <gtk/gtk.h> + +#include "ui/tools/tool-base.h" +#include <2geom/point.h> +#include "libnrtype/Layout-TNG.h" + +#define SP_TEXT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::TextTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_TEXT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::TextTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +struct SPCtrlLine; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class TextTool : public ToolBase { +public: + TextTool(); + virtual ~TextTool(); + + sigc::connection sel_changed_connection; + sigc::connection sel_modified_connection; + sigc::connection style_set_connection; + sigc::connection style_query_connection; + + GtkIMContext *imc; + + SPItem *text; // 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; + guint unipos; + + SPCtrlLine *cursor; + SPCanvasItem *indicator; + SPCanvasItem *frame; // hiliting the first frame of flowtext; FIXME: make this a list to accommodate arbitrarily many chained shapes + std::vector<SPCanvasItem*> text_selection_quads; + gint timeout; + guint show : 1; + guint phase : 1; + guint nascent_object : 1; // true if we're clicked on canvas to put cursor, but no text typed yet so ->text is still NULL + + guint over_text : 1; // true if cursor is over a text object + + guint dragging : 2; // dragging selection over text + + guint creating : 1; // dragging rubberband to create flowtext + SPCanvasItem *grabbed; // we grab while we are creating, to get events even if the mouse goes out of the window + Geom::Point p0; // initial point if the flowtext rect + + /* Preedit String */ + gchar* preedit_string; + + static const std::string prefsPath; + + virtual void setup(); + virtual void finish(); + virtual bool root_handler(GdkEvent* event); + virtual bool item_handler(SPItem* item, GdkEvent* event); + + virtual const std::string& getPrefsPath(); +}; + +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); +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 diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp new file mode 100644 index 000000000..c123aec27 --- /dev/null +++ b/src/ui/tools/tool-base.cpp @@ -0,0 +1,1541 @@ +/* + * 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, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#if GLIBMM_DISABLE_DEPRECATED && HAVE_GLIBMM_THREADS_H +#include <glibmm/threads.h> +#endif + +#include "shortcuts.h" +#include "file.h" +#include "ui/tools/tool-base.h" + +#include <string.h> +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> +#include <glibmm/i18n.h> +#include <cstring> +#include <string> + +#include "display/sp-canvas.h" +#include "xml/node-event-vector.h" +#include "sp-cursor.h" +#include "desktop.h" +#include "desktop-handles.h" +#include "desktop-events.h" +#include "desktop-style.h" +#include "widgets/desktop-widget.h" +#include "sp-namedview.h" +#include "selection.h" +#include "interface.h" +#include "macros.h" +#include "tools-switch.h" +#include "preferences.h" +#include "message-context.h" +#include "gradient-drag.h" +#include "attributes.h" +#include "rubberband.h" +#include "selcue.h" +#include "ui/tools/lpe-tool.h" +#include "ui/tool/control-point.h" +#include "shape-editor.h" +#include "sp-guide.h" +#include "color.h" + +// globals for temporary switching to selector by space +static bool selector_toggled = FALSE; +static int switch_selector_to = 0; + +// globals for temporary switching to dropper by 'D' +static bool dropper_toggled = FALSE; +static int switch_dropper_to = 0; + +//static gint xp = 0, yp = 0; // where drag started +//static gint tolerance = 0; +//static bool within_tolerance = false; + +// globals for keeping track of keyboard scroll events in order to accelerate +static guint32 scroll_event_time = 0; +static gdouble scroll_multiply = 1; +static guint scroll_keyval = 0; + + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void set_event_location(SPDesktop * desktop, GdkEvent * event); + + +void ToolBase::set(const Inkscape::Preferences::Entry& /*val*/) { +} + +void ToolBase::activate() { +} + +void ToolBase::deactivate() { +} + +void ToolBase::finish() { + this->enableSelectionCue(false); +} + +ToolBase::ToolBase() { + this->hot_y = 0; + this->xp = 0; + this->cursor_shape = 0; + this->pref_observer = 0; + this->hot_x = 0; + this->yp = 0; + this->within_tolerance = false; + this->tolerance = 0; + //this->key = 0; + this->item_to_select = 0; + + this->desktop = NULL; + this->cursor = NULL; + this->message_context = NULL; + this->_selcue = NULL; + this->_grdrag = NULL; + this->space_panning = false; + this->shape_editor = NULL; + this->_delayed_snap_event = NULL; + this->_dse_callback_in_process = false; + //this->tool_url = NULL; +} + +ToolBase::~ToolBase() { + if (this->message_context) { + delete this->message_context; + } + + if (this->cursor != NULL) { +#if GTK_CHECK_VERSION(3,0,0) + g_object_unref(this->cursor); +#else + gdk_cursor_unref(this->cursor); +#endif + this->cursor = NULL; + } + + if (this->desktop) { + this->desktop = NULL; + } + + if (this->pref_observer) { + delete this->pref_observer; + } + + if (this->_delayed_snap_event) { + delete this->_delayed_snap_event; + } +} + + +/** + * Set the cursor to a standard GDK cursor + */ +static void sp_event_context_set_cursor(ToolBase *event_context, GdkCursorType cursor_type) { + + GtkWidget *w = GTK_WIDGET(sp_desktop_canvas(event_context->desktop)); + GdkDisplay *display = gdk_display_get_default(); + GdkCursor *cursor = gdk_cursor_new_for_display(display, cursor_type); + +#if WITH_GTKMM_3_0 + if (cursor) { + gdk_window_set_cursor (gtk_widget_get_window (w), cursor); + g_object_unref (cursor); + } +#else + gdk_window_set_cursor (gtk_widget_get_window (w), cursor); + gdk_cursor_unref (cursor); +#endif + +} + +/** + * Recreates and draws cursor on desktop related to ToolBase. + */ +void ToolBase::sp_event_context_update_cursor() { + GtkWidget *w = GTK_WIDGET(sp_desktop_canvas(this->desktop)); + if (gtk_widget_get_window (w)) { + + GtkStyle *style = gtk_widget_get_style(w); + + /* fixme: */ + if (this->cursor_shape) { + GdkDisplay *display = gdk_display_get_default(); + if (gdk_display_supports_cursor_alpha(display) && gdk_display_supports_cursor_color(display)) { + bool fillHasColor=false, strokeHasColor=false; + guint32 fillColor = sp_desktop_get_color_tool(this->desktop, this->getPrefsPath(), true, &fillHasColor); + guint32 strokeColor = sp_desktop_get_color_tool(this->desktop, this->getPrefsPath(), false, &strokeHasColor); + double fillOpacity = fillHasColor ? sp_desktop_get_opacity_tool(this->desktop, this->getPrefsPath(), true) : 0; + double strokeOpacity = strokeHasColor ? sp_desktop_get_opacity_tool(this->desktop, this->getPrefsPath(), false) : 0; + + GdkPixbuf *pixbuf = sp_cursor_pixbuf_from_xpm( + this->cursor_shape, + style->black, style->white, + SP_RGBA32_U_COMPOSE(SP_RGBA32_R_U(fillColor),SP_RGBA32_G_U(fillColor),SP_RGBA32_B_U(fillColor),SP_COLOR_F_TO_U(fillOpacity)), + SP_RGBA32_U_COMPOSE(SP_RGBA32_R_U(strokeColor),SP_RGBA32_G_U(strokeColor),SP_RGBA32_B_U(strokeColor),SP_COLOR_F_TO_U(strokeOpacity)) + ); + if (pixbuf != NULL) { + if (this->cursor) { +#if GTK_CHECK_VERSION(3,0,0) + g_object_unref(this->cursor); +#else + gdk_cursor_unref(this->cursor); +#endif + } + this->cursor = gdk_cursor_new_from_pixbuf(display, pixbuf, this->hot_x, this->hot_y); + g_object_unref(pixbuf); + } + } else { + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_xpm_data((const gchar **)this->cursor_shape); + + if (pixbuf) { + if (this->cursor) { +#if GTK_CHECK_VERSION(3,0,0) + g_object_unref(this->cursor); +#else + gdk_cursor_unref(this->cursor); +#endif + } + this->cursor = gdk_cursor_new_from_pixbuf(display, + pixbuf, this->hot_x, this->hot_y); + g_object_unref(pixbuf); + } + } + } + gdk_window_set_cursor(gtk_widget_get_window (w), this->cursor); + gdk_flush(); + } + this->desktop->waiting_cursor = false; +} + +/** + * Callback that gets called on initialization of ToolBase object. + * Redraws mouse cursor, at the moment. + */ + +/** + * When you override it, call this method first. + */ +void ToolBase::setup() { + this->pref_observer = new ToolPrefObserver(this->getPrefsPath(), this); + Inkscape::Preferences::get()->addObserver(*(this->pref_observer)); + + this->sp_event_context_update_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, gint 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. + */ +gint gobble_motion_events(gint 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_MOTION_NOTIFY + && (event_next->motion.state & mask)) { + // kill it + gdk_event_free(event_next); + // get next + event_next = gdk_event_get(); + i++; + } + // otherwise, put it back onto the queue + if (event_next) + gdk_event_put(event_next); + + return i; +} + +/** + * 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 (tools_isactive(dt, TOOLS_SELECT)) { + if (selector_toggled) { + if (switch_selector_to) + tools_switch(dt, switch_selector_to); + selector_toggled = FALSE; + } else + return; + } else { + selector_toggled = TRUE; + switch_selector_to = tools_active(dt); + tools_switch(dt, TOOLS_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 (tools_isactive(dt, TOOLS_DROPPER)) { + if (dropper_toggled) { + if (switch_dropper_to) + tools_switch(dt, switch_dropper_to); + dropper_toggled = FALSE; + } else + return; + } else { + dropper_toggled = TRUE; + switch_dropper_to = tools_active(dt); + tools_switch(dt, TOOLS_DROPPER); + } +} + +/** + * Calculates and keeps track of scroll acceleration. + * Subroutine of sp_event_context_private_root_handler(). + */ +static gdouble accelerate_scroll(GdkEvent *event, gdouble acceleration, + SPCanvas */*canvas*/) { + guint32 time_diff = ((GdkEventKey *) event)->time - scroll_event_time; + + /* key pressed within 500ms ? (1/2 second) */ + if (time_diff > 500 || event->key.keyval != scroll_keyval) { + scroll_multiply = 1; // abort acceleration + } else { + scroll_multiply += acceleration; // continue acceleration + } + + scroll_event_time = ((GdkEventKey *) event)->time; + scroll_keyval = event->key.keyval; + + return scroll_multiply; +} + +/** + * Main event dispatch, gets called from Gdk. + */ +//static gint sp_event_context_private_root_handler( +// ToolBase *event_context, GdkEvent *event) { +// +// return event_context->ceventcontext->root_handler(event); +//} + +bool ToolBase::root_handler(GdkEvent* event) { + static Geom::Point button_w; + static unsigned int panning = 0; + 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); + + gint ret = FALSE; + + switch (event->type) { + case GDK_2BUTTON_PRESS: + if (panning) { + panning = 0; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), event->button.time); + ret = TRUE; + } else { + /* sp_desktop_dialog(); */ + } + break; + + case GDK_BUTTON_PRESS: + // save drag origin + xp = (gint) event->button.x; + yp = (gint) event->button.y; + within_tolerance = true; + + button_w = Geom::Point(event->button.x, event->button.y); + + switch (event->button.button) { + case 1: + if (this->space_panning) { + // When starting panning, make sure there are no snap events pending because these might disable the panning again + sp_event_context_discard_delayed_snap_event(this); + panning = 1; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK + | GDK_POINTER_MOTION_MASK + | GDK_POINTER_MOTION_HINT_MASK, NULL, + event->button.time - 1); + + ret = TRUE; + } + break; + + case 2: + 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 + sp_event_context_discard_delayed_snap_event(this); + panning = 2; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK + | GDK_POINTER_MOTION_HINT_MASK, NULL, + event->button.time - 1); + + } + + ret = TRUE; + break; + + case 3: + if ((event->button.state & GDK_SHIFT_MASK) || (event->button.state & GDK_CONTROL_MASK)) { + // When starting panning, make sure there are no snap events pending because these might disable the panning again + sp_event_context_discard_delayed_snap_event(this); + panning = 3; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK + | GDK_POINTER_MOTION_HINT_MASK, NULL, + event->button.time); + + ret = TRUE; + } else { + sp_event_root_menu_popup(desktop, NULL, event); + } + 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); + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK + | GDK_POINTER_MOTION_MASK + | GDK_POINTER_MOTION_HINT_MASK, NULL, + event->motion.time - 1); + } + + 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 = 0; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), event->button.time); + ret = TRUE; + } else { + if (within_tolerance && (abs((gint) event->motion.x - xp) + < tolerance) && (abs((gint) event->motion.y - yp) + < tolerance)) { + // 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; + sp_event_context_set_cursor(this, GDK_FLEUR); + } + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const moved_w(motion_w - button_w); + this->desktop->scroll_world(moved_w, true); // we're still scrolling, do not redraw + ret = TRUE; + } + } else if (zoom_rb) { + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(desktop->w2d(motion_w)); + + if (within_tolerance && (abs((gint) event->motion.x - xp) + < tolerance) && (abs((gint) event->motion.y - yp) + < tolerance)) { + break; // do not drag if we're within tolerance from origin + } + + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + within_tolerance = false; + + if (Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->move(motion_dt); + } else { + Inkscape::Rubberband::get(desktop)->start(desktop, motion_dt); + } + + if (zoom_rb == 2) { + gobble_motion_events(GDK_BUTTON2_MASK); + } + } + break; + + case GDK_BUTTON_RELEASE: + xp = yp = 0; + + if (panning_cursor == 1) { + panning_cursor = 0; + GtkWidget *w = GTK_WIDGET(sp_desktop_canvas(this->desktop)); + gdk_window_set_cursor(gtk_widget_get_window (w), this->cursor); + } + + if (within_tolerance && (panning || zoom_rb)) { + zoom_rb = 0; + + if (panning) { + panning = 0; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), + event->button.time); + } + + Geom::Point const event_w(event->button.x, event->button.y); + Geom::Point const event_dt(desktop->w2d(event_w)); + + double const zoom_inc = prefs->getDoubleLimited( + "/options/zoomincrement/value", M_SQRT2, 1.01, 10); + + desktop->zoom_relative_keep_point(event_dt, (event->button.state + & GDK_SHIFT_MASK) ? 1 / zoom_inc : zoom_inc); + + desktop->updateNow(); + ret = TRUE; + } else if (panning == event->button.button) { + panning = 0; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), + event->button.time); + + // in slow complex drawings, some of the motion events are lost; + // to make up for this, we scroll it once again to the button-up event coordinates + // (i.e. canvas will always get scrolled all the way to the mouse release point, + // even if few intermediate steps were visible) + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const moved_w(motion_w - button_w); + + this->desktop->scroll_world(moved_w); + desktop->updateNow(); + 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_group0_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. + unsigned int shortcut; + case GDK_KEY_Tab: + case GDK_KEY_ISO_Left_Tab: + case GDK_KEY_F1: + shortcut = get_group0_keyval(&event->key); + + if (event->key.state & GDK_SHIFT_MASK) { + shortcut |= SP_SHORTCUT_SHIFT_MASK; + } + + if (event->key.state & GDK_CONTROL_MASK) { + shortcut |= SP_SHORTCUT_CONTROL_MASK; + } + + if (event->key.state & GDK_MOD1_MASK) { + shortcut |= SP_SHORTCUT_ALT_MASK; + } + + ret = sp_shortcut_invoke(shortcut, desktop); + 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(NULL); + ret = TRUE; + } + break; + + case GDK_KEY_Left: // Ctrl Left + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, + acceleration, sp_desktop_canvas(desktop))); + + gobble_key_events(get_group0_keyval(&event->key), GDK_CONTROL_MASK); + this->desktop->scroll_world(i, 0); + ret = TRUE; + } + break; + + case GDK_KEY_Up: // Ctrl Up + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, + acceleration, sp_desktop_canvas(desktop))); + + gobble_key_events(get_group0_keyval(&event->key), GDK_CONTROL_MASK); + this->desktop->scroll_world(0, i); + ret = TRUE; + } + break; + + case GDK_KEY_Right: // Ctrl Right + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, + acceleration, sp_desktop_canvas(desktop))); + + gobble_key_events(get_group0_keyval(&event->key), GDK_CONTROL_MASK); + this->desktop->scroll_world(-i, 0); + ret = TRUE; + } + break; + + case GDK_KEY_Down: // Ctrl Down + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, + acceleration, sp_desktop_canvas(desktop))); + + gobble_key_events(get_group0_keyval(&event->key), GDK_CONTROL_MASK); + this->desktop->scroll_world(0, -i); + ret = TRUE; + } + break; + + case GDK_KEY_F10: + if (MOD__SHIFT_ONLY(event)) { + sp_event_root_menu_popup(desktop, NULL, event); + ret = TRUE; + } + break; + + case GDK_KEY_space: + xp = yp = 0; + within_tolerance = true; + panning = 4; + + this->space_panning = true; + this->message_context->set(Inkscape::INFORMATION_MESSAGE, + _("<b>Space+mouse move</b> to pan canvas")); + + ret = TRUE; + break; + + case GDK_KEY_z: + case GDK_KEY_Z: + if (MOD__ALT_ONLY(event)) { + desktop->zoom_grab_focus(); + ret = TRUE; + } + break; + + default: + break; + } + } + break; + + case GDK_KEY_RELEASE: + // Stop panning on any key release + if (this->space_panning) { + this->space_panning = false; + this->message_context->clear(); + } + + if (panning) { + panning = 0; + xp = yp = 0; + + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), + event->key.time); + + desktop->updateNow(); + } + + if (panning_cursor == 1) { + panning_cursor = 0; + GtkWidget *w = GTK_WIDGET(sp_desktop_canvas(this->desktop)); + gdk_window_set_cursor(gtk_widget_get_window (w), this->cursor); + } + + switch (get_group0_keyval(&event->key)) { + case GDK_KEY_space: + if (within_tolerance == true) { + // Space was pressed, but not panned + sp_toggle_selector(desktop); + ret = TRUE; + } + + within_tolerance = false; + 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: { + bool ctrl = (event->scroll.state & GDK_CONTROL_MASK); + bool wheelzooms = prefs->getBool("/options/wheelzooms/value"); + + int const wheel_scroll = prefs->getIntLimited( + "/options/wheelscroll/value", 40, 0, 1000); + +#if GTK_CHECK_VERSION(3,0,0) + // Size of smooth-scrolls (only used in GTK+ 3) + gdouble delta_x = 0; + gdouble delta_y = 0; +#endif + + /* shift + wheel, pan left--right */ + if (event->scroll.state & GDK_SHIFT_MASK) { + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + desktop->scroll_world(wheel_scroll, 0); + break; + + case GDK_SCROLL_DOWN: + desktop->scroll_world(-wheel_scroll, 0); + break; + + default: + break; + } + + /* ctrl + wheel, zoom in--out */ + } else if ((ctrl && !wheelzooms) || (!ctrl && wheelzooms)) { + 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; + + default: + rel_zoom = 0.0; + break; + } + + if (rel_zoom != 0.0) { + Geom::Point const scroll_dt = desktop->point(); + desktop->zoom_relative_keep_point(scroll_dt, rel_zoom); + } + + /* no modifier, pan up--down (left--right on multiwheel mice?) */ + } else { + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + desktop->scroll_world(0, wheel_scroll); + break; + + case GDK_SCROLL_DOWN: + desktop->scroll_world(0, -wheel_scroll); + break; + + case GDK_SCROLL_LEFT: + desktop->scroll_world(wheel_scroll, 0); + break; + + case GDK_SCROLL_RIGHT: + desktop->scroll_world(-wheel_scroll, 0); + break; + +#if GTK_CHECK_VERSION(3,0,0) + case GDK_SCROLL_SMOOTH: + gdk_event_get_scroll_deltas(event, &delta_x, &delta_y); + desktop->scroll_world(delta_x, delta_y); + break; +#endif + } + } + break; + } + default: + break; + } + + return ret; +} + +/** + * 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. + */ +//gint sp_event_context_private_item_handler(ToolBase *ec, SPItem *item, +// GdkEvent *event) { +// +// return ec->ceventcontext->item_handler(item, event); +//} + +bool ToolBase::item_handler(SPItem* item, GdkEvent* event) { + int ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if ((event->button.button == 3) && !((event->button.state & GDK_SHIFT_MASK) || (event->button.state & GDK_CONTROL_MASK))) { + sp_event_root_menu_popup(this->desktop, item, event); + ret = TRUE; + } + break; + + default: + break; + } + + return ret; +} + +/** + * Returns true if we're hovering above a knot (needed because we don't want to pre-snap in that case). + */ +bool sp_event_context_knot_mouseover(ToolBase *ec) +{ + if (ec->shape_editor) { + return ec->shape_editor->knot_mouseover(); + } + + return false; +} + +/** + * Creates new ToolBase object and calls its virtual setup() function. + * @todo This is bogus. pref_path should be a private property of the inheriting objects. + */ +//ToolBase * +//sp_event_context_new(GType type, SPDesktop *desktop, gchar const *pref_path, +// unsigned int key) { +// g_return_val_if_fail(g_type_is_a(type, SP_TYPE_EVENT_CONTEXT), NULL); +// g_return_val_if_fail(desktop != NULL, NULL); +// +// ToolBase * const ec = static_cast<ToolBase*>(g_object_new(type, NULL)); +// +// ec->desktop = desktop; +// ec->_message_context +// = new Inkscape::MessageContext(desktop->messageStack()); +// ec->key = key; +// ec->pref_observer = NULL; +// +// if (pref_path) { +// ec->pref_observer = new ToolPrefObserver(pref_path, ec); +// +// Inkscape::Preferences *prefs = Inkscape::Preferences::get(); +// prefs->addObserver(*(ec->pref_observer)); +// } +// +//// if ((SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(ec)))->setup) +//// (SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(ec)))->setup(ec); +// ec->ceventcontext->setup(); +// +// return ec; +//} + +/** + * Finishes ToolBase. + */ +//void sp_event_context_finish(ToolBase *ec) { +// g_return_if_fail(ec != NULL); +// g_return_if_fail(SP_IS_EVENT_CONTEXT(ec)); +// +// ec->enableSelectionCue(false); +// +//// if (ec->next) { +//// g_warning("Finishing event context with active link\n"); +//// } +// +//// if ((SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(ec)))->finish) +//// (SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(ec)))->finish(ec); +// ec->finish(); +//} + +//-------------------------------member functions + +/** + * Enables/disables the ToolBase's SelCue. + */ +void ToolBase::enableSelectionCue(bool enable) { + if (enable) { + if (!_selcue) { + _selcue = new Inkscape::SelCue(desktop); + } + } else { + delete _selcue; + _selcue = NULL; + } +} + +/** + * 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 = NULL; + } + } +} + +/** + * Delete a selected GrDrag point + */ +bool ToolBase::deleteSelectedDrag(bool just_one) { + + if (_grdrag && _grdrag->selected) { + _grdrag->deleteSelected(just_one); + return TRUE; + } + + return FALSE; +} + +/** + * Calls virtual set() function of ToolBase. + */ +void sp_event_context_read(ToolBase *ec, gchar const *key) { + g_return_if_fail(ec != NULL); + g_return_if_fail(SP_IS_EVENT_CONTEXT(ec)); + g_return_if_fail(key != NULL); + +// if ((SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(ec)))->set) { +// Inkscape::Preferences *prefs = Inkscape::Preferences::get(); +// Inkscape::Preferences::Entry val = prefs->getEntry( +// ec->pref_observer->observed_path + '/' + key); +// (SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(ec)))->set(ec, &val); +// } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::Preferences::Entry val = prefs->getEntry(ec->pref_observer->observed_path + '/' + key); + ec->set(val); +} + +/** + * Calls virtual activate() function of ToolBase. + */ +void sp_event_context_activate(ToolBase *ec) { + g_return_if_fail(ec != NULL); + g_return_if_fail(SP_IS_EVENT_CONTEXT(ec)); + + // Make sure no delayed snapping events are carried over after switching contexts + // (this is only an additional safety measure against sloppy coding, because each + // context should take care of this by itself. + sp_event_context_discard_delayed_snap_event(ec); + +// if ((SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(ec)))->activate) +// (SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(ec)))->activate(ec); + ec->activate(); +} + +/** + * Calls virtual deactivate() function of ToolBase. + */ +//void sp_event_context_deactivate(ToolBase *ec) { +// g_return_if_fail(ec != NULL); +// g_return_if_fail(SP_IS_EVENT_CONTEXT(ec)); +// +//// if ((SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(ec)))->deactivate) +//// (SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(ec)))->deactivate(ec); +// ec->deactivate(); +//} + +/** + * Calls virtual root_handler(), the main event handling function. + */ +gint sp_event_context_root_handler(ToolBase * event_context, + GdkEvent * event) +{ + switch (event->type) { + case GDK_MOTION_NOTIFY: + sp_event_context_snap_delay_handler(event_context, NULL, NULL, + (GdkEventMotion *) event, + DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER); + break; + case GDK_BUTTON_RELEASE: + if (event_context && event_context->_delayed_snap_event) { + // If we have any pending snapping action, then invoke it now + sp_event_context_snap_watchdog_callback( + event_context->_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. + event_context->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally( + false); + break; + default: + break; + } + + return sp_event_context_virtual_root_handler(event_context, event); +} + +gint sp_event_context_virtual_root_handler(ToolBase * event_context, GdkEvent * event) { + gint ret = false; + if (event_context) { // If no event-context is available then do nothing, otherwise Inkscape would crash + // (see the comment in SPDesktop::set_event_context, and bug LP #622350) + //ret = (SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(event_context)))->root_handler(event_context, event); + ret = event_context->root_handler(event); + + set_event_location(event_context->desktop, event); + } + return ret; +} + +/** + * Calls virtual item_handler(), the item event handling function. + */ +gint sp_event_context_item_handler(ToolBase * event_context, + SPItem * item, GdkEvent * event) { + switch (event->type) { + case GDK_MOTION_NOTIFY: + sp_event_context_snap_delay_handler(event_context, (gpointer) item, NULL, (GdkEventMotion *) event, DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER); + break; + case GDK_BUTTON_RELEASE: + if (event_context && event_context->_delayed_snap_event) { + // If we have any pending snapping action, then invoke it now + sp_event_context_snap_watchdog_callback(event_context->_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. + event_context->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); + break; + default: + break; + } + + return sp_event_context_virtual_item_handler(event_context, item, event); +} + +gint sp_event_context_virtual_item_handler(ToolBase * event_context, SPItem * item, GdkEvent * event) { + gint ret = false; + if (event_context) { // If no event-context is available then do nothing, otherwise Inkscape would crash + // (see the comment in SPDesktop::set_event_context, and bug LP #622350) + //ret = (SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(event_context)))->item_handler(event_context, item, event); + ret = event_context->item_handler(item, event); + + if (!ret) { + ret = sp_event_context_virtual_root_handler(event_context, event); + } else { + set_event_location(event_context->desktop, event); + } + } + + return ret; +} + +/** + * Shows coordinates on status bar. + */ +static void set_event_location(SPDesktop *desktop, GdkEvent *event) { + if (event->type != GDK_MOTION_NOTIFY) { + return; + } + + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point const button_dt(desktop->w2d(button_w)); + desktop->set_coordinate_status(button_dt); +} + +//------------------------------------------------------------------- +/** + * Create popup menu and tell Gtk to show it. + */ +void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event) { + + // It seems the param item is the SPItem at the bottom of the z-order + // 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 + item = sp_event_context_find_item (desktop, + Geom::Point(event->button.x, event->button.y), FALSE, FALSE); + + /* fixme: This is not what I want but works for now (Lauris) */ + if (event->type == GDK_KEY_PRESS) { + item = sp_desktop_selection(desktop)->singleItem(); + } + + ContextMenu* CM = new ContextMenu(desktop, item); + CM->show(); + + switch (event->type) { + case GDK_BUTTON_PRESS: + CM->popup(event->button.button, event->button.time); + break; + case GDK_KEY_PRESS: + CM->popup(0, event->key.time); + break; + default: + break; + } +} + +/** + * Show tool context specific modifier tip. + */ +void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, + GdkEvent *event, gchar const *ctrl_tip, gchar const *shift_tip, + gchar const *alt_tip) { + guint keyval = get_group0_keyval(&event->key); + + bool ctrl = ctrl_tip && (MOD__CTRL(event) || (keyval == GDK_KEY_Control_L) || (keyval + == GDK_KEY_Control_R)); + bool shift = shift_tip && (MOD__SHIFT(event) || (keyval == GDK_KEY_Shift_L) || (keyval + == GDK_KEY_Shift_R)); + bool alt = alt_tip && (MOD__ALT(event) || (keyval == GDK_KEY_Alt_L) || (keyval + == GDK_KEY_Alt_R) || (keyval == GDK_KEY_Meta_L) || (keyval == GDK_KEY_Meta_R)); + + gchar *tip = g_strdup_printf("%s%s%s%s%s", (ctrl ? ctrl_tip : ""), (ctrl + && (shift || alt) ? "; " : ""), (shift ? shift_tip : ""), ((ctrl + || shift) && alt ? "; " : ""), (alt ? alt_tip : "")); + + if (strlen(tip) > 0) { + message_context->flash(Inkscape::INFORMATION_MESSAGE, tip); + } + + g_free(tip); +} + +/** + * Return the keyval corresponding to the key event in group 0, i.e., + * in the main (English) layout. + * + * Use this instead of simply event->keyval, so that your keyboard shortcuts + * work regardless of layouts (e.g., in Cyrillic). + */ +guint get_group0_keyval(GdkEventKey *event) { + guint keyval = 0; + + gdk_keymap_translate_keyboard_state(gdk_keymap_get_for_display( + gdk_display_get_default()), event->hardware_keycode, + (GdkModifierType) event->state, 0 /*event->key.group*/, &keyval, + NULL, NULL, NULL); + + 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 = 0; + + if (select_under) { + SPItem *selected_at_point = desktop->getItemFromListAtPointBottom( + desktop->selection->itemList(), p); + item = desktop->getItemAtPoint(p, into_groups, selected_at_point); + if (item == NULL) { // we may have reached bottom, flip over to the top + item = desktop->getItemAtPoint(p, into_groups, NULL); + } + } else { + item = desktop->getItemAtPoint(p, into_groups, NULL); + } + + 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) { + GSList *temp = NULL; + temp = g_slist_prepend(temp, item); + SPItem *item_at_point = desktop->getItemFromListAtPointBottom(temp, p); + g_slist_free(temp); + + return item_at_point; +} + +ShapeEditor * +sp_event_context_get_shape_editor(ToolBase *ec) { + return ec->shape_editor; +} + +void event_context_print_event_info(GdkEvent *event, bool print_return) { + switch (event->type) { + case GDK_BUTTON_PRESS: + g_print("GDK_BUTTON_PRESS"); + break; + case GDK_2BUTTON_PRESS: + g_print("GDK_2BUTTON_PRESS"); + break; + case GDK_3BUTTON_PRESS: + g_print("GDK_3BUTTON_PRESS"); + break; + + case GDK_MOTION_NOTIFY: + g_print("GDK_MOTION_NOTIFY"); + break; + case GDK_ENTER_NOTIFY: + g_print("GDK_ENTER_NOTIFY"); + break; + + case GDK_LEAVE_NOTIFY: + g_print("GDK_LEAVE_NOTIFY"); + break; + case GDK_BUTTON_RELEASE: + g_print("GDK_BUTTON_RELEASE"); + break; + + case GDK_KEY_PRESS: + g_print("GDK_KEY_PRESS: %d", get_group0_keyval(&event->key)); + break; + case GDK_KEY_RELEASE: + g_print("GDK_KEY_RELEASE: %d", get_group0_keyval(&event->key)); + break; + default: + //g_print ("even type not recognized"); + break; + } + + if (print_return) { + g_print("\n"); + } +} + +/** + * Analyses the current event, calculates the mouse speed, turns snapping off (temporarily) if the + * mouse speed is above a threshold, and stores the current event such that it can be re-triggered when needed + * (re-triggering is controlled by a watchdog timer). + * + * @param ec Pointer to the event context. + * @param dse_item Pointer that store a reference to a canvas or to an item. + * @param dse_item2 Another pointer, storing a reference to a knot or controlpoint. + * @param event Pointer to the motion event. + * @param origin Identifier (enum) specifying where the delay (and the call to this method) were initiated. + */ +void sp_event_context_snap_delay_handler(ToolBase *ec, + gpointer const dse_item, gpointer const dse_item2, GdkEventMotion *event, + DelayedSnapEvent::DelayedSnapEventOrigin origin) +{ + static guint32 prev_time; + static boost::optional<Geom::Point> prev_pos; + + if (ec->_dse_callback_in_process) { + return; + } + + // Snapping occurs when dragging with the left mouse button down, or when hovering e.g. in the pen tool with left mouse button up + bool const c1 = event->state & GDK_BUTTON2_MASK; // We shouldn't hold back any events when other mouse buttons have been + bool const c2 = event->state & GDK_BUTTON3_MASK; // pressed, e.g. when scrolling with the middle mouse button; if we do then + // Inkscape will get stuck in an unresponsive state + bool const c3 = tools_isactive(ec->desktop, TOOLS_CALLIGRAPHIC); + // 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 + + if (c1 || c2 || c3) { + // 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 + sp_event_context_discard_delayed_snap_event(ec); + } else if (ec->desktop + && ec->desktop->namedview->snap_manager.snapprefs.getSnapEnabledGlobally()) { + // Snap when speed drops below e.g. 0.02 px/msec, or when no motion events have occurred for some period. + // i.e. snap when we're at stand still. A speed threshold enforces snapping for tablets, which might never + // be fully at stand still and might keep spitting out motion events. + ec->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(true); // put snapping on hold + + Geom::Point event_pos(event->x, event->y); + guint32 event_t = gdk_event_get_time((GdkEvent *) event); + + if (prev_pos) { + Geom::Coord dist = Geom::L2(event_pos - *prev_pos); + guint32 delta_t = event_t - prev_time; + gdouble speed = delta_t > 0 ? dist / delta_t : 1000; + //std::cout << "Mouse speed = " << speed << " px/msec " << std::endl; + if (speed > 0.02) { // Jitter threshold, might be needed for tablets + // We're moving fast, so postpone any snapping until the next GDK_MOTION_NOTIFY event. We + // will keep on postponing the snapping as long as the speed is high. + // We must snap at some point in time though, so set a watchdog timer at some time from + // now, just in case there's no future motion event that drops under the speed limit (when + // stopping abruptly) + delete ec->_delayed_snap_event; + ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, + event, origin); // watchdog is reset, i.e. pushed forward in time + // If the watchdog expires before a new motion event is received, we will snap (as explained + // above). This means however that when the timer is too short, we will always snap and that the + // speed threshold is ineffective. In the extreme case the delay is set to zero, and snapping will + // be immediate, as it used to be in the old days ;-). + } else { // Speed is very low, so we're virtually at stand still + // But if we're really standing still, then we should snap now. We could use some low-pass filtering, + // otherwise snapping occurs for each jitter movement. For this filtering we'll leave the watchdog to expire, + // snap, and set a new watchdog again. + if (ec->_delayed_snap_event == NULL) { // no watchdog has been set + // it might have already expired, so we'll set a new one; the snapping frequency will be limited this way + ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, + dse_item2, event, origin); + } // else: watchdog has been set before and we'll wait for it to expire + } + } else { + // This is the first GDK_MOTION_NOTIFY event, so postpone snapping and set the watchdog + g_assert(ec->_delayed_snap_event == NULL); + ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, + event, origin); + } + + prev_pos = event_pos; + prev_time = event_t; + } +} + +/** + * When the snap delay watchdog timer barks, this method will be called and will re-inject the last motion + * event in an appropriate place, with snapping being turned on again. + */ +gboolean sp_event_context_snap_watchdog_callback(gpointer data) { + // Snap NOW! For this the "postponed" flag will be reset and the last motion event will be repeated + DelayedSnapEvent *dse = reinterpret_cast<DelayedSnapEvent*> (data); + + if (dse == NULL) { + // This might occur when this method is called directly, i.e. not through the timer + // E.g. on GDK_BUTTON_RELEASE in sp_event_context_root_handler() + return FALSE; + } + + ToolBase *ec = dse->getEventContext(); + if (ec == NULL) { + delete dse; + return false; + } + if (ec->desktop == NULL) { + ec->_delayed_snap_event = NULL; + delete dse; + return false; + } + + ec->_dse_callback_in_process = true; + + SPDesktop *dt = ec->desktop; + dt->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); + + // Depending on where the delayed snap event originated from, we will inject it back at it's origin + // The switch below takes care of that and prepares the relevant parameters + switch (dse->getOrigin()) { + case DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER: + sp_event_context_virtual_root_handler(ec, dse->getEvent()); + break; + case DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER: { + gpointer item = dse->getItem(); + if (item && SP_IS_ITEM(item)) { + sp_event_context_virtual_item_handler(ec, SP_ITEM(item), dse->getEvent()); + } + } + break; + case DelayedSnapEvent::KNOT_HANDLER: { + gpointer knot = dse->getItem2(); + if (knot && SP_IS_KNOT(knot)) { + sp_knot_handler_request_position(dse->getEvent(), SP_KNOT(knot)); + } + } + break; + case DelayedSnapEvent::CONTROL_POINT_HANDLER: { + using Inkscape::UI::ControlPoint; + gpointer pitem2 = dse->getItem2(); + if (!pitem2) + { + ec->_delayed_snap_event = NULL; + delete dse; + return false; + } + ControlPoint *point = reinterpret_cast<ControlPoint*> (pitem2); + if (point) { + if (point->position().isFinite() && (dt == point->_desktop)) { + point->_eventHandler(ec, dse->getEvent()); + } + else { + //workaround: + //[Bug 781893] Crash after moving a Bezier node after Knot path effect? + // --> at some time, some point with X = 0 and Y = nan (not a number) is created ... + // even so, the desktop pointer is invalid and equal to 0xff + g_warning ("encountered non finite point when evaluating snapping callback"); + } + } + } + break; + case DelayedSnapEvent::GUIDE_HANDLER: { + gpointer item = dse->getItem(); + gpointer item2 = dse->getItem2(); + if (item && item2) { + g_assert(SP_IS_CANVAS_ITEM(item)); + g_assert(SP_IS_GUIDE(item2)); + sp_dt_guide_event(SP_CANVAS_ITEM(item), dse->getEvent(), item2); + } + } + break; + case DelayedSnapEvent::GUIDE_HRULER: + case DelayedSnapEvent::GUIDE_VRULER: { + gpointer item = dse->getItem(); + gpointer item2 = dse->getItem2(); + if (item && item2) { + g_assert(GTK_IS_WIDGET(item)); + g_assert(SP_IS_DESKTOP_WIDGET(item2)); + if (dse->getOrigin() == DelayedSnapEvent::GUIDE_HRULER) { + sp_dt_hruler_event(GTK_WIDGET(item), dse->getEvent(), SP_DESKTOP_WIDGET(item2)); + } else { + sp_dt_vruler_event(GTK_WIDGET(item), dse->getEvent(), SP_DESKTOP_WIDGET(item2)); + } + } + } + break; + default: + g_warning("Origin of snap-delay event has not been defined!;"); + break; + } + + ec->_delayed_snap_event = NULL; + delete dse; + + ec->_dse_callback_in_process = false; + + return FALSE; //Kills the timer and stops it from executing this callback over and over again. +} + +void sp_event_context_discard_delayed_snap_event(ToolBase *ec) { + delete ec->_delayed_snap_event; + ec->_delayed_snap_event = NULL; + ec->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: + */ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/tool-base.h b/src/ui/tools/tool-base.h new file mode 100644 index 000000000..5185f89b1 --- /dev/null +++ b/src/ui/tools/tool-base.h @@ -0,0 +1,244 @@ +#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, read the file 'COPYING' for more information + */ + +#include <glib-object.h> +#include <gdk/gdk.h> +#include "knot.h" + +#include "2geom/forward.h" +#include "preferences.h" + +class GrDrag; +class SPDesktop; +class SPItem; +class ShapeEditor; + +namespace Inkscape { + class MessageContext; + class SelCue; + namespace XML { + class Node; + } +} + +#define SP_EVENT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ToolBase*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_EVENT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::ToolBase*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ToolBase; + +gboolean sp_event_context_snap_watchdog_callback(gpointer data); +void sp_event_context_discard_delayed_snap_event(ToolBase *ec); + +class DelayedSnapEvent +{ +public: + enum DelayedSnapEventOrigin { + UNDEFINED_HANDLER = 0, + EVENTCONTEXT_ROOT_HANDLER, + EVENTCONTEXT_ITEM_HANDLER, + KNOT_HANDLER, + CONTROL_POINT_HANDLER, + GUIDE_HANDLER, + GUIDE_HRULER, + GUIDE_VRULER + }; + + DelayedSnapEvent(ToolBase *event_context, gpointer const dse_item, gpointer dse_item2, GdkEventMotion const *event, DelayedSnapEvent::DelayedSnapEventOrigin const origin) + : _timer_id(0), _event(NULL), _item(dse_item), _item2(dse_item2), _origin(origin), _event_context(event_context) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value = prefs->getDoubleLimited("/options/snapdelay/value", 0, 0, 1000); + _timer_id = g_timeout_add(value, &sp_event_context_snap_watchdog_callback, this); + _event = gdk_event_copy((GdkEvent*) event); + ((GdkEventMotion *)_event)->time = GDK_CURRENT_TIME; + } + + ~DelayedSnapEvent() { + if (_timer_id > 0) g_source_remove(_timer_id); // Kill the watchdog + if (_event != NULL) gdk_event_free(_event); // Remove the copy of the original event + } + + ToolBase* getEventContext() {return _event_context;} + DelayedSnapEventOrigin getOrigin() {return _origin;} + GdkEvent* getEvent() {return _event;} + gpointer getItem() {return _item;} + gpointer getItem2() {return _item2;} + +private: + guint _timer_id; + GdkEvent* _event; + gpointer _item; + gpointer _item2; + DelayedSnapEventOrigin _origin; + ToolBase* _event_context; +}; + +void sp_event_context_snap_delay_handler(ToolBase *ec, gpointer const dse_item, gpointer const dse_item2, GdkEventMotion *event, DelayedSnapEvent::DelayedSnapEventOrigin origin); + + +/** + * Base class for Event processors. + * + * This is per desktop object, which (its derivatives) implements + * different actions bound to mouse events. + * + * ToolBase is an abstract base class of all tools. As the name + * indicates, event context implementations process UI events (mouse + * movements and keypresses) and take actions (like creating or modifying + * objects). There is one event context implementation for each tool, + * plus few abstract base classes. Writing a new tool involves + * subclassing ToolBase. + */ +class ToolBase { +public: + void enableSelectionCue (bool enable=true); + void enableGrDrag (bool enable=true); + bool deleteSelectedDrag(bool just_one); + + ToolBase(); + virtual ~ToolBase(); + + SPDesktop *desktop; + Inkscape::Preferences::Observer *pref_observer; + gchar const *const *cursor_shape; + gint hot_x, hot_y; ///< indicates the cursor's hot spot + GdkCursor *cursor; + + gint xp, yp; ///< where drag started + gint tolerance; + bool within_tolerance; ///< are we still within tolerance of origin + + SPItem *item_to_select; ///< the item where mouse_press occurred, to + ///< be selected if this is a click not drag + + Inkscape::MessageContext *defaultMessageContext() { + return message_context; + } + + Inkscape::MessageContext *message_context; + + Inkscape::SelCue *_selcue; + + GrDrag *_grdrag; + GrDrag *get_drag () {return _grdrag;} + + ShapeEditor* shape_editor; + + bool space_panning; + + DelayedSnapEvent *_delayed_snap_event; + bool _dse_callback_in_process; + + virtual void setup(); + virtual void finish(); + + // Is called by our pref_observer if a preference has been changed. + virtual void set(const Inkscape::Preferences::Entry& val); + + virtual void activate(); + virtual void deactivate(); + + virtual bool root_handler(GdkEvent* event); + virtual bool item_handler(SPItem* item, GdkEvent* event); + + virtual const std::string& getPrefsPath() = 0; + + /** + * An observer that relays pref changes to the derived classes. + */ + class ToolPrefObserver: public Inkscape::Preferences::Observer { + public: + ToolPrefObserver(Glib::ustring const &path, ToolBase *ec) : + Inkscape::Preferences::Observer(path), ec(ec) { + } + + virtual void notify(Inkscape::Preferences::Entry const &val) { + ec->set(val); + } + + private: + ToolBase * const ec; + }; + +//protected: + void sp_event_context_update_cursor(); + +private: + ToolBase(const ToolBase&); + ToolBase& operator=(const ToolBase&); +}; + +#define SP_EVENT_CONTEXT_DESKTOP(e) (SP_EVENT_CONTEXT(e)->desktop) +#define SP_EVENT_CONTEXT_DOCUMENT(e) ((SP_EVENT_CONTEXT_DESKTOP(e))->doc()) + +#define SP_EVENT_CONTEXT_STATIC 0 + +//ToolBase *sp_event_context_new(GType type, SPDesktop *desktop, gchar const *pref_path, unsigned key); +//void sp_event_context_finish(ToolBase *ec); +void sp_event_context_read(ToolBase *ec, gchar const *key); +void sp_event_context_activate(ToolBase *ec); +//void sp_event_context_deactivate(ToolBase *ec); + +gint sp_event_context_root_handler(ToolBase *ec, GdkEvent *event); +gint sp_event_context_virtual_root_handler(ToolBase *ec, GdkEvent *event); +gint sp_event_context_item_handler(ToolBase *ec, SPItem *item, GdkEvent *event); +gint sp_event_context_virtual_item_handler(ToolBase *ec, SPItem *item, GdkEvent *event); + +void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event); + +gint gobble_key_events(guint keyval, gint mask); +gint gobble_motion_events(gint mask); + +//void sp_event_context_update_cursor(ToolBase *ec); + +void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, GdkEvent *event, + gchar const *ctrl_tip, gchar const *shift_tip, gchar const *alt_tip); + +guint get_group0_keyval(GdkEventKey *event); + +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); + +//ShapeEditor *sp_event_context_get_shape_editor (ToolBase *ec); +bool sp_event_context_knot_mouseover(ToolBase *ec); + +//void ec_shape_event_attr_changed(Inkscape::XML::Node *shape_repr, +// gchar const *name, gchar const *old_value, gchar const *new_value, +// bool const is_interactive, gpointer const data); +// +//void event_context_print_event_info(GdkEvent *event, bool print_return = true); + +} +} +} + +#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 000000000..0791eff5a --- /dev/null +++ b/src/ui/tools/tweak-tool.cpp @@ -0,0 +1,1524 @@ +/* + * tweaking paths without node editing + * + * Authors: + * bulia byak + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include "config.h" + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <numeric> + +#include "svg/svg.h" + +#include <glib.h> +#include "macros.h" +#include "document.h" +#include "document-undo.h" +#include "selection.h" +#include "desktop.h" +#include "desktop-events.h" +#include "desktop-handles.h" +#include "desktop-style.h" +#include "message-context.h" +#include "pixmaps/cursor-tweak-move.xpm" +#include "pixmaps/cursor-tweak-move-in.xpm" +#include "pixmaps/cursor-tweak-move-out.xpm" +#include "pixmaps/cursor-tweak-move-jitter.xpm" +#include "pixmaps/cursor-tweak-scale-up.xpm" +#include "pixmaps/cursor-tweak-scale-down.xpm" +#include "pixmaps/cursor-tweak-rotate-clockwise.xpm" +#include "pixmaps/cursor-tweak-rotate-counterclockwise.xpm" +#include "pixmaps/cursor-tweak-more.xpm" +#include "pixmaps/cursor-tweak-less.xpm" +#include "pixmaps/cursor-thin.xpm" +#include "pixmaps/cursor-thicken.xpm" +#include "pixmaps/cursor-attract.xpm" +#include "pixmaps/cursor-repel.xpm" +#include "pixmaps/cursor-push.xpm" +#include "pixmaps/cursor-roughen.xpm" +#include "pixmaps/cursor-color.xpm" +#include <boost/optional.hpp> +#include "xml/repr.h" +#include "context-fns.h" +#include "sp-item.h" +#include "inkscape.h" +#include "color.h" +#include "svg/svg-color.h" +#include "splivarot.h" +#include "sp-item-group.h" +#include "sp-shape.h" +#include "sp-path.h" +#include "path-chemistry.h" +#include "sp-gradient.h" +#include "sp-stop.h" +#include "sp-gradient-reference.h" +#include "sp-linear-gradient.h" +#include "sp-radial-gradient.h" +#include "gradient-chemistry.h" +#include "sp-text.h" +#include "sp-flowtext.h" +#include "display/sp-canvas.h" +#include "display/canvas-bpath.h" +#include "display/canvas-arena.h" +#include "display/curve.h" +#include "livarot/Shape.h" +#include <2geom/transforms.h> +#include <2geom/circle.h> +#include "preferences.h" +#include "style.h" +#include "box3d.h" +#include "sp-item-transform.h" +#include "filter-chemistry.h" +#include "filters/gaussian-blur.h" +#include "verbs.h" + +#include "ui/tools/tweak-tool.h" + +using Inkscape::DocumentUndo; + +#define DDC_RED_RGBA 0xff0000ff + +#define DYNA_MIN_WIDTH 1.0e-6 + +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createTweakContext() { + return new TweakTool(); + } + + bool tweakContextRegistered = ToolFactory::instance().registerObject("/tools/tweak", createTweakContext); +} + +const std::string& TweakTool::getPrefsPath() { + return TweakTool::prefsPath; +} + +const std::string TweakTool::prefsPath = "/tools/tweak"; + +TweakTool::TweakTool() : ToolBase() { + this->mode = 0; + this->dilate_area = 0; + this->usetilt = 0; + this->usepressure = 0; + this->is_drawing = false; + this->fidelity = 0; + + this->cursor_shape = cursor_push_xpm; + this->hot_x = 4; + this->hot_y = 4; + + /* attributes */ + this->dragging = FALSE; + + this->width = 0.2; + this->force = 0.2; + this->pressure = TC_DEFAULT_PRESSURE; + + this->is_dilating = false; + this->has_dilated = false; + + this->do_h = true; + this->do_s = true; + this->do_l = true; + this->do_o = false; +} + +TweakTool::~TweakTool() { + this->enableGrDrag(false); + + this->style_set_connection.disconnect(); + + if (this->dilate_area) { + sp_canvas_item_destroy(this->dilate_area); + this->dilate_area = NULL; + } +} + +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 = NULL; + + if (!desktop->selection->isEmpty()) { + num = g_slist_length(const_cast<GSList *>(desktop->selection->itemList())); + 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->cursor_shape = cursor_tweak_move_xpm; + 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->cursor_shape = cursor_tweak_move_out_xpm; + } else { + this->cursor_shape = cursor_tweak_move_in_xpm; + } + 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->cursor_shape = cursor_tweak_move_jitter_xpm; + 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->cursor_shape = cursor_tweak_scale_up_xpm; + } else { + this->cursor_shape = cursor_tweak_scale_down_xpm; + } + 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->cursor_shape = cursor_tweak_rotate_counterclockwise_xpm; + } else { + this->cursor_shape = cursor_tweak_rotate_clockwise_xpm; + } + 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->cursor_shape = cursor_tweak_less_xpm; + } else { + this->cursor_shape = cursor_tweak_more_xpm; + } + break; + case TWEAK_MODE_PUSH: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag to <b>push paths</b>."), sel_message); + this->cursor_shape = cursor_push_xpm; + 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->cursor_shape = cursor_thicken_xpm; + } else { + this->cursor_shape = cursor_thin_xpm; + } + 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->cursor_shape = cursor_repel_xpm; + } else { + this->cursor_shape = cursor_attract_xpm; + } + break; + case TWEAK_MODE_ROUGHEN: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>roughen paths</b>."), sel_message); + this->cursor_shape = cursor_roughen_xpm; + 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->cursor_shape = cursor_color_xpm; + break; + case TWEAK_MODE_COLORJITTER: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>randomize colors</b>."), sel_message); + this->cursor_shape = cursor_color_xpm; + 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->cursor_shape = cursor_color_xpm; + break; + } + + this->sp_event_context_update_cursor(); + 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::setup() { + ToolBase::setup(); + + { + /* TODO: have a look at sp_dyna_draw_context_setup where the same is done.. generalize? at least make it an arcto! */ + Geom::PathVector path; + Geom::Circle(0, 0, 1).getPath(path); + + SPCurve *c = new SPCurve(path); + + this->dilate_area = sp_canvas_bpath_new(sp_desktop_controls(this->desktop), c); + c->unref(); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->dilate_area), 0x00000000,(SPWindRule)0); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->dilate_area), 0xff9900ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_hide(this->dilate_area); + } + + this->is_drawing = false; + + sp_event_context_read(this, "width"); + sp_event_context_read(this, "mode"); + sp_event_context_read(this, "fidelity"); + sp_event_context_read(this, "force"); + sp_event_context_read(this, "usepressure"); + sp_event_context_read(this, "doh"); + sp_event_context_read(this, "dol"); + sp_event_context_read(this, "dos"); + sp_event_context_read(this, "doo"); + + this->style_set_connection = this->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(); + } +} + +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/SP_EVENT_CONTEXT(tc)->desktop->current_zoom(); +} + +static double +get_path_force (TweakTool *tc) +{ + double force = 8 * (tc->usepressure? tc->pressure : TC_DEFAULT_PRESSURE) + /sqrt(SP_EVENT_CONTEXT(tc)->desktop->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; + + if (SP_IS_BOX3D(item) && !is_transform_mode(mode) && !is_color_mode(mode)) { + // convert 3D boxes to ordinary groups before tweaking their shapes + item = box3d_convert_to_group(SP_BOX3D(item)); + selection->add(item); + } + + if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + GSList *items = g_slist_prepend (NULL, item); + GSList *selected = NULL; + GSList *to_select = NULL; + SPDocument *doc = item->document; + sp_item_list_to_curves (items, &selected, &to_select); + g_slist_free (items); + SPObject* newObj = doc->getObjectByRepr(static_cast<Inkscape::XML::Node *>(to_select->data)); + g_slist_free (to_select); + item = SP_ITEM(newObj); + selection->add(item); + } + + if (SP_IS_GROUP(item) && !SP_IS_BOX3D(item)) { + GSList *children = NULL; + for (SPObject *child = item->firstChild() ; child; child = child->getNext() ) { + if (SP_IS_ITEM(child)) { + children = g_slist_prepend(children, child); + } + } + + for (GSList *i = children; i; i = i->next) { + SPItem *child = SP_ITEM(i->data); + if (sp_tweak_dilate_recursive (selection, SP_ITEM(child), p, vector, mode, radius, force, fidelity, reverse)) + did = true; + } + + g_slist_free(children); + + } 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; + sp_item_move_rel(item, Geom::Translate(move[Geom::X], -move[Geom::Y])); + 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())); + sp_item_move_rel(item, Geom::Translate(move[Geom::X], -move[Geom::Y])); + 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); + sp_item_move_rel(item, Geom::Translate(move[Geom::X], -move[Geom::Y])); + 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); + sp_item_scale_rel(item, 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; + sp_item_rotate_rel(item, 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 + sp_object_ref(item, NULL); + item->deleteObject(true, true); + sp_object_unref(item, NULL); + } 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 (SP_IS_PATH(item) || SP_IS_SHAPE(item)) { + + Inkscape::XML::Node *newrepr = NULL; + gint pos = 0; + Inkscape::XML::Node *parent = NULL; + char const *id = NULL; + if (!SP_IS_PATH(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 == NULL) { + 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", NULL); + 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 + parent->appendChild(newrepr); + // move to the saved position + newrepr->setPosition(pos > 0 ? pos : 0); + + 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 { + if (SP_IS_LPE_ITEM(item) && SP_LPE_ITEM(item)->hasPathEffectRecursive()) { + item->getRepr()->setAttribute("inkscape:original-d", str); + } else { + item->getRepr()->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 = NULL; + } + } + + 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]; + sp_color_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]; + sp_color_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]; + } + sp_color_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]; + sp_color_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; + } + + sp_color_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 || !SP_IS_GRADIENT(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 + + double pos = 0; + double r = 0; + if (SP_IS_LINEARGRADIENT(gradient)) { + SPLinearGradient *lg = SP_LINEARGRADIENT(gradient); + + 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 lenfth-of-gradient-line units + r = radius / vl; + + } else if (SP_IS_RADIALGRADIENT(gradient)) { + SPRadialGradient *rg = SP_RADIALGRADIENT(gradient); + 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 accound 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 = NULL; + for (SPObject *child = vector->firstChild(); child; child = child->getNext()) { + if (!SP_IS_STOP(child)) { + continue; + } + SPStop *stop = SP_STOP (child); + + offset_h = stop->offset; + + if (child_prev) { + + 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->specified_color.v.c, rgb_goal, + force * (pos_e - offset_l) / (offset_h - offset_l), + do_h, do_s, do_l); + tweak_color (mode, SP_STOP(child_prev)->specified_color.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, SP_STOP(child_prev)->specified_color.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->specified_color.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; + } +} + +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 (SP_IS_GROUP(item)) { + for (SPObject *child = item->firstChild() ; child; child = child->getNext() ) { + if (SP_IS_ITEM(child)) { + if (sp_tweak_color_recursive (mode, SP_ITEM(child), 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 + SPObject *primitive_obj = style->getFilter()->children; + while (primitive_obj) { + if (SP_IS_FILTER_PRIMITIVE(primitive_obj)) { + SPFilterPrimitive *primitive = SP_FILTER_PRIMITIVE(primitive_obj); + //if primitive is gaussianblur + if(SP_IS_GAUSSIANBLUR(primitive)) { + SPGaussianBlur * spblur = SP_GAUSSIANBLUR(primitive); + float num = spblur->stdDeviation.getNumber(); + blur_now += num * i2dt.descrim(); // sum all blurs in the filter + } + } + primitive_obj = primitive_obj->next; + } + } + 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) +{ + Inkscape::Selection *selection = sp_desktop_selection(SP_EVENT_CONTEXT(tc)->desktop); + SPDesktop *desktop = SP_EVENT_CONTEXT(tc)->desktop; + + if (selection->isEmpty()) { + return false; + } + + bool did = false; + double radius = get_dilate_radius(tc); + + SPItem *item_at_point = SP_EVENT_CONTEXT(tc)->desktop->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]; + sp_color_rgb_to_hsv_floatv (hsv, + SP_RGBA32_R_F(fill_goal), + SP_RGBA32_G_F(fill_goal), + SP_RGBA32_B_F(fill_goal)); + sp_color_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); + sp_color_rgb_to_hsv_floatv (hsv, + SP_RGBA32_R_F(stroke_goal), + SP_RGBA32_G_F(stroke_goal), + SP_RGBA32_B_F(stroke_goal)); + sp_color_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); + + for (GSList *items = g_slist_copy(const_cast<GSList *>(selection->itemList())); + items != NULL; + items = items->next) { + + SPItem *item = SP_ITEM(items->data); + + 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(SP_EVENT_CONTEXT(tc)->desktop->point())); + sp_canvas_item_affine_absolute(tc->dilate_area, sm); + sp_canvas_item_show(tc->dilate_area); +} + +static void +sp_tweak_switch_mode (TweakTool *tc, gint mode, bool with_shift) +{ + SP_EVENT_CONTEXT(tc)->desktop->setToolboxSelectOneValue ("tweak_tool_mode", mode); + // 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); + SP_EVENT_CONTEXT(tc)->desktop->setToolboxSelectOneValue ("tweak_tool_mode", mode); + // 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: + sp_canvas_item_show(this->dilate_area); + break; + case GDK_LEAVE_NOTIFY: + sp_canvas_item_hide(this->dilate_area); + break; + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + + if (Inkscape::have_viable_layer(desktop, this->message_context) == 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); + + desktop->canvas->forceFullRedrawAfterInterruptions(3); + 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))); + sp_canvas_item_affine_absolute(this->dilate_area, sm); + sp_canvas_item_show(this->dilate_area); + + guint num = 0; + if (!desktop->selection->isEmpty()) { + num = g_slist_length(const_cast<GSList *>(desktop->selection->itemList())); + } + 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)); + + desktop->canvas->endForcedFullRedraws(); + this->is_drawing = false; + + if (this->is_dilating && event->button.button == 1 && !this->space_panning) { + 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; + switch (this->mode) { + case TWEAK_MODE_MOVE: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Move tweak")); + break; + case TWEAK_MODE_MOVE_IN_OUT: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Move in/out tweak")); + break; + case TWEAK_MODE_MOVE_JITTER: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Move jitter tweak")); + break; + case TWEAK_MODE_SCALE: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Scale tweak")); + break; + case TWEAK_MODE_ROTATE: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Rotate tweak")); + break; + case TWEAK_MODE_MORELESS: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Duplicate/delete tweak")); + break; + case TWEAK_MODE_PUSH: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Push path tweak")); + break; + case TWEAK_MODE_SHRINK_GROW: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Shrink/grow path tweak")); + break; + case TWEAK_MODE_ATTRACT_REPEL: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Attract/repel path tweak")); + break; + case TWEAK_MODE_ROUGHEN: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Roughen path tweak")); + break; + case TWEAK_MODE_COLORPAINT: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Color paint tweak")); + break; + case TWEAK_MODE_COLORJITTER: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Color jitter tweak")); + break; + case TWEAK_MODE_BLUR: + DocumentUndo::done(sp_desktop_document(SP_EVENT_CONTEXT(this)->desktop), + SP_VERB_CONTEXT_TWEAK, _("Blur tweak")); + break; + } + } + break; + } + case GDK_KEY_PRESS: + { + switch (get_group0_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 ("altx-tweak", 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 ("altx-tweak", 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 ("altx-tweak", 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 ("altx-tweak", 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 ("altx-tweak"); + 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_group0_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 000000000..6cbb9aded --- /dev/null +++ b/src/ui/tools/tweak-tool.h @@ -0,0 +1,106 @@ +#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, read the file 'COPYING' for more information + */ + +#include "ui/tools/tool-base.h" +#include <2geom/point.h> + +#define SAMPLING_SIZE 8 /* fixme: ?? */ + +#define TC_MIN_PRESSURE 0.0 +#define TC_MAX_PRESSURE 1.0 +#define TC_DEFAULT_PRESSURE 0.35 + +namespace Inkscape { +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(); + virtual ~TweakTool(); + + /* extended input data */ + gdouble pressure; + + /* attributes */ + guint dragging : 1; /* mouse state: mouse is dragging */ + guint usepressure : 1; + guint usetilt : 1; + + double width; + double force; + double fidelity; + + gint mode; + + bool is_drawing; + + bool is_dilating; + bool has_dilated; + Geom::Point last_push; + SPCanvasItem *dilate_area; + + bool do_h; + bool do_s; + bool do_l; + bool do_o; + + sigc::connection style_set_connection; + + static const std::string prefsPath; + + virtual void setup(); + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent* event); + + virtual const std::string& getPrefsPath(); + + 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 000000000..d4ede1053 --- /dev/null +++ b/src/ui/tools/zoom-tool.cpp @@ -0,0 +1,250 @@ +/* + * 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, read the file 'COPYING' for more information + */ + + +#include <gdk/gdkkeysyms.h> + +#include "macros.h" +#include "rubberband.h" +#include "display/sp-canvas-item.h" +#include "display/sp-canvas-util.h" +#include "desktop.h" +#include "pixmaps/cursor-zoom.xpm" +#include "pixmaps/cursor-zoom-out.xpm" +#include "preferences.h" +#include "selection-chemistry.h" + +#include "ui/tools/zoom-tool.h" +#include "tool-factory.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +namespace { + ToolBase* createZoomContext() { + return new ZoomTool(); + } + + bool zoomContextRegistered = ToolFactory::instance().registerObject("/tools/zoom", createZoomContext); +} + +const std::string& ZoomTool::getPrefsPath() { + return ZoomTool::prefsPath; +} + +const std::string ZoomTool::prefsPath = "/tools/zoom"; + +ZoomTool::ZoomTool() : ToolBase() { + this->grabbed = 0; + this->cursor_shape = cursor_zoom_xpm; + this->hot_x = 6; + this->hot_y = 6; + this->escaped = false; +} + +ZoomTool::~ZoomTool() { +} + +void ZoomTool::finish() { + this->enableGrDrag(false); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, GDK_CURRENT_TIME); + this->grabbed = NULL; + } +} + +void ZoomTool::setup() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/zoom/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/zoom/gradientdrag")) { + this->enableGrDrag(); + } + + ToolBase::setup(); +} + +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 && !this->space_panning) { + // 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_keep_point(button_dt, zoom_rel); + ret = true; + } + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK, + NULL, event->button.time); + + this->grabbed = SP_CANVAS_ITEM(desktop->acetate); + break; + } + + case GDK_MOTION_NOTIFY: + if ((event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + 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 && !this->space_panning) { + Geom::OptRect const b = Inkscape::Rubberband::get(desktop)->getRectangle(); + + if (b && !within_tolerance) { + 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_keep_point(button_dt, zoom_rel); + } + + ret = true; + } + + Inkscape::Rubberband::get(desktop)->stop(); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed, event->button.time); + this->grabbed = NULL; + } + + xp = yp = 0; + escaped = false; + break; + } + case GDK_KEY_PRESS: + switch (get_group0_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->cursor_shape = cursor_zoom_out_xpm; + this->sp_event_context_update_cursor(); + 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_group0_keyval (&event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->cursor_shape = cursor_zoom_xpm; + this->sp_event_context_update_cursor(); + 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 000000000..6e5cca7aa --- /dev/null +++ b/src/ui/tools/zoom-tool.h @@ -0,0 +1,47 @@ +#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, 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(); + virtual ~ZoomTool(); + + static const std::string prefsPath; + + virtual void setup(); + virtual void finish(); + virtual bool root_handler(GdkEvent* event); + + virtual const std::string& getPrefsPath(); + +private: + SPCanvasItem *grabbed; + bool escaped; +}; + +} +} +} + +#endif diff --git a/src/ui/widget/rotateable.cpp b/src/ui/widget/rotateable.cpp index 24a21e075..72ec69362 100644 --- a/src/ui/widget/rotateable.cpp +++ b/src/ui/widget/rotateable.cpp @@ -19,7 +19,7 @@ #include <gtkmm/eventbox.h> #include <glibmm/i18n.h> #include <2geom/point.h> -#include "event-context.h" +#include "ui/tools/tool-base.h" #include "rotateable.h" namespace Inkscape { diff --git a/src/ui/widget/selected-style.cpp b/src/ui/widget/selected-style.cpp index 000fdee4b..aa617353c 100644 --- a/src/ui/widget/selected-style.cpp +++ b/src/ui/widget/selected-style.cpp @@ -39,7 +39,7 @@ #include "sp-gradient.h" #include "svg/svg-color.h" #include "svg/css-ostringstream.h" -#include "event-context.h" +#include "ui/tools/tool-base.h" #include "message-context.h" #include "verbs.h" #include "color.h" diff --git a/src/ui/widget/spinbutton.cpp b/src/ui/widget/spinbutton.cpp index aa8f68ce8..7709a837b 100644 --- a/src/ui/widget/spinbutton.cpp +++ b/src/ui/widget/spinbutton.cpp @@ -16,7 +16,7 @@ #include "unit-menu.h" #include "unit-tracker.h" #include "util/expression-evaluator.h" -#include "event-context.h" +#include "ui/tools/tool-base.h" namespace Inkscape { namespace UI { |
