diff options
| author | Ted Gould <ted@gould.cx> | 2010-03-26 04:34:25 +0000 |
|---|---|---|
| committer | Ted Gould <ted@gould.cx> | 2010-03-26 04:34:25 +0000 |
| commit | 9e023a3aa964a0d3fa1e31e46d33657367ba68aa (patch) | |
| tree | 33f1392a340737e4eeefca6fd031f96c29befd2b /src/ui | |
| parent | Installing the pkgconfig file (diff) | |
| parent | Adding in shape-record.h (diff) | |
| download | inkscape-9e023a3aa964a0d3fa1e31e46d33657367ba68aa.tar.gz inkscape-9e023a3aa964a0d3fa1e31e46d33657367ba68aa.zip | |
Merge from trunk
(bzr r8254.1.53)
Diffstat (limited to 'src/ui')
67 files changed, 10935 insertions, 1697 deletions
diff --git a/src/ui/Makefile_insert b/src/ui/Makefile_insert index 3eb6c6b13..eb8966d11 100644 --- a/src/ui/Makefile_insert +++ b/src/ui/Makefile_insert @@ -9,4 +9,6 @@ ink_common_sources += \ ui/previewable.h \ ui/previewfillable.h \ ui/previewholder.cpp \ - ui/previewholder.h + ui/previewholder.h \ + ui/uxmanager.cpp \ + ui/uxmanager.h diff --git a/src/ui/clipboard.cpp b/src/ui/clipboard.cpp index af0ec0129..dd1f981b5 100644 --- a/src/ui/clipboard.cpp +++ b/src/ui/clipboard.cpp @@ -3,9 +3,11 @@ */ /* Authors: * Krzysztof Kosiński <tweenk@o2.pl> + * Jon A. Cruz <jon@joncruz.org> * Incorporates some code from selection-chemistry.cpp, see that file for more credits. * * Copyright (C) 2008 authors + * Copyright (C) 2010 Jon A. Cruz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -69,7 +71,6 @@ #include "svg/svg.h" // for sp_svg_transform_write, used in _copySelection #include "svg/css-ostringstream.h" // used in _parseColor #include "file.h" // for file_import, used in _pasteImage -#include "preferences.h" // for used in _pasteImage #include "text-context.h" #include "text-editing.h" #include "tools-switch.h" @@ -105,14 +106,14 @@ namespace UI { */ class ClipboardManagerImpl : public ClipboardManager { public: - virtual void copy(); + virtual void copy(SPDesktop *desktop); virtual void copyPathParameter(Inkscape::LivePathEffect::PathParam *); - virtual bool paste(bool in_place); - virtual bool pasteStyle(); - virtual bool pasteSize(bool, bool, bool); - virtual bool pastePathEffect(); - virtual Glib::ustring getPathParameter(); - virtual Glib::ustring getShapeOrTextObjectId(); + virtual bool paste(SPDesktop *desktop, bool in_place); + virtual bool pasteStyle(SPDesktop *desktop); + virtual bool pasteSize(SPDesktop *desktop, bool separately, bool apply_x, bool apply_y); + virtual bool pastePathEffect(SPDesktop *desktop); + virtual Glib::ustring getPathParameter(SPDesktop* desktop); + virtual Glib::ustring getShapeOrTextObjectId(SPDesktop *desktop); virtual const gchar *getFirstObjectID(); ClipboardManagerImpl(); @@ -126,10 +127,10 @@ private: void _copyTextPath(SPTextPath *); Inkscape::XML::Node *_copyNode(Inkscape::XML::Node *, Inkscape::XML::Document *, Inkscape::XML::Node *); - void _pasteDocument(SPDocument *, bool in_place); - void _pasteDefs(SPDocument *); - bool _pasteImage(); - bool _pasteText(); + void _pasteDocument(SPDesktop *desktop, SPDocument *clipdoc, bool in_place); + void _pasteDefs(SPDesktop *desktop, SPDocument *clipdoc); + bool _pasteImage(SPDocument *doc); + bool _pasteText(SPDesktop *desktop); SPCSSAttr *_parseColor(const Glib::ustring &); void _applyPathEffect(SPItem *, gchar const *); SPDocument *_retrieveClipboard(Glib::ustring = ""); @@ -142,7 +143,7 @@ private: void _createInternalClipboard(); void _discardInternalClipboard(); Inkscape::XML::Node *_createClipNode(); - Geom::Scale _getScale(Geom::Point const &, Geom::Point const &, Geom::Rect const &, bool, bool); + Geom::Scale _getScale(SPDesktop *desktop, Geom::Point const &min, Geom::Point const &max, Geom::Rect const &obj_rect, bool apply_x, bool apply_y); Glib::ustring _getBestTarget(); void _setClipboardTargets(); void _setClipboardColor(guint32); @@ -193,10 +194,11 @@ ClipboardManagerImpl::~ClipboardManagerImpl() {} /** * @brief Copy selection contents to the clipboard */ -void ClipboardManagerImpl::copy() +void ClipboardManagerImpl::copy(SPDesktop *desktop) { - SPDesktop *desktop = SP_ACTIVE_DESKTOP; - if ( desktop == NULL ) return; + if ( desktop == NULL ) { + return; + } Inkscape::Selection *selection = sp_desktop_selection(desktop); // Special case for when the gradient dragger is active - copies gradient color @@ -220,7 +222,9 @@ void ClipboardManagerImpl::copy() g_snprintf(color_str, 16, "#%06x", col >> 8); sp_repr_css_set_property(_text_style, "fill", color_str); float opacity = SP_RGBA32_A_F(col); - if (opacity > 1.0) opacity = 1.0; // safeguard + if (opacity > 1.0) { + opacity = 1.0; // safeguard + } Inkscape::CSSOStringStream opcss; opcss << opacity; sp_repr_css_set_property(_text_style, "opacity", opcss.str().data()); @@ -273,9 +277,13 @@ void ClipboardManagerImpl::copy() */ void ClipboardManagerImpl::copyPathParameter(Inkscape::LivePathEffect::PathParam *pp) { - if ( pp == NULL ) return; + if ( pp == NULL ) { + return; + } gchar *svgd = sp_svg_write_path( pp->get_pathvector() ); - if ( svgd == NULL || *svgd == '\0' ) return; + if ( svgd == NULL || *svgd == '\0' ) { + return; + } _discardInternalClipboard(); _createInternalClipboard(); @@ -294,12 +302,15 @@ void ClipboardManagerImpl::copyPathParameter(Inkscape::LivePathEffect::PathParam * @brief Paste from the system clipboard into the active desktop * @param in_place Whether to put the contents where they were when copied */ -bool ClipboardManagerImpl::paste(bool in_place) +bool ClipboardManagerImpl::paste(SPDesktop *desktop, bool in_place) { // do any checking whether we really are able to paste before requesting the contents - SPDesktop *desktop = SP_ACTIVE_DESKTOP; - if ( desktop == NULL ) return false; - if ( Inkscape::have_viable_layer(desktop, desktop->messageStack()) == false ) return false; + if ( desktop == NULL ) { + return false; + } + if ( Inkscape::have_viable_layer(desktop, desktop->messageStack()) == false ) { + return false; + } Glib::ustring target = _getBestTarget(); @@ -308,9 +319,13 @@ bool ClipboardManagerImpl::paste(bool in_place) // TODO: Handle x-special/gnome-copied-files and text/uri-list to support pasting files // if there is an image on the clipboard, paste it - if ( target == CLIPBOARD_GDK_PIXBUF_TARGET ) return _pasteImage(); + if ( target == CLIPBOARD_GDK_PIXBUF_TARGET ) { + return _pasteImage(desktop->doc()); + } // if there's only text, paste it into a selected text object or create a new one - if ( target == CLIPBOARD_TEXT_TARGET ) return _pasteText(); + if ( target == CLIPBOARD_TEXT_TARGET ) { + return _pasteText(desktop); + } // otherwise, use the import extensions SPDocument *tempdoc = _retrieveClipboard(target); @@ -319,7 +334,7 @@ bool ClipboardManagerImpl::paste(bool in_place) return false; } - _pasteDocument(tempdoc, in_place); + _pasteDocument(desktop, tempdoc, in_place); sp_document_unref(tempdoc); return true; @@ -338,8 +353,9 @@ const gchar *ClipboardManagerImpl::getFirstObjectID() Inkscape::XML::Node *root = sp_document_repr_root(tempdoc); - if (!root) + if (!root) { return NULL; + } Inkscape::XML::Node *ch = sp_repr_children(root); while (ch != NULL && @@ -349,8 +365,9 @@ const gchar *ClipboardManagerImpl::getFirstObjectID() strcmp(ch->name(), "svg:text") && strcmp(ch->name(), "svg:image") && strcmp(ch->name(), "svg:rect") - ) + ) { ch = ch->next(); + } if (ch) { return ch->attribute("id"); @@ -363,10 +380,11 @@ const gchar *ClipboardManagerImpl::getFirstObjectID() /** * @brief Implements the Paste Style action */ -bool ClipboardManagerImpl::pasteStyle() +bool ClipboardManagerImpl::pasteStyle(SPDesktop *desktop) { - SPDesktop *desktop = SP_ACTIVE_DESKTOP; - if (desktop == NULL) return false; + if (desktop == NULL) { + return false; + } // check whether something is selected Inkscape::Selection *selection = sp_desktop_selection(desktop); @@ -394,7 +412,7 @@ bool ClipboardManagerImpl::pasteStyle() bool pasted = false; if (clipnode) { - _pasteDefs(tempdoc); + _pasteDefs(desktop, tempdoc); SPCSSAttr *style = sp_repr_css_attr(clipnode, "style"); sp_desktop_set_style(desktop, style); pasted = true; @@ -414,12 +432,15 @@ bool ClipboardManagerImpl::pasteStyle() * @param apply_x Whether to scale the width of objects / selection * @param apply_y Whether to scale the height of objects / selection */ -bool ClipboardManagerImpl::pasteSize(bool separately, bool apply_x, bool apply_y) +bool ClipboardManagerImpl::pasteSize(SPDesktop *desktop, bool separately, bool apply_x, bool apply_y) { - if(!apply_x && !apply_y) return false; // pointless parameters + if (!apply_x && !apply_y) { + return false; // pointless parameters + } - SPDesktop *desktop = SP_ACTIVE_DESKTOP; - if ( desktop == NULL ) return false; + if ( desktop == NULL ) { + return false; + } Inkscape::Selection *selection = sp_desktop_selection(desktop); if (selection->isEmpty()) { _userWarn(desktop, _("Select <b>object(s)</b> to paste size to.")); @@ -447,8 +468,10 @@ bool ClipboardManagerImpl::pasteSize(bool separately, bool apply_x, bool apply_y for (GSList *i = const_cast<GSList*>(selection->itemList()) ; i ; i = i->next) { SPItem *item = SP_ITEM(i->data); Geom::OptRect obj_size = sp_item_bbox_desktop(item); - if ( !obj_size ) continue; - sp_item_scale_rel(item, _getScale(min, max, *obj_size, apply_x, apply_y)); + if ( !obj_size ) { + continue; + } + sp_item_scale_rel(item, _getScale(desktop, min, max, *obj_size, apply_x, apply_y)); } } // resize the selection as a whole @@ -456,7 +479,7 @@ bool ClipboardManagerImpl::pasteSize(bool separately, bool apply_x, bool apply_y Geom::OptRect sel_size = selection->bounds(); if ( sel_size ) { sp_selection_scale_relative(selection, sel_size->midpoint(), - _getScale(min, max, *sel_size, apply_x, apply_y)); + _getScale(desktop, min, max, *sel_size, apply_x, apply_y)); } } pasted = true; @@ -469,14 +492,14 @@ bool ClipboardManagerImpl::pasteSize(bool separately, bool apply_x, bool apply_y /** * @brief Applies a path effect from the clipboard to the selected path */ -bool ClipboardManagerImpl::pastePathEffect() +bool ClipboardManagerImpl::pastePathEffect(SPDesktop *desktop) { /** @todo FIXME: pastePathEffect crashes when moving the path with the applied effect, segfaulting in fork_private_if_necessary(). */ - SPDesktop *desktop = SP_ACTIVE_DESKTOP; - if ( desktop == NULL ) + if ( desktop == NULL ) { return false; + } Inkscape::Selection *selection = sp_desktop_selection(desktop); if (selection && selection->isEmpty()) { @@ -489,13 +512,14 @@ bool ClipboardManagerImpl::pastePathEffect() Inkscape::XML::Node *root = sp_document_repr_root(tempdoc); Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); if ( clipnode ) { - gchar const *effect = clipnode->attribute("inkscape:path-effect"); - if ( effect ) { - _pasteDefs(tempdoc); + gchar const *effectstack = clipnode->attribute("inkscape:path-effect"); + if ( effectstack ) { + _pasteDefs(desktop, tempdoc); // make sure all selected items are converted to paths first (i.e. rectangles) - sp_selected_path_to_curves(desktop, false); - for (GSList *item = const_cast<GSList *>(selection->itemList()) ; item ; item = item->next) { - _applyPathEffect(reinterpret_cast<SPItem*>(item->data), effect); + sp_selected_to_lpeitems(desktop); + for (GSList *itemptr = const_cast<GSList *>(selection->itemList()) ; itemptr ; itemptr = itemptr->next) { + SPItem *item = reinterpret_cast<SPItem*>(itemptr->data); + _applyPathEffect(item, effectstack); } return true; @@ -513,18 +537,18 @@ bool ClipboardManagerImpl::pastePathEffect() * @brief Get LPE path data from the clipboard * @return The retrieved path data (contents of the d attribute), or "" if no path was found */ -Glib::ustring ClipboardManagerImpl::getPathParameter() +Glib::ustring ClipboardManagerImpl::getPathParameter(SPDesktop* desktop) { SPDocument *tempdoc = _retrieveClipboard(); // any target will do here if ( tempdoc == NULL ) { - _userWarn(SP_ACTIVE_DESKTOP, _("Nothing on the clipboard.")); + _userWarn(desktop, _("Nothing on the clipboard.")); return ""; } Inkscape::XML::Node *root = sp_document_repr_root(tempdoc), *path = sp_repr_lookup_name(root, "svg:path", -1); // unlimited search depth if ( path == NULL ) { - _userWarn(SP_ACTIVE_DESKTOP, _("Clipboard does not contain a path.")); + _userWarn(desktop, _("Clipboard does not contain a path.")); sp_document_unref(tempdoc); return ""; } @@ -537,21 +561,22 @@ Glib::ustring ClipboardManagerImpl::getPathParameter() * @brief Get object id of a shape or text item from the clipboard * @return The retrieved id string (contents of the id attribute), or "" if no shape or text item was found */ -Glib::ustring ClipboardManagerImpl::getShapeOrTextObjectId() +Glib::ustring ClipboardManagerImpl::getShapeOrTextObjectId(SPDesktop *desktop) { SPDocument *tempdoc = _retrieveClipboard(); // any target will do here if ( tempdoc == NULL ) { - _userWarn(SP_ACTIVE_DESKTOP, _("Nothing on the clipboard.")); + _userWarn(desktop, _("Nothing on the clipboard.")); return ""; } Inkscape::XML::Node *root = sp_document_repr_root(tempdoc); Inkscape::XML::Node *repr = sp_repr_lookup_name(root, "svg:path", -1); // unlimited search depth - if ( repr == NULL ) + if ( repr == NULL ) { repr = sp_repr_lookup_name(root, "svg:text", -1); + } if ( repr == NULL ) { - _userWarn(SP_ACTIVE_DESKTOP, _("Clipboard does not contain a path.")); + _userWarn(desktop, _("Clipboard does not contain a path.")); sp_document_unref(tempdoc); return ""; } @@ -576,7 +601,9 @@ void ClipboardManagerImpl::_copySelection(Inkscape::Selection *selection) sorted_items = g_slist_sort(sorted_items, (GCompareFunc) sp_object_compare_position); for (GSList *i = sorted_items ; i ; i = i->next) { - if (!SP_IS_ITEM(i->data)) continue; + if (!SP_IS_ITEM(i->data)) { + continue; + } Inkscape::XML::Node *obj = SP_OBJECT_REPR(i->data); Inkscape::XML::Node *obj_copy = _copyNode(obj, _doc, _root); @@ -595,7 +622,7 @@ void ClipboardManagerImpl::_copySelection(Inkscape::Selection *selection) // copy style for Paste Style action if (sorted_items) { - if(SP_IS_ITEM(sorted_items->data)) { + if (SP_IS_ITEM(sorted_items->data)) { SPCSSAttr *style = take_style_from_item((SPItem *) sorted_items->data); sp_repr_css_set(_clipnode, style, "style"); sp_repr_css_attr_unref(style); @@ -630,17 +657,21 @@ void ClipboardManagerImpl::_copyUsedDefs(SPItem *item) if (style && (style->fill.isPaintserver())) { SPObject *server = SP_OBJECT_STYLE_FILL_SERVER(item); - if (SP_IS_LINEARGRADIENT(server) || SP_IS_RADIALGRADIENT(server)) + if (SP_IS_LINEARGRADIENT(server) || SP_IS_RADIALGRADIENT(server)) { _copyGradient(SP_GRADIENT(server)); - if (SP_IS_PATTERN(server)) + } + if (SP_IS_PATTERN(server)) { _copyPattern(SP_PATTERN(server)); + } } if (style && (style->stroke.isPaintserver())) { SPObject *server = SP_OBJECT_STYLE_STROKE_SERVER(item); - if (SP_IS_LINEARGRADIENT(server) || SP_IS_RADIALGRADIENT(server)) + if (SP_IS_LINEARGRADIENT(server) || SP_IS_RADIALGRADIENT(server)) { _copyGradient(SP_GRADIENT(server)); - if (SP_IS_PATTERN(server)) + } + if (SP_IS_PATTERN(server)) { _copyPattern(SP_PATTERN(server)); + } } // For shapes, copy all of the shape's markers @@ -659,8 +690,9 @@ void ClipboardManagerImpl::_copyUsedDefs(SPItem *item) for (PathEffectList::iterator it = lpeitem->path_effect_list->begin(); it != lpeitem->path_effect_list->end(); ++it) { LivePathEffectObject *lpeobj = (*it)->lpeobject; - if (lpeobj) + if (lpeobj) { _copyNode(SP_OBJECT_REPR(SP_OBJECT(lpeobj)), _doc, _defs); + } } } } @@ -682,8 +714,9 @@ void ClipboardManagerImpl::_copyUsedDefs(SPItem *item) _copyNode(SP_OBJECT_REPR(mask), _doc, _defs); // recurse into the mask for its gradients etc. for (SPObject *o = SP_OBJECT(mask)->children ; o != NULL ; o = o->next) { - if (SP_IS_ITEM(o)) + if (SP_IS_ITEM(o)) { _copyUsedDefs(SP_ITEM(o)); + } } } // Copy filters @@ -696,8 +729,9 @@ void ClipboardManagerImpl::_copyUsedDefs(SPItem *item) // recurse for (SPObject *o = SP_OBJECT(item)->children ; o != NULL ; o = o->next) { - if (SP_IS_ITEM(o)) + if (SP_IS_ITEM(o)) { _copyUsedDefs(SP_ITEM(o)); + } } } @@ -726,7 +760,9 @@ void ClipboardManagerImpl::_copyPattern(SPPattern *pattern) // items in the pattern may also use gradients and other patterns, so recurse for (SPObject *child = sp_object_first_child(SP_OBJECT(pattern)) ; child != NULL ; child = SP_OBJECT_NEXT(child) ) { - if (!SP_IS_ITEM (child)) continue; + if (!SP_IS_ITEM (child)) { + continue; + } _copyUsedDefs(SP_ITEM(child)); } pattern = pattern->ref->getObject(); @@ -740,11 +776,15 @@ void ClipboardManagerImpl::_copyPattern(SPPattern *pattern) void ClipboardManagerImpl::_copyTextPath(SPTextPath *tp) { SPItem *path = sp_textpath_get_path_item(tp); - if(!path) return; + if (!path) { + return; + } Inkscape::XML::Node *path_node = SP_OBJECT_REPR(path); // Do not copy the text path to defs if it's already copied - if(sp_repr_lookup_child(_root, "id", path_node->attribute("id"))) return; + if (sp_repr_lookup_child(_root, "id", path_node->attribute("id"))) { + return; + } _copyNode(path_node, _doc, _defs); } @@ -771,9 +811,8 @@ Inkscape::XML::Node *ClipboardManagerImpl::_copyNode(Inkscape::XML::Node *node, * @param in_place Whether to paste the selection where it was when copied * @pre @c clipdoc is not empty and items can be added to the current layer */ -void ClipboardManagerImpl::_pasteDocument(SPDocument *clipdoc, bool in_place) +void ClipboardManagerImpl::_pasteDocument(SPDesktop *desktop, SPDocument *clipdoc, bool in_place) { - SPDesktop *desktop = SP_ACTIVE_DESKTOP; SPDocument *target_document = sp_desktop_document(desktop); Inkscape::XML::Node *root = sp_document_repr_root(clipdoc), @@ -781,16 +820,24 @@ void ClipboardManagerImpl::_pasteDocument(SPDocument *clipdoc, bool in_place) Inkscape::XML::Document *target_xmldoc = sp_document_repr_doc(target_document); // copy definitions - _pasteDefs(clipdoc); + _pasteDefs(desktop, clipdoc); // copy objects GSList *pasted_objects = NULL; for (Inkscape::XML::Node *obj = root->firstChild() ; obj ; obj = obj->next()) { // Don't copy metadata, defs, named views and internal clipboard contents to the document - if (!strcmp(obj->name(), "svg:defs")) continue; - if (!strcmp(obj->name(), "svg:metadata")) continue; - if (!strcmp(obj->name(), "sodipodi:namedview")) continue; - if (!strcmp(obj->name(), "inkscape:clipboard")) continue; + if (!strcmp(obj->name(), "svg:defs")) { + continue; + } + if (!strcmp(obj->name(), "svg:metadata")) { + continue; + } + if (!strcmp(obj->name(), "sodipodi:namedview")) { + continue; + } + if (!strcmp(obj->name(), "inkscape:clipboard")) { + continue; + } Inkscape::XML::Node *obj_copy = _copyNode(obj, target_xmldoc, target_parent); pasted_objects = g_slist_prepend(pasted_objects, (gpointer) obj_copy); } @@ -822,11 +869,12 @@ void ClipboardManagerImpl::_pasteDocument(SPDocument *clipdoc, bool in_place) if (!in_place) { SnapManager &m = desktop->namedview->snap_manager; - m.setup(desktop, false); // Don't display the snapindicator + m.setup(desktop); + sp_event_context_discard_delayed_snap_event(desktop->event_context); // get offset from mouse pointer to bbox center, snap to grid if enabled Geom::Point mouse_offset = desktop->point() - sel_bbox->midpoint(); - offset = m.multipleOfGridPitch(mouse_offset - offset) + offset; + offset = m.multipleOfGridPitch(mouse_offset - offset, sel_bbox->midpoint() + offset) + offset; } sp_selection_move_relative(selection, offset); @@ -841,10 +889,9 @@ void ClipboardManagerImpl::_pasteDocument(SPDocument *clipdoc, bool in_place) * @param clipdoc The document to paste * @pre @c clipdoc != NULL and pasting into the active document is possible */ -void ClipboardManagerImpl::_pasteDefs(SPDocument *clipdoc) +void ClipboardManagerImpl::_pasteDefs(SPDesktop *desktop, SPDocument *clipdoc) { // boilerplate vars copied from _pasteDocument - SPDesktop *desktop = SP_ACTIVE_DESKTOP; SPDocument *target_document = sp_desktop_document(desktop); Inkscape::XML::Node *root = sp_document_repr_root(clipdoc), @@ -863,35 +910,38 @@ void ClipboardManagerImpl::_pasteDefs(SPDocument *clipdoc) /** * @brief Retrieve a bitmap image from the clipboard and paste it into the active document */ -bool ClipboardManagerImpl::_pasteImage() +bool ClipboardManagerImpl::_pasteImage(SPDocument *doc) { - SPDocument *doc = SP_ACTIVE_DOCUMENT; - if ( doc == NULL ) return false; + if ( doc == NULL ) { + return false; + } // retrieve image data Glib::RefPtr<Gdk::Pixbuf> img = _clipboard->wait_for_image(); - if (!img) return false; - - // Very stupid hack: Write into a file, then import the file into the document. - // To avoid using tmpfile and POSIX file handles, make the filename based on current time. - // This wasn't my idea, I just copied this from selection-chemistry.cpp - // and just can't think of something saner at the moment. Pasting more than - // one image per second will overwrite the image. - // However, I don't think anyone is able to copy a _different_ image into inkscape - // in 1 second. - time_t rawtime; - char image_filename[128]; - - time(&rawtime); - strftime(image_filename, 128, "inkscape_pasted_image_%Y%m%d_%H%M%S.png", localtime( &rawtime )); - /// @todo Check whether the encoding is correct here - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - std::string save_folder = Glib::filename_from_utf8(prefs->getString("/dialogs/save_as/path")); - - gchar *image_path = g_build_filename(save_folder.data(), image_filename, NULL); - img->save(image_path, "png"); - file_import(doc, image_path, NULL); - g_free(image_path); + if (!img) { + return false; + } + + // TODO unify with interface.cpp's sp_ui_drag_data_received() + // AARGH stupid + Inkscape::Extension::DB::InputList o; + Inkscape::Extension::db.get_input_list(o); + Inkscape::Extension::DB::InputList::const_iterator i = o.begin(); + while (i != o.end() && strcmp( (*i)->get_mimetype(), "image/png" ) != 0) { + ++i; + } + Inkscape::Extension::Extension *png = *i; + bool save = (strcmp(png->get_param_optiongroup("link"), "embed") == 0); + png->set_param_optiongroup("link", "embed"); + png->set_gui(false); + + gchar *filename = g_build_filename( g_get_tmp_dir(), "inkscape-clipboard-import", NULL ); + img->save(filename, "png"); + file_import(doc, filename, png); + g_free(filename); + + png->set_param_optiongroup("link", save ? "embed" : "link"); + png->set_gui(true); return true; } @@ -899,14 +949,16 @@ bool ClipboardManagerImpl::_pasteImage() /** * @brief Paste text into the selected text object or create a new one to hold it */ -bool ClipboardManagerImpl::_pasteText() +bool ClipboardManagerImpl::_pasteText(SPDesktop *desktop) { - SPDesktop *desktop = SP_ACTIVE_DESKTOP; - if ( desktop == NULL ) return false; + if ( desktop == NULL ) { + return false; + } // if the text editing tool is active, paste the text into the active text object - if (tools_isactive(desktop, TOOLS_TEXT)) + if (tools_isactive(desktop, TOOLS_TEXT)) { return sp_text_paste_inline(desktop->event_context); + } // try to parse the text as a color and, if successful, apply it as the current style SPCSSAttr *css = _parseColor(_clipboard->wait_for_text()); @@ -930,29 +982,43 @@ SPCSSAttr *ClipboardManagerImpl::_parseColor(const Glib::ustring &text) Glib::ustring::size_type len = text.bytes(); char *str = const_cast<char *>(text.data()); bool attempt_alpha = false; - if ( !str || ( *str == '\0' ) ) return NULL; // this is OK due to boolean short-circuit + if ( !str || ( *str == '\0' ) ) { + return NULL; // this is OK due to boolean short-circuit + } // those conditionals guard against parsing e.g. the string "fab" as "fab000" // (incomplete color) and "45fab71" as "45fab710" (incomplete alpha) if ( *str == '#' ) { - if ( len < 7 ) return NULL; - if ( len >= 9 ) attempt_alpha = true; + if ( len < 7 ) { + return NULL; + } + if ( len >= 9 ) { + attempt_alpha = true; + } } else { - if ( len < 6 ) return NULL; - if ( len >= 8 ) attempt_alpha = true; + if ( len < 6 ) { + return NULL; + } + if ( len >= 8 ) { + attempt_alpha = true; + } } unsigned int color = 0, alpha = 0xff; // skip a leading #, if present - if ( *str == '#' ) ++str; + if ( *str == '#' ) { + ++str; + } // try to parse first 6 digits int res = sscanf(str, "%6x", &color); if ( res && ( res != EOF ) ) { if (attempt_alpha) {// try to parse alpha if there's enough characters sscanf(str + 6, "%2x", &alpha); - if ( !res || res == EOF ) alpha = 0xff; + if ( !res || res == EOF ) { + alpha = 0xff; + } } SPCSSAttr *color_css = sp_repr_css_attr_new(); @@ -963,7 +1029,9 @@ SPCSSAttr *ClipboardManagerImpl::_parseColor(const Glib::ustring &text) sp_repr_css_set_property(color_css, "fill", color_str); float opacity = static_cast<float>(alpha)/static_cast<float>(0xff); - if (opacity > 1.0) opacity = 1.0; // safeguard + if (opacity > 1.0) { + opacity = 1.0; // safeguard + } Inkscape::CSSOStringStream opcss; opcss << opacity; sp_repr_css_set_property(color_css, "fill-opacity", opcss.str().data()); @@ -976,19 +1044,31 @@ SPCSSAttr *ClipboardManagerImpl::_parseColor(const Glib::ustring &text) /** * @brief Applies a pasted path effect to a given item */ -void ClipboardManagerImpl::_applyPathEffect(SPItem *item, gchar const *effect) +void ClipboardManagerImpl::_applyPathEffect(SPItem *item, gchar const *effectstack) { - if ( item == NULL ) return; - if ( SP_IS_RECT(item) ) return; + if ( item == NULL ) { + return; + } + if ( SP_IS_RECT(item) ) { + return; + } if (SP_IS_LPE_ITEM(item)) { SPLPEItem *lpeitem = SP_LPE_ITEM(item); - SPObject *obj = sp_uri_reference_resolve(_clipboardSPDoc, effect); - if (!obj) return; - // if the effect is not used by anyone, we might as well take it - LivePathEffectObject *lpeobj = LIVEPATHEFFECT(obj)->fork_private_if_necessary(1); - sp_lpe_item_add_path_effect(lpeitem, lpeobj); + // for each effect in the stack, check if we need to fork it before adding it to the item + std::istringstream iss(effectstack); + std::string href; + while (std::getline(iss, href, ';')) + { + SPObject *obj = sp_uri_reference_resolve(_clipboardSPDoc, href.c_str()); + if (!obj) { + return; + } + // if the effectstack is not used by anyone, we might as well take it + LivePathEffectObject *lpeobj = LIVEPATHEFFECT(obj)->fork_private_if_necessary(1); + sp_lpe_item_add_path_effect(lpeitem, lpeobj); + } } } @@ -1000,10 +1080,11 @@ void ClipboardManagerImpl::_applyPathEffect(SPItem *item, gchar const *effect) SPDocument *ClipboardManagerImpl::_retrieveClipboard(Glib::ustring required_target) { Glib::ustring best_target; - if ( required_target == "" ) + if ( required_target == "" ) { best_target = _getBestTarget(); - else + } else { best_target = required_target; + } if ( best_target == "" ) { return NULL; @@ -1053,15 +1134,18 @@ SPDocument *ClipboardManagerImpl::_retrieveClipboard(Glib::ustring required_targ // there is no specific plain SVG input extension, so if we can paste the Inkscape SVG format, // we use the image/svg+xml mimetype to look up the input extension - if(target == "image/x-inkscape-svg") + if (target == "image/x-inkscape-svg") { target = "image/svg+xml"; + } Inkscape::Extension::DB::InputList inlist; Inkscape::Extension::db.get_input_list(inlist); Inkscape::Extension::DB::InputList::const_iterator in = inlist.begin(); - for (; in != inlist.end() && target != (*in)->get_mimetype() ; ++in){}; - if ( in == inlist.end() ) + for (; in != inlist.end() && target != (*in)->get_mimetype() ; ++in) { + }; + if ( in == inlist.end() ) { return NULL; // this shouldn't happen unless _getBestTarget returns something bogus + } SPDocument *tempdoc = NULL; try { @@ -1086,7 +1170,9 @@ void ClipboardManagerImpl::_onGet(Gtk::SelectionData &sel, guint /*info*/) g_assert( _clipboardSPDoc != NULL ); Glib::ustring target = sel.get_target(); - if(target == "") return; // this shouldn't happen + if (target == "") { + return; // this shouldn't happen + } if (target == CLIPBOARD_TEXT_TARGET) { target = "image/x-inkscape-svg"; @@ -1095,8 +1181,11 @@ void ClipboardManagerImpl::_onGet(Gtk::SelectionData &sel, guint /*info*/) Inkscape::Extension::DB::OutputList outlist; Inkscape::Extension::db.get_output_list(outlist); Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin(); - for ( ; out != outlist.end() && target != (*out)->get_mimetype() ; ++out){}; - if ( out == outlist.end() && target != "image/png") return; // this also shouldn't happen + for ( ; out != outlist.end() && target != (*out)->get_mimetype() ; ++out) { + }; + if ( out == outlist.end() && target != "image/png") { + return; // this also shouldn't happen + } // FIXME: Temporary hack until we add support for memory output. // Save to a temporary file, read it back and then set the clipboard contents @@ -1117,10 +1206,12 @@ void ClipboardManagerImpl::_onGet(Gtk::SelectionData &sel, guint /*info*/) // read from namedview Inkscape::XML::Node *nv = sp_repr_lookup_name (_clipboardSPDoc->rroot, "sodipodi:namedview"); - if (nv && nv->attribute("pagecolor")) + if (nv && nv->attribute("pagecolor")) { bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); - if (nv && nv->attribute("inkscape:pageopacity")) + } + if (nv && nv->attribute("inkscape:pageopacity")) { bgcolor |= SP_COLOR_F_TO_U(sp_repr_get_double_attribute (nv, "inkscape:pageopacity", 1.0)); + } sp_export_png_file(_clipboardSPDoc, filename, area, width, height, dpi, dpi, bgcolor, NULL, NULL, true, NULL); } @@ -1200,9 +1291,8 @@ void ClipboardManagerImpl::_discardInternalClipboard() /** * @brief Get the scale to resize an item, based on the command and desktop state */ -Geom::Scale ClipboardManagerImpl::_getScale(Geom::Point const &min, Geom::Point const &max, Geom::Rect const &obj_rect, bool apply_x, bool apply_y) +Geom::Scale ClipboardManagerImpl::_getScale(SPDesktop *desktop, Geom::Point const &min, Geom::Point const &max, Geom::Rect const &obj_rect, bool apply_x, bool apply_y) { - SPDesktop *desktop = SP_ACTIVE_DESKTOP; double scale_x = 1.0; double scale_y = 1.0; @@ -1215,8 +1305,12 @@ Geom::Scale ClipboardManagerImpl::_getScale(Geom::Point const &min, Geom::Point // If the "lock aspect ratio" button is pressed and we paste only a single coordinate, // resize the second one by the same ratio too if (desktop->isToolboxButtonActive("lock")) { - if (apply_x && !apply_y) scale_y = scale_x; - if (apply_y && !apply_x) scale_x = scale_y; + if (apply_x && !apply_y) { + scale_y = scale_x; + } + if (apply_y && !apply_x) { + scale_x = scale_y; + } } return Geom::Scale(scale_x, scale_y); @@ -1240,36 +1334,43 @@ Glib::ustring ClipboardManagerImpl::_getBestTarget() g_debug("End clipboard targets\n"); //*/ - for(std::list<Glib::ustring>::iterator i = _preferred_targets.begin() ; + for (std::list<Glib::ustring>::iterator i = _preferred_targets.begin() ; i != _preferred_targets.end() ; ++i) { - if ( std::find(targets.begin(), targets.end(), *i) != targets.end() ) + if ( std::find(targets.begin(), targets.end(), *i) != targets.end() ) { return *i; + } } #ifdef WIN32 if (OpenClipboard(NULL)) { // If both bitmap and metafile are present, pick the one that was exported first. UINT format = EnumClipboardFormats(0); while (format) { - if (format == CF_ENHMETAFILE || format == CF_DIB || format == CF_BITMAP) + if (format == CF_ENHMETAFILE || format == CF_DIB || format == CF_BITMAP) { break; + } format = EnumClipboardFormats(format); } CloseClipboard(); - if (format == CF_ENHMETAFILE) + if (format == CF_ENHMETAFILE) { return CLIPBOARD_WIN32_EMF_TARGET; - if (format == CF_DIB || format == CF_BITMAP) + } + if (format == CF_DIB || format == CF_BITMAP) { return CLIPBOARD_GDK_PIXBUF_TARGET; + } } - if (IsClipboardFormatAvailable(CF_ENHMETAFILE)) + if (IsClipboardFormatAvailable(CF_ENHMETAFILE)) { return CLIPBOARD_WIN32_EMF_TARGET; + } #endif - if (_clipboard->wait_is_image_available()) + if (_clipboard->wait_is_image_available()) { return CLIPBOARD_GDK_PIXBUF_TARGET; - if (_clipboard->wait_is_text_available()) + } + if (_clipboard->wait_is_text_available()) { return CLIPBOARD_TEXT_TARGET; + } return ""; } @@ -1324,7 +1425,8 @@ void ClipboardManagerImpl::_setClipboardTargets() Inkscape::Extension::DB::OutputList outlist; Inkscape::Extension::db.get_output_list(outlist); Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin(); - for ( ; out != outlist.end() && target != (*out)->get_mimetype() ; ++out); + for ( ; out != outlist.end() && target != (*out)->get_mimetype() ; ++out) { + } if ( out != outlist.end() ) { // FIXME: Temporary hack until we add support for memory output. // Save to a temporary file, read it back and then set the clipboard contents @@ -1382,18 +1484,20 @@ void ClipboardManagerImpl::_inkscape_wait_for_targets(std::list<Glib::ustring> & GdkAtom* targets = 0; gint n_targets = 0; gboolean test = gtk_clipboard_wait_for_targets( gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), &targets, &n_targets ); - if(!test) + if (!test) { n_targets = 0; //otherwise it will be -1. + } //Add the targets to the C++ container: - for(int i = 0; i < n_targets; i++) + for (int i = 0; i < n_targets; i++) { //Convert the atom to a string: gchar* const atom_name = gdk_atom_name(targets[i]); Glib::ustring target; - if(atom_name) + if (atom_name) { target = Glib::ScopedPtr<char>(atom_name).get(); //This frees the gchar*. + } listTargets.push_back(target); } @@ -1409,8 +1513,10 @@ ClipboardManager::ClipboardManager() {} ClipboardManager::~ClipboardManager() {} ClipboardManager *ClipboardManager::get() { - if ( _instance == NULL ) + if ( _instance == NULL ) { _instance = new ClipboardManagerImpl; + } + return _instance; } diff --git a/src/ui/clipboard.h b/src/ui/clipboard.h index 54c05ac0d..6020ecdd8 100644 --- a/src/ui/clipboard.h +++ b/src/ui/clipboard.h @@ -6,8 +6,10 @@ */ /* Authors: * Krzysztof Kosiński <tweenk@o2.pl> + * Jon A. Cruz <jon@joncruz.org> * * Copyright (C) 2008 authors + * Copyright (C) 2010 Jon A. Cruz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -39,16 +41,16 @@ namespace UI { class ClipboardManager { public: - virtual void copy() = 0; + virtual void copy(SPDesktop *desktop) = 0; virtual void copyPathParameter(Inkscape::LivePathEffect::PathParam *) = 0; - virtual bool paste(bool in_place = false) = 0; - virtual bool pasteStyle() = 0; - virtual bool pasteSize(bool separately, bool apply_x, bool apply_y) = 0; - virtual bool pastePathEffect() = 0; - virtual Glib::ustring getPathParameter() = 0; - virtual Glib::ustring getShapeOrTextObjectId() = 0; + virtual bool paste(SPDesktop *desktop, bool in_place = false) = 0; + virtual bool pasteStyle(SPDesktop *desktop) = 0; + virtual bool pasteSize(SPDesktop *desktop, bool separately, bool apply_x, bool apply_y) = 0; + virtual bool pastePathEffect(SPDesktop *desktop) = 0; + virtual Glib::ustring getPathParameter(SPDesktop* desktop) = 0; + virtual Glib::ustring getShapeOrTextObjectId(SPDesktop *desktop) = 0; virtual const gchar *getFirstObjectID() = 0; - + static ClipboardManager *get(); protected: ClipboardManager(); // singleton @@ -56,7 +58,7 @@ protected: private: ClipboardManager(const ClipboardManager &); ///< no copy ClipboardManager &operator=(const ClipboardManager &); ///< no assign - + static ClipboardManager *_instance; }; diff --git a/src/ui/dialog/CMakeLists.txt b/src/ui/dialog/CMakeLists.txt index ea63d6023..98c4a47bb 100644 --- a/src/ui/dialog/CMakeLists.txt +++ b/src/ui/dialog/CMakeLists.txt @@ -9,6 +9,7 @@ ENDIF(WIN32) SET(ui_dialog_SRC aboutbox.cpp align-and-distribute.cpp +color-item.cpp debug.cpp dialog.cpp dialog-manager.cpp diff --git a/src/ui/dialog/Makefile_insert b/src/ui/dialog/Makefile_insert index fac5bad80..033bec875 100644 --- a/src/ui/dialog/Makefile_insert +++ b/src/ui/dialog/Makefile_insert @@ -18,6 +18,8 @@ ink_common_sources += \ ui/dialog/behavior.h \ ui/dialog/calligraphic-profile-rename.h \ ui/dialog/calligraphic-profile-rename.cpp \ + ui/dialog/color-item.cpp \ + ui/dialog/color-item.h \ ui/dialog/debug.cpp \ ui/dialog/debug.h \ ui/dialog/dialog.cpp \ @@ -75,8 +77,6 @@ ink_common_sources += \ ui/dialog/print-colors-preview-dialog.h \ ui/dialog/scriptdialog.cpp \ ui/dialog/scriptdialog.h \ - ui/dialog/spray-option.cpp \ - ui/dialog/spray-option.h \ ui/dialog/svg-fonts-dialog.cpp \ ui/dialog/svg-fonts-dialog.h \ ui/dialog/swatches.cpp \ diff --git a/src/ui/dialog/aboutbox.cpp b/src/ui/dialog/aboutbox.cpp index 025bec37a..bbd02fa5d 100644 --- a/src/ui/dialog/aboutbox.cpp +++ b/src/ui/dialog/aboutbox.cpp @@ -103,8 +103,8 @@ AboutBox::AboutBox() : Gtk::Dialog(_("About Inkscape")) { Gtk::Label *label=new Gtk::Label(); gchar *label_text = - g_strdup_printf("<small><i>Inkscape %s, built %s</i></small>", - Inkscape::version_string, __DATE__); + g_strdup_printf("<small><i>Inkscape %s</i></small>", + Inkscape::version_string); label->set_markup(label_text); label->set_alignment(Gtk::ALIGN_RIGHT, Gtk::ALIGN_CENTER); g_free(label_text); diff --git a/src/ui/dialog/align-and-distribute.cpp b/src/ui/dialog/align-and-distribute.cpp index 2bba0a0f8..a75a8d68d 100644 --- a/src/ui/dialog/align-and-distribute.cpp +++ b/src/ui/dialog/align-and-distribute.cpp @@ -24,20 +24,20 @@ #include "unclump.h" #include "document.h" #include "enums.h" -#include "graphlayout/graphlayout.h" +#include "graphlayout.h" #include "inkscape.h" #include "macros.h" -#include "node-context.h" //For access to ShapeEditor #include "preferences.h" -#include "removeoverlap/removeoverlap.h" +#include "removeoverlap.h" #include "selection.h" -#include "shape-editor.h" //For node align/distribute methods #include "sp-flowtext.h" #include "sp-item-transform.h" #include "sp-text.h" #include "text-editing.h" #include "tools-switch.h" #include "ui/icon-names.h" +#include "ui/tool/node-tool.h" +#include "ui/tool/multi-path-manipulator.h" #include "util/glib-list-iterators.h" #include "verbs.h" #include "widgets/icon.h" @@ -429,12 +429,13 @@ private : if (!_dialog.getDesktop()) return; SPEventContext *event_context = sp_desktop_event_context(_dialog.getDesktop()); - if (!SP_IS_NODE_CONTEXT (event_context)) return ; + if (!INK_IS_NODE_TOOL (event_context)) return; + InkNodeTool *nt = INK_NODE_TOOL(event_context); if (_distribute) - event_context->shape_editor->distribute((Geom::Dim2)_orientation); + nt->_multipath->distributeNodes(_orientation); else - event_context->shape_editor->align((Geom::Dim2)_orientation); + nt->_multipath->alignNodes(_orientation); } }; diff --git a/src/ui/dialog/color-item.cpp b/src/ui/dialog/color-item.cpp new file mode 100644 index 000000000..cb6cfbbbe --- /dev/null +++ b/src/ui/dialog/color-item.cpp @@ -0,0 +1,837 @@ +/** @file + * @brief Inkscape color swatch UI item. + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <errno.h> +#include <glibmm/i18n.h> +#include <gtkmm/label.h> +#include <gtk/gtkdnd.h> + +#include "color-item.h" + +#include "desktop.h" +#include "desktop-handles.h" +#include "desktop-style.h" +#include "display/nr-plain-stuff.h" +#include "document.h" +#include "inkscape.h" // for SP_ACTIVE_DESKTOP +#include "io/resource.h" +#include "io/sys.h" +#include "message-context.h" +#include "sp-gradient.h" +#include "sp-item.h" +#include "svg/svg-color.h" +#include "xml/node.h" +#include "xml/repr.h" + +#include "color.h" // for SP_RGBA32_U_COMPOSE + + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +static std::vector<std::string> mimeStrings; +static std::map<std::string, guint> mimeToInt; + + +#if ENABLE_MAGIC_COLORS +// TODO remove this soon: +extern std::vector<SwatchPage*> possible; +#endif // ENABLE_MAGIC_COLORS + + +#if ENABLE_MAGIC_COLORS +static bool bruteForce( SPDocument* document, Inkscape::XML::Node* node, Glib::ustring const& match, int r, int g, int b ) +{ + bool changed = false; + + if ( node ) { + gchar const * val = node->attribute("inkscape:x-fill-tag"); + if ( val && (match == val) ) { + SPObject *obj = document->getObjectByRepr( node ); + + gchar c[64] = {0}; + sp_svg_write_color( c, sizeof(c), SP_RGBA32_U_COMPOSE( r, g, b, 0xff ) ); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, "fill", c ); + + sp_desktop_apply_css_recursive( obj, css, true ); + static_cast<SPItem*>(obj)->updateRepr(); + + changed = true; + } + + val = node->attribute("inkscape:x-stroke-tag"); + if ( val && (match == val) ) { + SPObject *obj = document->getObjectByRepr( node ); + + gchar c[64] = {0}; + sp_svg_write_color( c, sizeof(c), SP_RGBA32_U_COMPOSE( r, g, b, 0xff ) ); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, "stroke", c ); + + sp_desktop_apply_css_recursive( (SPItem*)obj, css, true ); + ((SPItem*)obj)->updateRepr(); + + changed = true; + } + + Inkscape::XML::Node* first = node->firstChild(); + changed |= bruteForce( document, first, match, r, g, b ); + + changed |= bruteForce( document, node->next(), match, r, g, b ); + } + + return changed; +} +#endif // ENABLE_MAGIC_COLORS + +static void handleClick( GtkWidget* /*widget*/, gpointer callback_data ) { + ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); + if ( item ) { + item->buttonClicked(false); + } +} + +static void handleSecondaryClick( GtkWidget* /*widget*/, gint /*arg1*/, gpointer callback_data ) { + ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); + if ( item ) { + item->buttonClicked(true); + } +} + +static gboolean handleEnterNotify( GtkWidget* /*widget*/, GdkEventCrossing* /*event*/, gpointer callback_data ) { + ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); + if ( item ) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if ( desktop ) { + gchar* msg = g_strdup_printf(_("Color: <b>%s</b>; <b>Click</b> to set fill, <b>Shift+click</b> to set stroke"), + item->def.descr.c_str()); + desktop->tipsMessageContext()->set(Inkscape::INFORMATION_MESSAGE, msg); + g_free(msg); + } + } + return FALSE; +} + +static gboolean handleLeaveNotify( GtkWidget* /*widget*/, GdkEventCrossing* /*event*/, gpointer callback_data ) { + ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); + if ( item ) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if ( desktop ) { + desktop->tipsMessageContext()->clear(); + } + } + return FALSE; +} + +static void dieDieDie( GtkObject *obj, gpointer user_data ) +{ + g_message("die die die %p %p", obj, user_data ); +} + +static bool getBlock( std::string& dst, guchar ch, std::string const str ) +{ + bool good = false; + std::string::size_type pos = str.find(ch); + if ( pos != std::string::npos ) + { + std::string::size_type pos2 = str.find( '(', pos ); + if ( pos2 != std::string::npos ) { + std::string::size_type endPos = str.find( ')', pos2 ); + if ( endPos != std::string::npos ) { + dst = str.substr( pos2 + 1, (endPos - pos2 - 1) ); + good = true; + } + } + } + return good; +} + +static bool popVal( guint64& numVal, std::string& str ) +{ + bool good = false; + std::string::size_type endPos = str.find(','); + if ( endPos == std::string::npos ) { + endPos = str.length(); + } + + if ( endPos != std::string::npos && endPos > 0 ) { + std::string xxx = str.substr( 0, endPos ); + const gchar* ptr = xxx.c_str(); + gchar* endPtr = 0; + numVal = g_ascii_strtoull( ptr, &endPtr, 10 ); + if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) { + // overflow + } else if ( (numVal == 0) && (endPtr == ptr) ) { + // failed conversion + } else { + good = true; + str.erase( 0, endPos + 1 ); + } + } + + return good; +} + +// TODO resolve this more cleanly: +extern gboolean colorItemHandleButtonPress( GtkWidget* /*widget*/, GdkEventButton* event, gpointer user_data); + +static void colorItemDragBegin( GtkWidget */*widget*/, GdkDragContext* dc, gpointer data ) +{ + ColorItem* item = reinterpret_cast<ColorItem*>(data); + if ( item ) + { + using Inkscape::IO::Resource::get_path; + using Inkscape::IO::Resource::ICONS; + using Inkscape::IO::Resource::SYSTEM; + int width = 32; + int height = 24; + + if (item->def.getType() != ege::PaintDef::RGB){ + GError *error = NULL; + gsize bytesRead = 0; + gsize bytesWritten = 0; + gchar *localFilename = g_filename_from_utf8( get_path(SYSTEM, ICONS, "remove-color.png"), + -1, + &bytesRead, + &bytesWritten, + &error); + GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file_at_scale(localFilename, width, height, FALSE, &error); + g_free(localFilename); + gtk_drag_set_icon_pixbuf( dc, pixbuf, 0, 0 ); + } else { + GdkPixbuf* pixbuf = 0; + if ( item->getGradient() ){ + guchar* px = g_new( guchar, 3 * height * width ); + nr_render_checkerboard_rgb( px, width, height, 3 * width, 0, 0 ); + + sp_gradient_render_vector_block_rgb( item->getGradient(), + px, width, height, 3 * width, + 0, width, TRUE ); + + pixbuf = gdk_pixbuf_new_from_data( px, GDK_COLORSPACE_RGB, FALSE, 8, + width, height, width * 3, + 0, // add delete function + 0 ); + } else { + Glib::RefPtr<Gdk::Pixbuf> thumb = Gdk::Pixbuf::create( Gdk::COLORSPACE_RGB, false, 8, width, height ); + guint32 fillWith = (0xff000000 & (item->def.getR() << 24)) + | (0x00ff0000 & (item->def.getG() << 16)) + | (0x0000ff00 & (item->def.getB() << 8)); + thumb->fill( fillWith ); + pixbuf = thumb->gobj(); + g_object_ref(G_OBJECT(pixbuf)); + } + gtk_drag_set_icon_pixbuf( dc, pixbuf, 0, 0 ); + } + } + +} + +//"drag-drop" +// gboolean dragDropColorData( GtkWidget *widget, +// GdkDragContext *drag_context, +// gint x, +// gint y, +// guint time, +// gpointer user_data) +// { +// // TODO finish + +// return TRUE; +// } + + +SwatchPage::SwatchPage() : + _name(), + _prefWidth(0), + _colors() +{ +} + +SwatchPage::~SwatchPage() +{ +} + + +ColorItem::ColorItem(ege::PaintDef::ColorType type) : + Previewable(), + def(type), + tips(), + _previews(), + _isFill(false), + _isStroke(false), + _isLive(false), + _linkIsTone(false), + _linkPercent(0), + _linkGray(0), + _linkSrc(0), + _grad(0), + _pixData(0), + _pixWidth(0), + _pixHeight(0), + _listeners() +{ +} + +ColorItem::ColorItem( unsigned int r, unsigned int g, unsigned int b, Glib::ustring& name ) : + Previewable(), + def( r, g, b, name ), + tips(), + _previews(), + _isFill(false), + _isStroke(false), + _isLive(false), + _linkIsTone(false), + _linkPercent(0), + _linkGray(0), + _linkSrc(0), + _grad(0), + _pixData(0), + _pixWidth(0), + _pixHeight(0), + _listeners() +{ +} + +ColorItem::~ColorItem() +{ +} + +ColorItem::ColorItem(ColorItem const &other) : + Inkscape::UI::Previewable() +{ + if ( this != &other ) { + *this = other; + } +} + +ColorItem &ColorItem::operator=(ColorItem const &other) +{ + if ( this != &other ) { + def = other.def; + + // TODO - correct linkage + _linkSrc = other._linkSrc; + g_message("Erk!"); + } + return *this; +} + +void ColorItem::setState( bool fill, bool stroke ) +{ + if ( (_isFill != fill) || (_isStroke != stroke) ) { + _isFill = fill; + _isStroke = stroke; + + for ( std::vector<Gtk::Widget*>::iterator it = _previews.begin(); it != _previews.end(); ++it ) { + Gtk::Widget* widget = *it; + if ( IS_EEK_PREVIEW(widget->gobj()) ) { + EekPreview * preview = EEK_PREVIEW(widget->gobj()); + + int val = eek_preview_get_linked( preview ); + val &= ~(PREVIEW_FILL | PREVIEW_STROKE); + if ( _isFill ) { + val |= PREVIEW_FILL; + } + if ( _isStroke ) { + val |= PREVIEW_STROKE; + } + eek_preview_set_linked( preview, static_cast<LinkType>(val) ); + } + } + } +} + +void ColorItem::setGradient(SPGradient *grad) +{ + if (_grad != grad) { + _grad = grad; + // TODO regen and push to listeners + } +} + +void ColorItem::setPixData(guchar* px, int width, int height) +{ + if (px != _pixData) { + if (_pixData) { + g_free(_pixData); + } + _pixData = px; + _pixWidth = width; + _pixHeight = height; + + _updatePreviews(); + } +} + +void ColorItem::_dragGetColorData( GtkWidget */*widget*/, + GdkDragContext */*drag_context*/, + GtkSelectionData *data, + guint info, + guint /*time*/, + gpointer user_data) +{ + ColorItem* item = reinterpret_cast<ColorItem*>(user_data); + std::string key; + if ( info < mimeStrings.size() ) { + key = mimeStrings[info]; + } else { + g_warning("ERROR: unknown value (%d)", info); + } + + if ( !key.empty() ) { + char* tmp = 0; + int len = 0; + int format = 0; + item->def.getMIMEData(key, tmp, len, format); + if ( tmp ) { + GdkAtom dataAtom = gdk_atom_intern( key.c_str(), FALSE ); + gtk_selection_data_set( data, dataAtom, format, (guchar*)tmp, len ); + delete[] tmp; + } + } +} + +void ColorItem::_dropDataIn( GtkWidget */*widget*/, + GdkDragContext */*drag_context*/, + gint /*x*/, gint /*y*/, + GtkSelectionData */*data*/, + guint /*info*/, + guint /*event_time*/, + gpointer /*user_data*/) +{ +} + +void ColorItem::_colorDefChanged(void* data) +{ + ColorItem* item = reinterpret_cast<ColorItem*>(data); + if ( item ) { + item->_updatePreviews(); + } +} + +void ColorItem::_updatePreviews() +{ + for ( std::vector<Gtk::Widget*>::iterator it = _previews.begin(); it != _previews.end(); ++it ) { + Gtk::Widget* widget = *it; + if ( IS_EEK_PREVIEW(widget->gobj()) ) { + EekPreview * preview = EEK_PREVIEW(widget->gobj()); + + _regenPreview(preview); + + widget->queue_draw(); + } + } + + for ( std::vector<ColorItem*>::iterator it = _listeners.begin(); it != _listeners.end(); ++it ) { + guint r = def.getR(); + guint g = def.getG(); + guint b = def.getB(); + + if ( (*it)->_linkIsTone ) { + r = ( ((*it)->_linkPercent * (*it)->_linkGray) + ((100 - (*it)->_linkPercent) * r) ) / 100; + g = ( ((*it)->_linkPercent * (*it)->_linkGray) + ((100 - (*it)->_linkPercent) * g) ) / 100; + b = ( ((*it)->_linkPercent * (*it)->_linkGray) + ((100 - (*it)->_linkPercent) * b) ) / 100; + } else { + r = ( ((*it)->_linkPercent * 255) + ((100 - (*it)->_linkPercent) * r) ) / 100; + g = ( ((*it)->_linkPercent * 255) + ((100 - (*it)->_linkPercent) * g) ) / 100; + b = ( ((*it)->_linkPercent * 255) + ((100 - (*it)->_linkPercent) * b) ) / 100; + } + + (*it)->def.setRGB( r, g, b ); + } + + +#if ENABLE_MAGIC_COLORS + // Look for objects using this color + { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if ( desktop ) { + SPDocument* document = sp_desktop_document( desktop ); + Inkscape::XML::Node *rroot = sp_document_repr_root( document ); + if ( rroot ) { + + // Find where this thing came from + Glib::ustring paletteName; + bool found = false; + int index = 0; + for ( std::vector<SwatchPage*>::iterator it2 = possible.begin(); it2 != possible.end() && !found; ++it2 ) { + SwatchPage* curr = *it2; + index = 0; + for ( std::vector<ColorItem*>::iterator zz = curr->_colors.begin(); zz != curr->_colors.end(); ++zz ) { + if ( this == *zz ) { + found = true; + paletteName = curr->_name; + break; + } else { + index++; + } + } + } + + if ( !paletteName.empty() ) { + gchar* str = g_strdup_printf("%d|", index); + paletteName.insert( 0, str ); + g_free(str); + str = 0; + + if ( bruteForce( document, rroot, paletteName, def.getR(), def.getG(), def.getB() ) ) { + sp_document_done( document , SP_VERB_DIALOG_SWATCHES, + _("Change color definition")); + } + } + } + } + } +#endif // ENABLE_MAGIC_COLORS + +} + +void ColorItem::_regenPreview(EekPreview * preview) +{ + if ( def.getType() != ege::PaintDef::RGB ) { + using Inkscape::IO::Resource::get_path; + using Inkscape::IO::Resource::ICONS; + using Inkscape::IO::Resource::SYSTEM; + GError *error = NULL; + gsize bytesRead = 0; + gsize bytesWritten = 0; + gchar *localFilename = g_filename_from_utf8( get_path(SYSTEM, ICONS, "remove-color.png"), + -1, + &bytesRead, + &bytesWritten, + &error); + GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(localFilename, &error); + if (!pixbuf) { + g_warning("Null pixbuf for %p [%s]", localFilename, localFilename ); + } + g_free(localFilename); + + eek_preview_set_pixbuf( preview, pixbuf ); + } + else if ( !_pixData ){ + eek_preview_set_color( preview, + (def.getR() << 8) | def.getR(), + (def.getG() << 8) | def.getG(), + (def.getB() << 8) | def.getB() ); + } else { + GdkPixbuf* pixbuf = gdk_pixbuf_new_from_data( _pixData, GDK_COLORSPACE_RGB, FALSE, 8, + _pixWidth, _pixHeight, _pixWidth * 3, + 0, // add delete function + 0 ); + eek_preview_set_pixbuf( preview, pixbuf ); + } + + eek_preview_set_linked( preview, (LinkType)((_linkSrc ? PREVIEW_LINK_IN:0) + | (_listeners.empty() ? 0:PREVIEW_LINK_OUT) + | (_isLive ? PREVIEW_LINK_OTHER:0)) ); +} + +Gtk::Widget* ColorItem::getPreview(PreviewStyle style, ViewType view, ::PreviewSize size, guint ratio) +{ + Gtk::Widget* widget = 0; + if ( style == PREVIEW_STYLE_BLURB) { + Gtk::Label *lbl = new Gtk::Label(def.descr); + lbl->set_alignment(Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER); + widget = lbl; + } else { +// Glib::ustring blank(" "); +// if ( size == Inkscape::ICON_SIZE_MENU || size == Inkscape::ICON_SIZE_DECORATION ) { +// blank = " "; +// } + + GtkWidget* eekWidget = eek_preview_new(); + EekPreview * preview = EEK_PREVIEW(eekWidget); + Gtk::Widget* newBlot = Glib::wrap(eekWidget); + + _regenPreview(preview); + + eek_preview_set_details( preview, (::PreviewStyle)style, (::ViewType)view, (::PreviewSize)size, ratio ); + + def.addCallback( _colorDefChanged, this ); + + GValue val = {0, {{0}, {0}}}; + g_value_init( &val, G_TYPE_BOOLEAN ); + g_value_set_boolean( &val, FALSE ); + g_object_set_property( G_OBJECT(preview), "focus-on-click", &val ); + +/* + Gtk::Button *btn = new Gtk::Button(blank); + Gdk::Color color; + color.set_rgb((_r << 8)|_r, (_g << 8)|_g, (_b << 8)|_b); + btn->modify_bg(Gtk::STATE_NORMAL, color); + btn->modify_bg(Gtk::STATE_ACTIVE, color); + btn->modify_bg(Gtk::STATE_PRELIGHT, color); + btn->modify_bg(Gtk::STATE_SELECTED, color); + + Gtk::Widget* newBlot = btn; +*/ + + tips.set_tip((*newBlot), def.descr); + +/* + newBlot->signal_clicked().connect( sigc::mem_fun(*this, &ColorItem::buttonClicked) ); + + sigc::signal<void> type_signal_something; +*/ + + g_signal_connect( G_OBJECT(newBlot->gobj()), + "clicked", + G_CALLBACK(handleClick), + this); + + g_signal_connect( G_OBJECT(newBlot->gobj()), + "alt-clicked", + G_CALLBACK(handleSecondaryClick), + this); + + g_signal_connect( G_OBJECT(newBlot->gobj()), + "button-press-event", + G_CALLBACK(colorItemHandleButtonPress), + this); + + { + std::vector<std::string> listing = def.getMIMETypes(); + int entryCount = listing.size(); + GtkTargetEntry* entries = new GtkTargetEntry[entryCount]; + GtkTargetEntry* curr = entries; + for ( std::vector<std::string>::iterator it = listing.begin(); it != listing.end(); ++it ) { + curr->target = g_strdup(it->c_str()); + curr->flags = 0; + if ( mimeToInt.find(*it) == mimeToInt.end() ){ + // these next lines are order-dependent: + mimeToInt[*it] = mimeStrings.size(); + mimeStrings.push_back(*it); + } + curr->info = mimeToInt[curr->target]; + curr++; + } + gtk_drag_source_set( GTK_WIDGET(newBlot->gobj()), + GDK_BUTTON1_MASK, + entries, entryCount, + GdkDragAction(GDK_ACTION_MOVE | GDK_ACTION_COPY) ); + for ( int i = 0; i < entryCount; i++ ) { + g_free(entries[i].target); + } + delete[] entries; + } + + g_signal_connect( G_OBJECT(newBlot->gobj()), + "drag-data-get", + G_CALLBACK(ColorItem::_dragGetColorData), + this); + + g_signal_connect( G_OBJECT(newBlot->gobj()), + "drag-begin", + G_CALLBACK(colorItemDragBegin), + this ); + + g_signal_connect( G_OBJECT(newBlot->gobj()), + "enter-notify-event", + G_CALLBACK(handleEnterNotify), + this); + + g_signal_connect( G_OBJECT(newBlot->gobj()), + "leave-notify-event", + G_CALLBACK(handleLeaveNotify), + this); + +// g_signal_connect( G_OBJECT(newBlot->gobj()), +// "drag-drop", +// G_CALLBACK(dragDropColorData), +// this); + + if ( def.isEditable() ) + { +// gtk_drag_dest_set( GTK_WIDGET(newBlot->gobj()), +// GTK_DEST_DEFAULT_ALL, +// destColorTargets, +// G_N_ELEMENTS(destColorTargets), +// GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE) ); + + +// g_signal_connect( G_OBJECT(newBlot->gobj()), +// "drag-data-received", +// G_CALLBACK(_dropDataIn), +// this ); + } + + g_signal_connect( G_OBJECT(newBlot->gobj()), + "destroy", + G_CALLBACK(dieDieDie), + this); + + + widget = newBlot; + } + + _previews.push_back( widget ); + + return widget; +} + +void ColorItem::buttonClicked(bool secondary) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + char const * attrName = secondary ? "stroke" : "fill"; + + SPCSSAttr *css = sp_repr_css_attr_new(); + Glib::ustring descr; + switch (def.getType()) { + case ege::PaintDef::CLEAR: { + // TODO actually make this clear + sp_repr_css_set_property( css, attrName, "none" ); + descr = secondary? _("Remove stroke color") : _("Remove fill color"); + break; + } + case ege::PaintDef::NONE: { + sp_repr_css_set_property( css, attrName, "none" ); + descr = secondary? _("Set stroke color to none") : _("Set fill color to none"); + break; + } + case ege::PaintDef::RGB: { + Glib::ustring colorspec; + if ( _grad ){ + colorspec = "url(#"; + colorspec += _grad->getId(); + colorspec += ")"; + } else { + gchar c[64]; + guint32 rgba = (def.getR() << 24) | (def.getG() << 16) | (def.getB() << 8) | 0xff; + sp_svg_write_color(c, sizeof(c), rgba); + colorspec = c; + } + sp_repr_css_set_property( css, attrName, colorspec.c_str() ); + descr = secondary? _("Set stroke color from swatch") : _("Set fill color from swatch"); + break; + } + } + sp_desktop_set_style(desktop, css); + sp_repr_css_attr_unref(css); + + sp_document_done( sp_desktop_document(desktop), SP_VERB_DIALOG_SWATCHES, descr.c_str() ); + } +} + +void ColorItem::_wireMagicColors( SwatchPage *colorSet ) +{ + if ( colorSet ) + { + for ( std::vector<ColorItem*>::iterator it = colorSet->_colors.begin(); it != colorSet->_colors.end(); ++it ) + { + std::string::size_type pos = (*it)->def.descr.find("*{"); + if ( pos != std::string::npos ) + { + std::string subby = (*it)->def.descr.substr( pos + 2 ); + std::string::size_type endPos = subby.find("}*"); + if ( endPos != std::string::npos ) + { + subby.erase( endPos ); + //g_message("FOUND MAGIC at '%s'", (*it)->def.descr.c_str()); + //g_message(" '%s'", subby.c_str()); + + if ( subby.find('E') != std::string::npos ) + { + (*it)->def.setEditable( true ); + } + + if ( subby.find('L') != std::string::npos ) + { + (*it)->_isLive = true; + } + + std::string part; + // Tint. index + 1 more val. + if ( getBlock( part, 'T', subby ) ) { + guint64 colorIndex = 0; + if ( popVal( colorIndex, part ) ) { + guint64 percent = 0; + if ( popVal( percent, part ) ) { + (*it)->_linkTint( *(colorSet->_colors[colorIndex]), percent ); + } + } + } + + // Shade/tone. index + 1 or 2 more val. + if ( getBlock( part, 'S', subby ) ) { + guint64 colorIndex = 0; + if ( popVal( colorIndex, part ) ) { + guint64 percent = 0; + if ( popVal( percent, part ) ) { + guint64 grayLevel = 0; + if ( !popVal( grayLevel, part ) ) { + grayLevel = 0; + } + (*it)->_linkTone( *(colorSet->_colors[colorIndex]), percent, grayLevel ); + } + } + } + + } + } + } + } +} + + +void ColorItem::_linkTint( ColorItem& other, int percent ) +{ + if ( !_linkSrc ) + { + other._listeners.push_back(this); + _linkIsTone = false; + _linkPercent = percent; + if ( _linkPercent > 100 ) + _linkPercent = 100; + if ( _linkPercent < 0 ) + _linkPercent = 0; + _linkGray = 0; + _linkSrc = &other; + + ColorItem::_colorDefChanged(&other); + } +} + +void ColorItem::_linkTone( ColorItem& other, int percent, int grayLevel ) +{ + if ( !_linkSrc ) + { + other._listeners.push_back(this); + _linkIsTone = true; + _linkPercent = percent; + if ( _linkPercent > 100 ) + _linkPercent = 100; + if ( _linkPercent < 0 ) + _linkPercent = 0; + _linkGray = grayLevel; + _linkSrc = &other; + + ColorItem::_colorDefChanged(&other); + } +} + +} // namespace Dialogs +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/color-item.h b/src/ui/dialog/color-item.h new file mode 100644 index 000000000..4aac86a30 --- /dev/null +++ b/src/ui/dialog/color-item.h @@ -0,0 +1,128 @@ +/** @file + * @brief Inkscape color swatch UI item. + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_DIALOGS_COLOR_ITEM_H +#define SEEN_DIALOGS_COLOR_ITEM_H + +#include <gtkmm/tooltips.h> + +#include "widgets/ege-paint-def.h" +#include "ui/previewable.h" + +class SPGradient; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +class ColorItem; + +class SwatchPage +{ +public: + SwatchPage(); + ~SwatchPage(); + + Glib::ustring _name; + int _prefWidth; + std::vector<ColorItem*> _colors; +}; + + +/** + * The color swatch you see on screen as a clickable box. + */ +class ColorItem : public Inkscape::UI::Previewable +{ + friend void _loadPaletteFile( gchar const *filename ); +public: + ColorItem( ege::PaintDef::ColorType type ); + ColorItem( unsigned int r, unsigned int g, unsigned int b, + Glib::ustring& name ); + virtual ~ColorItem(); + ColorItem(ColorItem const &other); + virtual ColorItem &operator=(ColorItem const &other); + virtual Gtk::Widget* getPreview(PreviewStyle style, + ViewType view, + ::PreviewSize size, + guint ratio); + void buttonClicked(bool secondary = false); + + void setGradient(SPGradient *grad); + SPGradient * getGradient() const { return _grad; } + + void setPixData(guchar* px, int width, int height); + + void setState( bool fill, bool stroke ); + bool isFill() { return _isFill; } + bool isStroke() { return _isStroke; } + + ege::PaintDef def; + +private: + + static void _dropDataIn( GtkWidget *widget, + GdkDragContext *drag_context, + gint x, gint y, + GtkSelectionData *data, + guint info, + guint event_time, + gpointer user_data); + + static void _dragGetColorData( GtkWidget *widget, + GdkDragContext *drag_context, + GtkSelectionData *data, + guint info, + guint time, + gpointer user_data); + + static void _wireMagicColors( SwatchPage *colorSet ); + static void _colorDefChanged(void* data); + + void _updatePreviews(); + void _regenPreview(EekPreview * preview); + + void _linkTint( ColorItem& other, int percent ); + void _linkTone( ColorItem& other, int percent, int grayLevel ); + + Gtk::Tooltips tips; + std::vector<Gtk::Widget*> _previews; + + bool _isFill; + bool _isStroke; + bool _isLive; + bool _linkIsTone; + int _linkPercent; + int _linkGray; + ColorItem* _linkSrc; + SPGradient* _grad; + guchar *_pixData; + int _pixWidth; + int _pixHeight; + std::vector<ColorItem*> _listeners; +}; + +} // namespace Dialogs +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOGS_COLOR_ITEM_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:encoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog-manager.cpp b/src/ui/dialog/dialog-manager.cpp index 2116d46c3..30cbed649 100644 --- a/src/ui/dialog/dialog-manager.cpp +++ b/src/ui/dialog/dialog-manager.cpp @@ -40,7 +40,6 @@ #include "ui/dialog/icon-preview.h" #include "ui/dialog/floating-behavior.h" #include "ui/dialog/dock-behavior.h" -#include "ui/dialog/spray-option.h" #include "ui/dialog/print-colors-preview-dialog.h" #include "preferences.h" @@ -114,7 +113,6 @@ DialogManager::DialogManager() { registerFactory("Transformation", &create<Transformation, FloatingBehavior>); registerFactory("UndoHistory", &create<UndoHistory, FloatingBehavior>); registerFactory("InputDevices", &create<InputDialog, FloatingBehavior>); - registerFactory("SprayOptionClass", &create<SprayOptionClass, FloatingBehavior>); } else { @@ -142,7 +140,6 @@ DialogManager::DialogManager() { registerFactory("Transformation", &create<Transformation, DockBehavior>); registerFactory("UndoHistory", &create<UndoHistory, DockBehavior>); registerFactory("InputDevices", &create<InputDialog, DockBehavior>); - registerFactory("SprayOptionClass", &create<SprayOptionClass, DockBehavior>); } } diff --git a/src/ui/dialog/document-properties.cpp b/src/ui/dialog/document-properties.cpp index a7241ea40..86baa85cd 100644 --- a/src/ui/dialog/document-properties.cpp +++ b/src/ui/dialog/document-properties.cpp @@ -222,7 +222,7 @@ DocumentProperties::build_page() Gtk::Label* label_bor = manage (new Gtk::Label); label_bor->set_markup (_("<b>Border</b>")); Gtk::Label *label_for = manage (new Gtk::Label); - label_for->set_markup (_("<b>Format</b>")); + label_for->set_markup (_("<b>Page Size</b>")); _page_sizer.init(); Gtk::Widget *const widget_array[] = diff --git a/src/ui/dialog/filedialogimpl-gtkmm.cpp b/src/ui/dialog/filedialogimpl-gtkmm.cpp index e650cf842..916e3ec97 100644 --- a/src/ui/dialog/filedialogimpl-gtkmm.cpp +++ b/src/ui/dialog/filedialogimpl-gtkmm.cpp @@ -521,7 +521,7 @@ bool SVGPreview::set(Glib::ustring &fileName, int dialogType) return FALSE; } long fileLen = info.st_size; - if (fileLen > 0x150000L) + if (fileLen > 0xA00000L) { showingNoPreview = false; showTooLarge(fileLen); diff --git a/src/ui/dialog/filedialogimpl-win32.cpp b/src/ui/dialog/filedialogimpl-win32.cpp index d22a368f2..0f3672f25 100644 --- a/src/ui/dialog/filedialogimpl-win32.cpp +++ b/src/ui/dialog/filedialogimpl-win32.cpp @@ -61,7 +61,7 @@ namespace Dialog const int PreviewWidening = 150; const char PreviewWindowClassName[] = "PreviewWnd"; -const unsigned long MaxPreviewFileSize = 1344; // kB +const unsigned long MaxPreviewFileSize = 10240; // kB #define IDC_SHOW_PREVIEW 1000 @@ -454,15 +454,15 @@ UINT_PTR CALLBACK FileOpenDialogImplWin32::GetOpenFileName_hookproc( pImpl = (FileOpenDialogImplWin32*)ofn->lCustData; // Subclass the parent - pImpl->_base_window_proc = (WNDPROC)GetWindowLongPtr(hParentWnd, GWL_WNDPROC); - SetWindowLongPtr(hParentWnd, GWL_WNDPROC, (LONG_PTR)file_dialog_subclass_proc); + pImpl->_base_window_proc = (WNDPROC)GetWindowLongPtr(hParentWnd, GWLP_WNDPROC); + SetWindowLongPtr(hParentWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(file_dialog_subclass_proc)); // Add a button to the toolbar pImpl->_toolbar_wnd = FindWindowEx(hParentWnd, NULL, "ToolbarWindow32", NULL); pImpl->_show_preview_button_bitmap = LoadBitmap( hInstance, MAKEINTRESOURCE(IDC_SHOW_PREVIEW)); - TBADDBITMAP tbAddBitmap = {NULL, (UINT)pImpl->_show_preview_button_bitmap}; + TBADDBITMAP tbAddBitmap = {NULL, reinterpret_cast<UINT_PTR>(pImpl->_show_preview_button_bitmap)}; const int iBitmapIndex = SendMessage(pImpl->_toolbar_wnd, TB_ADDBITMAP, 1, (LPARAM)&tbAddBitmap); @@ -1373,10 +1373,10 @@ void FileOpenDialogImplWin32::render_preview() if(_preview_bitmap_image) // Is the image a pixbuf? { // Set the transformation - const Matrix matrix = { + const Cairo::Matrix matrix( scaleFactor, 0, 0, scaleFactor, - svgX, svgY }; + svgX, svgY); context->set_matrix (matrix); // Render the image diff --git a/src/ui/dialog/filter-effects-dialog.cpp b/src/ui/dialog/filter-effects-dialog.cpp index 1345ffe55..132e5fd4e 100644 --- a/src/ui/dialog/filter-effects-dialog.cpp +++ b/src/ui/dialog/filter-effects-dialog.cpp @@ -1265,7 +1265,7 @@ void FilterEffectsDialog::FilterModifier::update_filters() SPFilter* f = (SPFilter*)l->data; row[_columns.filter] = f; const gchar* lbl = f->label(); - const gchar* id = SP_OBJECT_ID(f); + const gchar* id = f->getId(); row[_columns.label] = lbl ? lbl : (id ? id : "filter"); } @@ -1485,7 +1485,7 @@ void FilterEffectsDialog::PrimitiveList::update() row[_columns.primitive] = prim; row[_columns.type_id] = FPConverter.get_id_from_key(prim->repr->name()); row[_columns.type] = _(FPConverter.get_label(row[_columns.type_id]).c_str()); - row[_columns.id] = SP_OBJECT_ID(prim); + row[_columns.id] = prim->getId(); if(prim == active_prim) { get_selection()->select(row); diff --git a/src/ui/dialog/icon-preview.cpp b/src/ui/dialog/icon-preview.cpp index 336afc3c5..088f63031 100644 --- a/src/ui/dialog/icon-preview.cpp +++ b/src/ui/dialog/icon-preview.cpp @@ -90,7 +90,7 @@ IconPreviewPanel::IconPreviewPanel() : std::vector<Glib::ustring> pref_sizes = prefs->getAllDirs("/iconpreview/sizes/default"); std::vector<int> rawSizes; - + for (std::vector<Glib::ustring>::iterator i = pref_sizes.begin(); i != pref_sizes.end(); ++i) { if (prefs->getBool(*i + "/show", true)) { int sizeVal = prefs->getInt(*i + "/value", -1); @@ -215,7 +215,7 @@ void IconPreviewPanel::refreshPreview() while ( items && !target ) { SPItem* item = SP_ITEM( items->data ); SPObject * obj = SP_OBJECT(item); - gchar const *id = SP_OBJECT_ID( obj ); + gchar const *id = obj->getId(); if ( id ) { target = obj; } @@ -248,7 +248,7 @@ void IconPreviewPanel::modeToggled() void IconPreviewPanel::renderPreview( SPObject* obj ) { SPDocument * doc = SP_OBJECT_DOCUMENT(obj); - gchar * id = SP_OBJECT_ID(obj); + gchar const * id = obj->getId(); // g_message(" setting up to render '%s' as the icon", id ); diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 90516063c..40efc8282 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -433,15 +433,28 @@ void InkscapePreferences::initPageTools() this->AddPage(_page_node, _("Node"), iter_tools, PREFS_PAGE_TOOLS_NODE); AddSelcueCheckbox(_page_node, "/tools/nodes", true); AddGradientCheckbox(_page_node, "/tools/nodes", true); - _page_node.add_group_header( _("Path outline:")); + _page_node.add_group_header( _("Path outline")); _t_node_pathoutline_color.init(_("Path outline color"), "/tools/nodes/highlight_color", 0xff0000ff); - _page_node.add_line( false, _("Path outline color"), _t_node_pathoutline_color, "", _("Selects the color used for showing the path outline."), false); - _t_node_pathflash_enabled.init ( _("Path outline flash on mouse-over"), "/tools/nodes/pathflash_enabled", false); + _page_node.add_line( false, "", _t_node_pathoutline_color, "", _("Selects the color used for showing the path outline."), false); + _t_node_show_outline.init(_("Always show outline"), "/tools/nodes/show_outline", false); + _page_node.add_line( true, "", _t_node_show_outline, "", _("Show outlines for all paths, not only invisible paths")); + _t_node_live_outline.init(_("Update outline when dragging nodes"), "/tools/nodes/live_outline", false); + _page_node.add_line( true, "", _t_node_live_outline, "", _("Update the outline when dragging or transforming nodes. If this is off, the outline will only update when completing a drag.")); + _t_node_live_objects.init(_("Update paths when dragging nodes"), "/tools/nodes/live_objects", false); + _page_node.add_line( true, "", _t_node_live_objects, "", _("Update paths when dragging or transforming nodes. If this is off, paths will only be updated when completing a drag.")); + _t_node_show_path_direction.init(_("Show path direction on outlines"), "/tools/nodes/show_path_direction", false); + _page_node.add_line( true, "", _t_node_show_path_direction, "", _("Visualize the direction of selected paths by drawing small arrows in the middle of each outline segment")); + _t_node_pathflash_enabled.init ( _("Show temporary path outline"), "/tools/nodes/pathflash_enabled", false); _page_node.add_line( true, "", _t_node_pathflash_enabled, "", _("When hovering over a path, briefly flash its outline.")); - _t_node_pathflash_unselected.init ( _("Suppress path outline flash when one path selected"), "/tools/nodes/pathflash_unselected", false); - _page_node.add_line( true, "", _t_node_pathflash_unselected, "", _("If a path is selected, do not continue flashing path outlines.")); + _t_node_pathflash_selected.init ( _("Show temporary outline for selected paths"), "/tools/nodes/pathflash_selected", false); + _page_node.add_line( true, "", _t_node_pathflash_selected, "", _("Show temporary outline even when a path is selected for editing")); _t_node_pathflash_timeout.init("/tools/nodes/pathflash_timeout", 0, 10000.0, 100.0, 100.0, 1000.0, true, false); _page_node.add_line( false, _("Flash time"), _t_node_pathflash_timeout, "ms", _("Specifies how long the path outline will be visible after a mouse-over (in milliseconds). Specify 0 to have the outline shown until mouse leaves the path."), false); + _page_node.add_group_header(_("Editing preferences")); + _t_node_single_node_transform_handles.init(_("Show transform handles for single nodes"), "/tools/nodes/single_node_transform_handles", false); + _page_node.add_line( true, "", _t_node_single_node_transform_handles, "", _("Show transform handles even when only a single node is selected.")); + _t_node_delete_preserves_shape.init(_("Deleting nodes preserves shape"), "/tools/nodes/delete_preserves_shape", true); + _page_node.add_line( true, "", _t_node_delete_preserves_shape, "", _("Move handles next to deleted nodes to resemble original shape. Hold Ctrl to get the other behavior.")); //Tweak this->AddPage(_page_tweak, _("Tweak"), iter_tools, PREFS_PAGE_TOOLS_TWEAK); @@ -657,6 +670,28 @@ void InkscapePreferences::initPageMasks() _mask_mask_remove.init ( _("Remove clippath/mask object after applying"), "/options/maskobject/remove", true); _page_mask.add_line(true, "", _mask_mask_remove, "", _("After applying, remove the object used as the clipping path or mask from the drawing")); + + _page_mask.add_group_header( _("Before applying clippath/mask:")); + + _mask_grouping_none.init( _("Do not group clipped/masked objects"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_NONE, true, 0); + _mask_grouping_separate.init( _("Enclose every clipped/masked object in its own group"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_SEPARATE, false, &_mask_grouping_none); + _mask_grouping_all.init( _("Put all clipped/masked objects into one group"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_ALL, false, &_mask_grouping_none); + + _page_mask.add_line(true, "", _mask_grouping_none, "", + _("Apply clippath/mask to every object")); + + _page_mask.add_line(true, "", _mask_grouping_separate, "", + _("Apply clippath/mask to groups containing single object")); + + _page_mask.add_line(true, "", _mask_grouping_all, "", + _("Apply clippath/mask to group containing all objects")); + + _page_mask.add_group_header( _("After releasing clippath/mask:")); + + _mask_ungrouping.init ( _("Ungroup automatically created groups"), "/options/maskobject/ungrouping", true); + _page_mask.add_line(true, "", _mask_ungrouping, "", + _("Ungroup groups created when setting clip/mask")); + this->AddPage(_page_mask, _("Clippaths and masks"), PREFS_PAGE_MASKS); } @@ -987,7 +1022,7 @@ void InkscapePreferences::initPageGrids() _page_grids.add_line( false, "", _grids_notebook, "", "", false); _grids_notebook.append_page(_grids_xy, CanvasGrid::getName( GRID_RECTANGULAR )); _grids_notebook.append_page(_grids_axonom, CanvasGrid::getName( GRID_AXONOMETRIC )); - _grids_xy_units.init("/options/grids/units"); + _grids_xy_units.init("/options/grids/xy/units"); _grids_xy.add_line( false, _("Grid units:"), _grids_xy_units, "", "", false); _grids_xy_origin_x.init("/options/grids/xy/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); _grids_xy_origin_y.init("/options/grids/xy/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); @@ -1069,7 +1104,7 @@ void InkscapePreferences::initPageUI() _("Chinese/Taiwan (zh_TW)"), _("Croatian (hr)"), _("Czech (cs)"), _("Danish (da)"), _("Dutch (nl)"), _("Dzongkha (dz)"), _("German (de)"), _("Greek (el)"), _("English (en)"), _("English/Australia (en_AU)"), _("English/Canada (en_CA)"), _("English/Great Britain (en_GB)"), _("Pig Latin (en_US@piglatin)"), - _("Esperanto (eo)"), _("Estonian (et)"), _("Finnish (fi)"), + _("Esperanto (eo)"), _("Estonian (et)"), _("Farsi (fa)"), _("Finnish (fi)"), _("French (fr)"), _("Irish (ga)"), _("Galician (gl)"), _("Hebrew (he)"), _("Hungarian (hu)"), _("Indonesian (id)"), _("Italian (it)"), _("Japanese (ja)"), _("Khmer (km)"), _("Kinyarwanda (rw)"), _("Korean (ko)"), _("Lithuanian (lt)"), _("Macedonian (mk)"), _("Mongolian (mn)"), _("Nepali (ne)"), _("Norwegian Bokmål (nb)"), _("Norwegian Nynorsk (nn)"), _("Panjabi (pa)"), @@ -1077,7 +1112,7 @@ void InkscapePreferences::initPageUI() _("Serbian (sr)"), _("Serbian in Latin script (sr@latin)"), _("Slovak (sk)"), _("Slovenian (sl)"), _("Spanish (es)"), _("Spanish/Mexico (es_MX)"), _("Swedish (sv)"), _("Thai (th)"), _("Turkish (tr)"), _("Ukrainian (uk)"), _("Vietnamese (vi)")}; Glib::ustring langValues[] = {"", "sq", "am", "ar", "hy", "az", "eu", "be", "bg", "bn", "br", "ca", "ca@valencia", "zh_CN", "zh_TW", "hr", "cs", "da", "nl", - "dz", "de", "el", "en", "en_AU", "en_CA", "en_GB", "en_US@piglatin", "eo", "et", "fi", "fr", "ga", + "dz", "de", "el", "en", "en_AU", "en_CA", "en_GB", "en_US@piglatin", "eo", "et", "fa", "fi", "fr", "ga", "gl", "he", "hu", "id", "it", "ja", "km", "rw", "ko", "lt", "mk", "mn", "ne", "nb", "nn", "pa", "pl", "pt", "pt_BR", "ro", "ru", "sr", "sr@latin", "sk", "sl", "es", "es_MX", "sv", "th", "tr", "uk", "vi" }; @@ -1121,6 +1156,12 @@ void InkscapePreferences::initPageUI() _page_ui.add_line( false, _("Zoom correction factor (in %):"), _ui_zoom_correction, "", _("Adjust the slider until the length of the ruler on your screen matches its real length. This information is used when zooming to 1:1, 1:2, etc., to display objects in their true sizes"), true); + + _ui_partialdynamic.init( _("Enable dynamic relayout for incomplete sections."), "/options/workarounds/dynamicnotdone", false); + _page_ui.add_line( false, "", _ui_partialdynamic, "", + _("When on, will allow dynamic layout of components that are not completely finished being refactored."), true); + + this->AddPage(_page_ui, _("Interface"), PREFS_PAGE_UI); } diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index 16e62df59..0ba8c965d 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -141,9 +141,15 @@ protected: PrefRadioButton _t_sel_trans_obj, _t_sel_trans_outl, _t_sel_cue_none, _t_sel_cue_mark, _t_sel_cue_box, _t_bbox_visual, _t_bbox_geometric; PrefCheckButton _t_cvg_keep_objects, _t_cvg_convert_whole_groups; + PrefCheckButton _t_node_show_outline; + PrefCheckButton _t_node_live_outline; + PrefCheckButton _t_node_live_objects; PrefCheckButton _t_node_pathflash_enabled; - PrefCheckButton _t_node_pathflash_unselected; + PrefCheckButton _t_node_pathflash_selected; PrefSpinButton _t_node_pathflash_timeout; + PrefCheckButton _t_node_show_path_direction; + PrefCheckButton _t_node_single_node_transform_handles; + PrefCheckButton _t_node_delete_preserves_shape; PrefColorPicker _t_node_pathoutline_color; PrefRadioButton _win_dockable, _win_floating; @@ -167,6 +173,8 @@ protected: PrefCheckButton _mask_mask_on_top; PrefCheckButton _mask_mask_remove; + PrefRadioButton _mask_grouping_none, _mask_grouping_separate, _mask_grouping_all; + PrefCheckButton _mask_ungrouping; PrefRadioButton _blur_quality_best, _blur_quality_better, _blur_quality_normal, _blur_quality_worse, _blur_quality_worst; PrefRadioButton _filter_quality_best, _filter_quality_better, _filter_quality_normal, _filter_quality_worse, _filter_quality_worst; @@ -197,6 +205,7 @@ protected: PrefCombo _misc_small_tools; PrefCheckButton _ui_colorsliders_top; PrefSpinButton _misc_recent; + PrefCheckButton _ui_partialdynamic; ZoomCorrRulerSlider _ui_zoom_correction; //Spellcheck diff --git a/src/ui/dialog/layers.cpp b/src/ui/dialog/layers.cpp index a06b6c9b6..98bf236fc 100644 --- a/src/ui/dialog/layers.cpp +++ b/src/ui/dialog/layers.cpp @@ -304,7 +304,7 @@ bool LayersPanel::_checkForUpdated(const Gtk::TreePath &/*path*/, const Gtk::Tre Glib::ustring tmp = row[_model->_colLabel]; if ( layer == row[_model->_colObject] ) { - row[_model->_colLabel] = layer->label() ? layer->label() : SP_OBJECT_ID(layer); + row[_model->_colLabel] = layer->label() ? layer->label() : layer->getId(); row[_model->_colVisible] = SP_IS_ITEM(layer) ? !SP_ITEM(layer)->isHidden() : false; row[_model->_colLocked] = SP_IS_ITEM(layer) ? SP_ITEM(layer)->isLocked() : false; @@ -381,7 +381,7 @@ void LayersPanel::_addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::R Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); Gtk::TreeModel::Row row = *iter; row[_model->_colObject] = child; - row[_model->_colLabel] = child->label() ? child->label() : SP_OBJECT_ID(child); + row[_model->_colLabel] = child->label() ? child->label() : child->getId(); row[_model->_colVisible] = SP_IS_ITEM(child) ? !SP_ITEM(child)->isHidden() : false; row[_model->_colLocked] = SP_IS_ITEM(child) ? SP_ITEM(child)->isLocked() : false; diff --git a/src/ui/dialog/spray-option.cpp b/src/ui/dialog/spray-option.cpp deleted file mode 100644 index a9e037381..000000000 --- a/src/ui/dialog/spray-option.cpp +++ /dev/null @@ -1,397 +0,0 @@ -/*Julien LERAY (julien.leray@ecl2010.ec-lyon.fr), interface for the spray tool*/ - - -#ifdef HAVE_CONFIG_H -# include <config.h> -#endif - -#include <gtkmm/spinbutton.h> - -#include "desktop-handles.h" -#include "unclump.h" -#include "document.h" -#include "enums.h" -#include "graphlayout/graphlayout.h" -#include "inkscape.h" -#include "macros.h" -#include "node-context.h" -#include "preferences.h" -#include "removeoverlap/removeoverlap.h" -#include "selection.h" -#include "shape-editor.h" -#include "sp-flowtext.h" -#include "sp-item-transform.h" -#include "sp-text.h" -#include "text-editing.h" -#include "tools-switch.h" -#include "ui/icon-names.h" -#include "util/glib-list-iterators.h" -#include "verbs.h" -#include "widgets/icon.h" - -#include "spray-option.h" - -namespace Inkscape { -namespace UI { -namespace Dialog { - - -//Classes qui permettent de créer les environnements Gaussienne, Witdh... - - - -class Action { -public: - Action(const Glib::ustring &id, - const Glib::ustring &/*tiptext*/, - guint /*row*/, - guint /*column*/, - Gtk::Table &parent, - Gtk::Tooltips &/*tooltips*/, - SprayOptionClass &dialog): - _dialog(dialog), - _id(id), - _parent(parent) {} - - virtual ~Action(){} - virtual void on_button_click(){} - SprayOptionClass &_dialog; - -private : - - Glib::ustring _id; - Gtk::Table &_parent; -}; - -class ActionE : public Action { -private: - Gtk::Label _Label; - Gtk::SpinButton _Gap; - guint _min, _max; - Glib::ustring _pref_path; - -public: - ActionE(const Glib::ustring &id, - const Glib::ustring &tiptext, - guint row, guint column, - SprayOptionClass &dialog, - guint min, guint max, - Glib::ustring const &pref_path ): - Action(id, tiptext, row, column, - dialog._Table(), dialog.tooltips(), dialog), - _min(min), - _max(max), - _pref_path(pref_path) - { - dialog._Table().set_col_spacings(3); - - double increm = ((double)_max - (double)_min)/10; - double val_ini = ((double)_max + (double)_min)/2; - _Gap.set_digits(1); - _Gap.set_size_request(60, -1); - _Gap.set_increments(increm , 0); - _Gap.set_range(_min, _max); - _Gap.set_value(val_ini); - dialog.tooltips().set_tip(_Gap, - tiptext); - _Gap.signal_changed().connect(sigc::mem_fun(*this, &ActionE::on_button_click)); //rajout douteux - _Label.set_label(id); - - dialog._Table().attach(_Label, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL); - dialog._Table().attach(_Gap, column+1, column+2, row, row+1, Gtk::EXPAND, Gtk::EXPAND); - } - - virtual void on_button_click(){ - if (!_dialog.getDesktop()) return; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - - prefs->setDouble(_pref_path, SP_VERB_CONTEXT_SPRAY); - - double const Gap = _Gap.get_value(); - - - prefs->setDouble(_pref_path, Gap); - - sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_CONTEXT_SPRAY, - _("Remove overlaps")); - } - - -}; - -class ActionF : public Action { -private: - Gtk::Label _Label; - Gtk::Label _Label1; - Gtk::Label _Label2; - Gtk::SpinButton _Gap1; - Gtk::SpinButton _Gap2; - Glib::ustring _pref1_path; - Glib::ustring _pref2_path; - -public: - ActionF(const Glib::ustring &id, - const Glib::ustring &tiptext, - guint row, guint column, - SprayOptionClass &dialog, - Glib::ustring const &pref1_path, - Glib::ustring const &pref2_path ): - Action(id, tiptext, row, column, - dialog._Table(), dialog.tooltips(), dialog), - _pref1_path(pref1_path), - _pref2_path(pref2_path) - { - dialog.F_Table().set_col_spacings(3); - - _Label.set_label(id); - - _Gap1.set_digits(1); - _Gap1.set_size_request(60, -1); - _Gap1.set_increments(0.1, 0); - _Gap1.set_range(0, 10); - _Gap1.set_value(1); - dialog.tooltips().set_tip(_Gap1, - _("Minimum")); - - _Label1.set_label(Q_("Min")); - - _Gap2.set_digits(1); - _Gap2.set_size_request(60, -1); - _Gap2.set_increments(0.1, 0); - _Gap2.set_range(0, 10); - _Gap2.set_value(1); - dialog.tooltips().set_tip(_Gap2, - _("Maximum")); - - _Label2.set_label(_("Max:")); - - _Gap1.signal_changed().connect(sigc::mem_fun(*this, &ActionF::on_button_click)); - _Gap2.signal_changed().connect(sigc::mem_fun(*this, &ActionF::on_button_click)); - - dialog.F_Table().attach(_Label, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL); - dialog.F_Table().attach(_Label1, column+1, column+2, row, row+1, Gtk::FILL, Gtk::FILL); - dialog.F_Table().attach(_Gap1, column+2, column+3, row, row+1, Gtk::EXPAND, Gtk::EXPAND); - dialog.F_Table().attach(_Label2, column+3, column+4, row, row+1, Gtk::FILL, Gtk::FILL); - dialog.F_Table().attach(_Gap2, column+4, column+5, row, row+1, Gtk::EXPAND, Gtk::EXPAND); - - } - - virtual void on_button_click(){ - if (!_dialog.getDesktop()) return; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - - prefs->setDouble(_pref1_path, SP_VERB_CONTEXT_SPRAY); - prefs->setDouble(_pref2_path, SP_VERB_CONTEXT_SPRAY); - - double const Gap1 = _Gap1.get_value(); - double const Gap2 = _Gap2.get_value(); - - prefs->setDouble(_pref1_path, Gap1); - prefs->setDouble(_pref2_path, Gap2); - - sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_CONTEXT_SPRAY, - _("Remove overlaps")); - } - - -}; - - - -void SprayOptionClass::combo_action() { - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - cout<<"combo.get_active_row_number = "<<_combo.get_active_row_number()<<endl; - - int const distrib = _combo.get_active_row_number(); - - prefs->setInt("/tools/spray/distribution", distrib); - - - sp_document_done(sp_desktop_document(this->getDesktop()), SP_VERB_CONTEXT_SPRAY, - _("Remove overlaps")); - -} - - - - -void SprayOptionClass::action() { - for (list<Action *>::iterator it = _actionList.begin(); it != _actionList.end(); ++it) { - (*it)->on_button_click(); - } - combo_action(); -} - - - - - - -void on_selection_changed(Inkscape::Application */*inkscape*/, Inkscape::Selection */*selection*/, SprayOptionClass *daad) -{ - daad->randomize_bbox = Geom::OptRect(); -} - -///////////////////////////////////////////////////////// -//Construction de l'interface -///////////////////////////////////////////////////////// - - -SprayOptionClass::SprayOptionClass() - : UI::Widget::Panel ("", "/dialogs/spray", SP_VERB_DIALOG_SPRAY_OPTION), - _actionList(), - _distributionFrame(Q_("sprayOptions|Distribution")), - _Frame(Q_("sprayOptions|Cursor Options")), - _FFrame(Q_("sprayOptions|Random Options")), - _distributionTable(), - _gaussianTable(1, 5, false), - _ETable(3,2,false), - _FTable(2,5,false), - _anchorBox(), - _unifBox(), - _gaussianBox(), - _HBox(), - _FHBox(), - _BoutonBox(), - _distributionBox(), - _VBox(), - _FVBox(), - _ActionBox(), - _anchorLabel(Q_("sprayOptions|Distribution:")), - _unifLabel(Q_("sprayOptions|Uniform")), - _gaussLabel(Q_("sprayOptions|Gaussian")), - _Label(), - _FLabel(), - _unif(), - _gauss(), - _combo(), - _tooltips() -{ - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - - //ComboBoxText - - _combo.append_text(Q_("sprayOptions|Uniform")); - _combo.append_text(Q_("sprayOptions|Gaussian")); - - _combo.set_active(prefs->getInt("/tools/spray/distribution", 1)); - _combo.signal_changed().connect(sigc::mem_fun(*this, &SprayOptionClass::combo_action)); - - _anchorBox.pack_start(_anchorLabel); - _anchorBox.pack_start(_combo); - - _gaussianBox.pack_start(_anchorBox); - - - _distributionBox.pack_start(_gaussianBox); - _distributionFrame.add(_distributionBox); - - - //Hbox Random - addFButton(Q_("sprayOptions|Scale:") ,_("Apply a scale factor"), 0, 0, "/tools/spray/scale_min","/tools/spray/scale_max"); - addFButton(Q_("sprayOptions|Rotation:") ,_("Apply rotation"), 1, 0, "/tools/spray/rot_min","/tools/spray/rot_max"); - _FHBox.pack_start(_FLabel); - _FHBox.pack_start(_FTable); - - //Implementation dans la Vbox Cursor - _FVBox.pack_start(_FHBox); - _FFrame.add(_FVBox); - - //Hbox Cursor - addEButton(Q_("sprayOptions|Ratio:") ,_("Eccentricity of the ellipse"), 0, 0, 0, 1,"/tools/spray/ratio"); - addEButton(Q_("sprayOptions|Angle:") ,_("Angle of the ellipse"), 1, 0, 0, 5,"/tools/spray/tilt"); - addEButton(Q_("sprayOptions|Width:") ,_("Size of the ellipse"), 2, 0, 0, 1,"/tools/spray/width"); - _HBox.pack_start(_Label); - _HBox.pack_start(_ETable); - - //Implementation dans la Vbox Cursor - _VBox.pack_start(_HBox); - _Frame.add(_VBox); - - Gtk::Box *contents = _getContents(); - contents->set_spacing(4); - - - - - - - // Crée dans l'ordre suivant les différentes Frames (cadres de réglages) - - contents->pack_start(_distributionFrame, true, true); - contents->pack_start(_FFrame, true, true); - contents->pack_start(_Frame, true, true); - - - - // Connect to the global selection change, to invalidate cached randomize_bbox - g_signal_connect (G_OBJECT (INKSCAPE), "change_selection", G_CALLBACK (on_selection_changed), this); - randomize_bbox = Geom::OptRect(); - - show_all_children(); - - - -} - -SprayOptionClass::~SprayOptionClass() -{ - sp_signal_disconnect_by_data (G_OBJECT (INKSCAPE), this); - - for (std::list<Action *>::iterator it = _actionList.begin(); - it != _actionList.end(); - it ++) - delete *it; -} - - - - - - - -//Fonctions qui lient la demande d'ajout d'une interface graphique à l'action correspondante - -void SprayOptionClass::addEButton(const Glib::ustring &id, - const Glib::ustring &tiptext, - guint row, guint column, - guint min, guint max, - Glib::ustring const &pref_path) -{ - _actionList.push_back( new ActionE(id, tiptext,row, column,*this,min ,max, pref_path )); -} - -void SprayOptionClass::addFButton(const Glib::ustring &id, - const Glib::ustring &tiptext, - guint row, guint column, - Glib::ustring const &pref1_path, - Glib::ustring const &pref2_path) -{ - _actionList.push_back( new ActionF(id, tiptext,row, column,*this,pref1_path, pref2_path )); -} - - - - - -SprayOptionClass &SprayOptionClass::get_SprayOptionClass() -{ - return *this; -} - -} // namespace Dialog -} // namespace UI -} // namespace Inkscape - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/spray-option.h b/src/ui/dialog/spray-option.h deleted file mode 100644 index 42090a120..000000000 --- a/src/ui/dialog/spray-option.h +++ /dev/null @@ -1,145 +0,0 @@ - -/*Julien LERAY (julien.leray@ecl2010.ec-lyon.fr), interface for the spray tool*/ - -#ifndef INKSCAPE_UI_DIALOG_SPRAY_OPTION_H -#define INKSCAPE_UI_DIALOG_SPRAY_OPTION_H - -#include <gtkmm/notebook.h> -#include <glibmm/i18n.h> - -#include <list> -#include <gtkmm/frame.h> -#include <gtkmm/tooltips.h> -#include <gtkmm/comboboxtext.h> -#include <gtkmm/table.h> -#include <gtkmm/buttonbox.h> -#include <gtkmm/label.h> -#include "libnr/nr-dim2.h" -#include "libnr/nr-rect.h" - - -#include "ui/widget/panel.h" -#include "ui/widget/notebook-page.h" - -#ifdef HAVE_CONFIG_H -# include <config.h> -#endif - -#include <gtkmm/spinbutton.h> -#include "desktop-handles.h" -#include "unclump.h" -#include "document.h" -#include "enums.h" -#include "graphlayout/graphlayout.h" -#include "inkscape.h" -#include "macros.h" -#include "node-context.h" -#include "preferences.h" -#include "removeoverlap/removeoverlap.h" -#include "selection.h" -#include "shape-editor.h" -#include "sp-flowtext.h" -#include "sp-item-transform.h" -#include "sp-text.h" -#include "text-editing.h" -#include "tools-switch.h" -#include "ui/icon-names.h" -#include "util/glib-list-iterators.h" -#include "verbs.h" -#include "widgets/icon.h" - -#include "spray-context.h" -#include "verbs.h" - -#include <iostream> -using namespace std; - -using namespace Inkscape::UI::Widget; - -class SPItem; - - -namespace Inkscape { -namespace UI { -namespace Dialog { - -class Action; - -class SprayOptionClass : public Widget::Panel { - -private: - - SprayOptionClass(SprayOptionClass const &d); - SprayOptionClass& operator=(SprayOptionClass const &d); - -public: - SprayOptionClass(); - virtual ~SprayOptionClass(); - void test() { cout<<"appel de test !!"<<endl; } - static SprayOptionClass &getInstance() { return *new SprayOptionClass(); } - - - Gtk::Table &_Table(){return _ETable;} - Gtk::Table &F_Table(){return _FTable;} - Gtk::Tooltips &tooltips(){return _tooltips;} - void action(); - void combo_action(); - Geom::OptRect randomize_bbox; - - SprayOptionClass &get_SprayOptionClass(); - -protected: - - void addGaussianButton(guint row, guint col); - void addEButton(const Glib::ustring &id, const Glib::ustring &tiptext, guint row, guint column, - guint min, guint max, const Glib::ustring &pref_path); - void addFButton(const Glib::ustring &id, const Glib::ustring &tiptext, guint row, guint column, - const Glib::ustring &pref1_path, const Glib::ustring &pref2_path); - - std::list<Action *> _actionList; - Gtk::Frame _distributionFrame; - Gtk::Frame _Frame; - Gtk::Frame _FFrame; - Gtk::Table _distributionTable; - Gtk::Table _gaussianTable; - Gtk::Table _ETable; - Gtk::Table _FTable; - Gtk::HBox _anchorBox; - Gtk::HBox _unifBox; - Gtk::HBox _gaussianBox; - Gtk::HBox _HBox; - Gtk::HBox _FHBox; - Gtk::HBox _BoutonBox; - Gtk::VBox _distributionBox; - Gtk::VBox _VBox; - Gtk::VBox _FVBox; - Gtk::VBox _ActionBox; - Gtk::Label _anchorLabel; - Gtk::Label _unifLabel; - Gtk::Label _gaussLabel; - Gtk::Label _Label; - Gtk::Label _FLabel; - Gtk::CheckButton _unif; - Gtk::CheckButton _gauss; - Gtk::ComboBoxText _combo; - Gtk::Tooltips _tooltips; -}; - - -} // namespace Dialog -} // namespace UI -} // namespace Inkscape - -#endif // INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_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:encoding=utf-8:textwidth=99 : - diff --git a/src/ui/dialog/svg-fonts-dialog.cpp b/src/ui/dialog/svg-fonts-dialog.cpp index cb22e029b..998f4e1e1 100644 --- a/src/ui/dialog/svg-fonts-dialog.cpp +++ b/src/ui/dialog/svg-fonts-dialog.cpp @@ -252,7 +252,7 @@ void SvgFontsDialog::update_fonts() row[_columns.spfont] = f; row[_columns.svgfont] = new SvgFont(f); const gchar* lbl = f->label(); - const gchar* id = SP_OBJECT_ID(f); + const gchar* id = f->getId(); row[_columns.label] = lbl ? lbl : (id ? id : "font"); } @@ -653,7 +653,7 @@ Gtk::VBox* SvgFontsDialog::glyphs_tab(){ missing_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::missing_glyph_description_from_selected_path)); missing_glyph_reset_button.set_label(_("Reset")); missing_glyph_reset_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::reset_missing_glyph_description)); - + glyphs_vbox.pack_start(*missing_glyph_hbox, false,false); glyphs_vbox.add(_GlyphsListScroller); diff --git a/src/ui/dialog/swatches.cpp b/src/ui/dialog/swatches.cpp index 450d4202d..6f013f4f3 100644 --- a/src/ui/dialog/swatches.cpp +++ b/src/ui/dialog/swatches.cpp @@ -1,3 +1,4 @@ + /** @file * @brief Color swatches dialog */ @@ -22,6 +23,7 @@ #include <glibmm/i18n.h> #include <gdkmm/pixbuf.h> +#include "color-item.h" #include "desktop.h" #include "desktop-handles.h" #include "desktop-style.h" @@ -36,278 +38,36 @@ #include "path-prefix.h" #include "preferences.h" #include "sp-item.h" -#include "svg/svg-color.h" #include "sp-gradient-fns.h" #include "sp-gradient.h" #include "sp-gradient-vector.h" #include "swatches.h" #include "style.h" +#include "ui/previewholder.h" #include "widgets/gradient-vector.h" #include "widgets/eek-preview.h" #include "display/nr-plain-stuff.h" #include "sp-gradient-reference.h" -#define USE_DOCUMENT_PALETTE 1 namespace Inkscape { namespace UI { namespace Dialogs { #define VBLOCK 16 +#define PREVIEW_PIXBUF_WIDTH 128 void _loadPaletteFile( gchar const *filename ); -/** - * The color swatch you see on screen as a clickable box. - */ -class ColorItem : public Inkscape::UI::Previewable -{ - friend void _loadPaletteFile( gchar const *filename ); -public: - ColorItem( ege::PaintDef::ColorType type ); - ColorItem( unsigned int r, unsigned int g, unsigned int b, - Glib::ustring& name ); - virtual ~ColorItem(); - ColorItem(ColorItem const &other); - virtual ColorItem &operator=(ColorItem const &other); - virtual Gtk::Widget* getPreview(PreviewStyle style, - ViewType view, - ::PreviewSize size, - guint ratio); - void buttonClicked(bool secondary = false); - - void setState( bool fill, bool stroke ); - bool isFill() { return _isFill; } - bool isStroke() { return _isStroke; } - - ege::PaintDef def; - void* ptr; - -private: - static void _dropDataIn( GtkWidget *widget, - GdkDragContext *drag_context, - gint x, gint y, - GtkSelectionData *data, - guint info, - guint event_time, - gpointer user_data); - - static void _dragGetColorData( GtkWidget *widget, - GdkDragContext *drag_context, - GtkSelectionData *data, - guint info, - guint time, - gpointer user_data); - - static void _wireMagicColors( void* p ); - static void _colorDefChanged(void* data); - - void _linkTint( ColorItem& other, int percent ); - void _linkTone( ColorItem& other, int percent, int grayLevel ); - - Gtk::Tooltips tips; - std::vector<Gtk::Widget*> _previews; - - bool _isFill; - bool _isStroke; - bool _isLive; - bool _linkIsTone; - int _linkPercent; - int _linkGray; - ColorItem* _linkSrc; - std::vector<ColorItem*> _listeners; -}; - - - -ColorItem::ColorItem(ege::PaintDef::ColorType type) : - def(type), - ptr(0), - _isFill(false), - _isStroke(false), - _isLive(false), - _linkIsTone(false), - _linkPercent(0), - _linkGray(0), - _linkSrc(0) -{ -} - -ColorItem::ColorItem( unsigned int r, unsigned int g, unsigned int b, Glib::ustring& name ) : - def( r, g, b, name ), - ptr(0), - _isFill(false), - _isStroke(false), - _isLive(false), - _linkIsTone(false), - _linkPercent(0), - _linkGray(0), - _linkSrc(0) -{ -} - -ColorItem::~ColorItem() -{ -} - -ColorItem::ColorItem(ColorItem const &other) : - Inkscape::UI::Previewable() -{ - if ( this != &other ) { - *this = other; - } -} - -ColorItem &ColorItem::operator=(ColorItem const &other) -{ - if ( this != &other ) { - def = other.def; - - // TODO - correct linkage - _linkSrc = other._linkSrc; - g_message("Erk!"); - } - return *this; -} - -void ColorItem::setState( bool fill, bool stroke ) -{ - if ( (_isFill != fill) || (_isStroke != stroke) ) { - _isFill = fill; - _isStroke = stroke; - - for ( std::vector<Gtk::Widget*>::iterator it = _previews.begin(); it != _previews.end(); ++it ) { - Gtk::Widget* widget = *it; - if ( IS_EEK_PREVIEW(widget->gobj()) ) { - EekPreview * preview = EEK_PREVIEW(widget->gobj()); - - int val = eek_preview_get_linked( preview ); - val &= ~(PREVIEW_FILL | PREVIEW_STROKE); - if ( _isFill ) { - val |= PREVIEW_FILL; - } - if ( _isStroke ) { - val |= PREVIEW_STROKE; - } - eek_preview_set_linked( preview, static_cast<LinkType>(val) ); - } - } - } -} - - -class JustForNow -{ -public: - JustForNow() : _prefWidth(0) {} - - Glib::ustring _name; - int _prefWidth; - std::vector<ColorItem*> _colors; -}; - -static std::vector<JustForNow*> possible; +class DocTrack; -static std::vector<std::string> mimeStrings; -static std::map<std::string, guint> mimeToInt; +std::vector<SwatchPage*> possible; +static std::map<SPDocument*, SwatchPage*> docPalettes; +static std::vector<DocTrack*> docTrackings; +static std::map<SwatchesPanel*, SPDocument*> docPerPanel; -static std::map<ColorItem*, guchar*> previewMap; -static std::map<ColorItem*, SPGradient*> gradMap; // very temporary workaround. -void ColorItem::_dragGetColorData( GtkWidget */*widget*/, - GdkDragContext */*drag_context*/, - GtkSelectionData *data, - guint info, - guint /*time*/, - gpointer user_data) -{ - ColorItem* item = reinterpret_cast<ColorItem*>(user_data); - std::string key; - if ( info < mimeStrings.size() ) { - key = mimeStrings[info]; - } else { - g_warning("ERROR: unknown value (%d)", info); - } - - if ( !key.empty() ) { - char* tmp = 0; - int len = 0; - int format = 0; - item->def.getMIMEData(key, tmp, len, format); - if ( tmp ) { - GdkAtom dataAtom = gdk_atom_intern( key.c_str(), FALSE ); - gtk_selection_data_set( data, dataAtom, format, (guchar*)tmp, len ); - delete[] tmp; - } - } -} - -static void dragBegin( GtkWidget */*widget*/, GdkDragContext* dc, gpointer data ) -{ - ColorItem* item = reinterpret_cast<ColorItem*>(data); - if ( item ) - { - using Inkscape::IO::Resource::get_path; - using Inkscape::IO::Resource::ICONS; - using Inkscape::IO::Resource::SYSTEM; - int width = 32; - int height = 24; - - if (item->def.getType() != ege::PaintDef::RGB){ - GError *error = NULL; - gsize bytesRead = 0; - gsize bytesWritten = 0; - gchar *localFilename = g_filename_from_utf8( get_path(SYSTEM, ICONS, "remove-color.png"), - -1, - &bytesRead, - &bytesWritten, - &error); - GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file_at_scale(localFilename, width, height, FALSE, &error); - g_free(localFilename); - gtk_drag_set_icon_pixbuf( dc, pixbuf, 0, 0 ); - } else { - GdkPixbuf* pixbuf = 0; - if ( gradMap.find(item) == gradMap.end() ){ - Glib::RefPtr<Gdk::Pixbuf> thumb = Gdk::Pixbuf::create( Gdk::COLORSPACE_RGB, false, 8, width, height ); - guint32 fillWith = (0xff000000 & (item->def.getR() << 24)) - | (0x00ff0000 & (item->def.getG() << 16)) - | (0x0000ff00 & (item->def.getB() << 8)); - thumb->fill( fillWith ); - pixbuf = thumb->gobj(); - } else { - SPGradient* grad = gradMap[item]; - - guchar* px = g_new( guchar, 3 * height * width ); - nr_render_checkerboard_rgb( px, width, height, 3 * width, 0, 0 ); - - sp_gradient_render_vector_block_rgb( grad, - px, width, height, 3 * width, - 0, width, TRUE ); - - pixbuf = gdk_pixbuf_new_from_data( px, GDK_COLORSPACE_RGB, FALSE, 8, - width, height, width * 3, - 0, // add delete function - 0 ); - } - gtk_drag_set_icon_pixbuf( dc, pixbuf, 0, 0 ); - } - } - -} - -//"drag-drop" -// gboolean dragDropColorData( GtkWidget *widget, -// GdkDragContext *drag_context, -// gint x, -// gint y, -// guint time, -// gpointer user_data) -// { -// // TODO finish - -// return TRUE; -// } static void handleClick( GtkWidget* /*widget*/, gpointer callback_data ) { ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); @@ -323,34 +83,10 @@ static void handleSecondaryClick( GtkWidget* /*widget*/, gint /*arg1*/, gpointer } } -static gboolean handleEnterNotify( GtkWidget* /*widget*/, GdkEventCrossing* /*event*/, gpointer callback_data ) { - ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); - if ( item ) { - SPDesktop *desktop = SP_ACTIVE_DESKTOP; - if ( desktop ) { - gchar* msg = g_strdup_printf(_("Color: <b>%s</b>; <b>Click</b> to set fill, <b>Shift+click</b> to set stroke"), - item->def.descr.c_str()); - desktop->tipsMessageContext()->set(Inkscape::INFORMATION_MESSAGE, msg); - g_free(msg); - } - } - return FALSE; -} - -static gboolean handleLeaveNotify( GtkWidget* /*widget*/, GdkEventCrossing* /*event*/, gpointer callback_data ) { - ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); - if ( item ) { - SPDesktop *desktop = SP_ACTIVE_DESKTOP; - if ( desktop ) { - desktop->tipsMessageContext()->clear(); - } - } - return FALSE; -} - static GtkWidget* popupMenu = 0; static std::vector<GtkWidget*> popupExtras; static ColorItem* bounceTarget = 0; +static SwatchesPanel* bouncePanel = 0; static void redirClick( GtkMenuItem *menuitem, gpointer /*user_data*/ ) { @@ -366,7 +102,6 @@ static void redirSecondaryClick( GtkMenuItem *menuitem, gpointer /*user_data*/ ) } } -#if USE_DOCUMENT_PALETTE static void editGradientImpl( SPGradient* gr ) { if ( gr ) { @@ -378,7 +113,7 @@ static void editGradientImpl( SPGradient* gr ) static void editGradient( GtkMenuItem */*menuitem*/, gpointer /*user_data*/ ) { if ( bounceTarget ) { - SwatchesPanel* swp = bounceTarget->ptr ? reinterpret_cast<SwatchesPanel*>(bounceTarget->ptr) : 0; + SwatchesPanel* swp = bouncePanel; SPDesktop* desktop = swp ? swp->getDesktop() : 0; SPDocument *doc = desktop ? desktop->doc() : 0; if (doc) { @@ -386,7 +121,7 @@ static void editGradient( GtkMenuItem */*menuitem*/, gpointer /*user_data*/ ) const GSList *gradients = sp_document_get_resource_list(doc, "gradient"); for (const GSList *item = gradients; item; item = item->next) { SPGradient* grad = SP_GRADIENT(item->data); - if ( targetName == grad->id ) { + if ( targetName == grad->getId() ) { editGradientImpl( grad ); break; } @@ -398,7 +133,7 @@ static void editGradient( GtkMenuItem */*menuitem*/, gpointer /*user_data*/ ) static void addNewGradient( GtkMenuItem */*menuitem*/, gpointer /*user_data*/ ) { if ( bounceTarget ) { - SwatchesPanel* swp = bounceTarget->ptr ? reinterpret_cast<SwatchesPanel*>(bounceTarget->ptr) : 0; + SwatchesPanel* swp = bouncePanel; SPDesktop* desktop = swp ? swp->getDesktop() : 0; SPDocument *doc = desktop ? desktop->doc() : 0; if (doc) { @@ -420,20 +155,35 @@ static void addNewGradient( GtkMenuItem */*menuitem*/, gpointer /*user_data*/ ) editGradientImpl( gr ); - // Work-around for timing of gradient addition change. Must follow edit. - if ( swp ) { - swp->handleGradientsChange(); - } } } } -#endif // USE_DOCUMENT_PALETTE -static gboolean handleButtonPress( GtkWidget* /*widget*/, GdkEventButton* event, gpointer user_data) +static SwatchesPanel* findContainingPanel( GtkWidget *widget ) +{ + SwatchesPanel *swp = 0; + + std::map<GtkWidget*, SwatchesPanel*> rawObjects; + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); it != docPerPanel.end(); ++it) { + rawObjects[GTK_WIDGET(it->first->gobj())] = it->first; + } + + for (GtkWidget* curr = widget; curr && !swp; curr = gtk_widget_get_parent(curr)) { + if (rawObjects.find(curr) != rawObjects.end()) { + swp = rawObjects[curr]; + } + } + + return swp; +} + +gboolean colorItemHandleButtonPress( GtkWidget* widget, GdkEventButton* event, gpointer user_data) { gboolean handled = FALSE; if ( (event->button == 3) && (event->type == GDK_BUTTON_PRESS) ) { + SwatchesPanel* swp = findContainingPanel( widget ); + if ( !popupMenu ) { popupMenu = gtk_menu_new(); GtkWidget* child = 0; @@ -455,7 +205,6 @@ static gboolean handleButtonPress( GtkWidget* /*widget*/, GdkEventButton* event, user_data); gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); -#if USE_DOCUMENT_PALETTE child = gtk_separator_menu_item_new(); gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); popupExtras.push_back(child); @@ -489,20 +238,19 @@ static gboolean handleButtonPress( GtkWidget* /*widget*/, GdkEventButton* event, gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); //popupExtras.push_back(child); gtk_widget_set_sensitive( child, FALSE ); -#endif // USE_DOCUMENT_PALETTE gtk_widget_show_all(popupMenu); } ColorItem* item = reinterpret_cast<ColorItem*>(user_data); if ( item ) { - SwatchesPanel* swp = item->ptr ? reinterpret_cast<SwatchesPanel*>(item->ptr) : 0; bool show = swp && (swp->getSelectedIndex() == 0); for ( std::vector<GtkWidget*>::iterator it = popupExtras.begin(); it != popupExtras.end(); ++ it) { gtk_widget_set_sensitive(*it, show); } bounceTarget = item; + bouncePanel = swp; if ( popupMenu ) { gtk_menu_popup(GTK_MENU(popupMenu), NULL, NULL, NULL, NULL, event->button, event->time); handled = TRUE; @@ -513,374 +261,6 @@ static gboolean handleButtonPress( GtkWidget* /*widget*/, GdkEventButton* event, return handled; } -static void dieDieDie( GtkObject *obj, gpointer user_data ) -{ - g_message("die die die %p %p", obj, user_data ); -} - -#include "color.h" // for SP_RGBA32_U_COMPOSE - -void ColorItem::_dropDataIn( GtkWidget */*widget*/, - GdkDragContext */*drag_context*/, - gint /*x*/, gint /*y*/, - GtkSelectionData */*data*/, - guint /*info*/, - guint /*event_time*/, - gpointer /*user_data*/) -{ -} - -static bool bruteForce( SPDocument* document, Inkscape::XML::Node* node, Glib::ustring const& match, int r, int g, int b ) -{ - bool changed = false; - - if ( node ) { - gchar const * val = node->attribute("inkscape:x-fill-tag"); - if ( val && (match == val) ) { - SPObject *obj = document->getObjectByRepr( node ); - - gchar c[64] = {0}; - sp_svg_write_color( c, sizeof(c), SP_RGBA32_U_COMPOSE( r, g, b, 0xff ) ); - SPCSSAttr *css = sp_repr_css_attr_new(); - sp_repr_css_set_property( css, "fill", c ); - - sp_desktop_apply_css_recursive( (SPItem*)obj, css, true ); - ((SPItem*)obj)->updateRepr(); - - changed = true; - } - - val = node->attribute("inkscape:x-stroke-tag"); - if ( val && (match == val) ) { - SPObject *obj = document->getObjectByRepr( node ); - - gchar c[64] = {0}; - sp_svg_write_color( c, sizeof(c), SP_RGBA32_U_COMPOSE( r, g, b, 0xff ) ); - SPCSSAttr *css = sp_repr_css_attr_new(); - sp_repr_css_set_property( css, "stroke", c ); - - sp_desktop_apply_css_recursive( (SPItem*)obj, css, true ); - ((SPItem*)obj)->updateRepr(); - - changed = true; - } - - Inkscape::XML::Node* first = node->firstChild(); - changed |= bruteForce( document, first, match, r, g, b ); - - changed |= bruteForce( document, node->next(), match, r, g, b ); - } - - return changed; -} - -void ColorItem::_colorDefChanged(void* data) -{ - ColorItem* item = reinterpret_cast<ColorItem*>(data); - if ( item ) { - for ( std::vector<Gtk::Widget*>::iterator it = item->_previews.begin(); it != item->_previews.end(); ++it ) { - Gtk::Widget* widget = *it; - if ( IS_EEK_PREVIEW(widget->gobj()) ) { - EekPreview * preview = EEK_PREVIEW(widget->gobj()); - eek_preview_set_color( preview, - (item->def.getR() << 8) | item->def.getR(), - (item->def.getG() << 8) | item->def.getG(), - (item->def.getB() << 8) | item->def.getB() ); - - eek_preview_set_linked( preview, (LinkType)((item->_linkSrc ? PREVIEW_LINK_IN:0) - | (item->_listeners.empty() ? 0:PREVIEW_LINK_OUT) - | (item->_isLive ? PREVIEW_LINK_OTHER:0)) ); - - widget->queue_draw(); - } - } - - for ( std::vector<ColorItem*>::iterator it = item->_listeners.begin(); it != item->_listeners.end(); ++it ) { - guint r = item->def.getR(); - guint g = item->def.getG(); - guint b = item->def.getB(); - - if ( (*it)->_linkIsTone ) { - r = ( ((*it)->_linkPercent * (*it)->_linkGray) + ((100 - (*it)->_linkPercent) * r) ) / 100; - g = ( ((*it)->_linkPercent * (*it)->_linkGray) + ((100 - (*it)->_linkPercent) * g) ) / 100; - b = ( ((*it)->_linkPercent * (*it)->_linkGray) + ((100 - (*it)->_linkPercent) * b) ) / 100; - } else { - r = ( ((*it)->_linkPercent * 255) + ((100 - (*it)->_linkPercent) * r) ) / 100; - g = ( ((*it)->_linkPercent * 255) + ((100 - (*it)->_linkPercent) * g) ) / 100; - b = ( ((*it)->_linkPercent * 255) + ((100 - (*it)->_linkPercent) * b) ) / 100; - } - - (*it)->def.setRGB( r, g, b ); - } - - - // Look for objects using this color - { - SPDesktop *desktop = SP_ACTIVE_DESKTOP; - if ( desktop ) { - SPDocument* document = sp_desktop_document( desktop ); - Inkscape::XML::Node *rroot = sp_document_repr_root( document ); - if ( rroot ) { - - // Find where this thing came from - Glib::ustring paletteName; - bool found = false; - int index = 0; - for ( std::vector<JustForNow*>::iterator it2 = possible.begin(); it2 != possible.end() && !found; ++it2 ) { - JustForNow* curr = *it2; - index = 0; - for ( std::vector<ColorItem*>::iterator zz = curr->_colors.begin(); zz != curr->_colors.end(); ++zz ) { - if ( item == *zz ) { - found = true; - paletteName = curr->_name; - break; - } else { - index++; - } - } - } - - if ( !paletteName.empty() ) { - gchar* str = g_strdup_printf("%d|", index); - paletteName.insert( 0, str ); - g_free(str); - str = 0; - - if ( bruteForce( document, rroot, paletteName, item->def.getR(), item->def.getG(), item->def.getB() ) ) { - sp_document_done( document , SP_VERB_DIALOG_SWATCHES, - _("Change color definition")); - } - } - } - } - } - } -} - - -Gtk::Widget* ColorItem::getPreview(PreviewStyle style, ViewType view, ::PreviewSize size, guint ratio) -{ - Gtk::Widget* widget = 0; - if ( style == PREVIEW_STYLE_BLURB) { - Gtk::Label *lbl = new Gtk::Label(def.descr); - lbl->set_alignment(Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER); - widget = lbl; - } else { -// Glib::ustring blank(" "); -// if ( size == Inkscape::ICON_SIZE_MENU || size == Inkscape::ICON_SIZE_DECORATION ) { -// blank = " "; -// } - - GtkWidget* eekWidget = eek_preview_new(); - EekPreview * preview = EEK_PREVIEW(eekWidget); - Gtk::Widget* newBlot = Glib::wrap(eekWidget); - - if ( previewMap.find(this) == previewMap.end() ){ - eek_preview_set_color( preview, (def.getR() << 8) | def.getR(), - (def.getG() << 8) | def.getG(), - (def.getB() << 8) | def.getB()); - } else { - guchar* px = previewMap[this]; - int width = 128; - int height = 16; - GdkPixbuf* pixbuf = gdk_pixbuf_new_from_data( px, GDK_COLORSPACE_RGB, FALSE, 8, - width, height, width * 3, - 0, // add delete function - 0 ); - eek_preview_set_pixbuf( preview, pixbuf ); - } - if ( def.getType() != ege::PaintDef::RGB ) { - using Inkscape::IO::Resource::get_path; - using Inkscape::IO::Resource::ICONS; - using Inkscape::IO::Resource::SYSTEM; - GError *error = NULL; - gsize bytesRead = 0; - gsize bytesWritten = 0; - gchar *localFilename = g_filename_from_utf8( get_path(SYSTEM, ICONS, "remove-color.png"), - -1, - &bytesRead, - &bytesWritten, - &error); - GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(localFilename, &error); - if (!pixbuf) { - g_warning("Null pixbuf for %p [%s]", localFilename, localFilename ); - } - g_free(localFilename); - - eek_preview_set_pixbuf( preview, pixbuf ); - } - - eek_preview_set_details( preview, (::PreviewStyle)style, (::ViewType)view, (::PreviewSize)size, ratio ); - eek_preview_set_linked( preview, (LinkType)((_linkSrc ? PREVIEW_LINK_IN:0) - | (_listeners.empty() ? 0:PREVIEW_LINK_OUT) - | (_isLive ? PREVIEW_LINK_OTHER:0)) ); - - def.addCallback( _colorDefChanged, this ); - - GValue val = {0, {{0}, {0}}}; - g_value_init( &val, G_TYPE_BOOLEAN ); - g_value_set_boolean( &val, FALSE ); - g_object_set_property( G_OBJECT(preview), "focus-on-click", &val ); - -/* - Gtk::Button *btn = new Gtk::Button(blank); - Gdk::Color color; - color.set_rgb((_r << 8)|_r, (_g << 8)|_g, (_b << 8)|_b); - btn->modify_bg(Gtk::STATE_NORMAL, color); - btn->modify_bg(Gtk::STATE_ACTIVE, color); - btn->modify_bg(Gtk::STATE_PRELIGHT, color); - btn->modify_bg(Gtk::STATE_SELECTED, color); - - Gtk::Widget* newBlot = btn; -*/ - - tips.set_tip((*newBlot), def.descr); - -/* - newBlot->signal_clicked().connect( sigc::mem_fun(*this, &ColorItem::buttonClicked) ); - - sigc::signal<void> type_signal_something; -*/ - - g_signal_connect( G_OBJECT(newBlot->gobj()), - "clicked", - G_CALLBACK(handleClick), - this); - - g_signal_connect( G_OBJECT(newBlot->gobj()), - "alt-clicked", - G_CALLBACK(handleSecondaryClick), - this); - - g_signal_connect( G_OBJECT(newBlot->gobj()), - "button-press-event", - G_CALLBACK(handleButtonPress), - this); - - { - std::vector<std::string> listing = def.getMIMETypes(); - int entryCount = listing.size(); - GtkTargetEntry* entries = new GtkTargetEntry[entryCount]; - GtkTargetEntry* curr = entries; - for ( std::vector<std::string>::iterator it = listing.begin(); it != listing.end(); ++it ) { - curr->target = g_strdup(it->c_str()); - curr->flags = 0; - if ( mimeToInt.find(*it) == mimeToInt.end() ){ - // these next lines are order-dependent: - mimeToInt[*it] = mimeStrings.size(); - mimeStrings.push_back(*it); - } - curr->info = mimeToInt[curr->target]; - curr++; - } - gtk_drag_source_set( GTK_WIDGET(newBlot->gobj()), - GDK_BUTTON1_MASK, - entries, entryCount, - GdkDragAction(GDK_ACTION_MOVE | GDK_ACTION_COPY) ); - for ( int i = 0; i < entryCount; i++ ) { - g_free(entries[i].target); - } - delete[] entries; - } - - g_signal_connect( G_OBJECT(newBlot->gobj()), - "drag-data-get", - G_CALLBACK(ColorItem::_dragGetColorData), - this); - - g_signal_connect( G_OBJECT(newBlot->gobj()), - "drag-begin", - G_CALLBACK(dragBegin), - this ); - - g_signal_connect( G_OBJECT(newBlot->gobj()), - "enter-notify-event", - G_CALLBACK(handleEnterNotify), - this); - - g_signal_connect( G_OBJECT(newBlot->gobj()), - "leave-notify-event", - G_CALLBACK(handleLeaveNotify), - this); - -// g_signal_connect( G_OBJECT(newBlot->gobj()), -// "drag-drop", -// G_CALLBACK(dragDropColorData), -// this); - - if ( def.isEditable() ) - { -// gtk_drag_dest_set( GTK_WIDGET(newBlot->gobj()), -// GTK_DEST_DEFAULT_ALL, -// destColorTargets, -// G_N_ELEMENTS(destColorTargets), -// GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE) ); - - -// g_signal_connect( G_OBJECT(newBlot->gobj()), -// "drag-data-received", -// G_CALLBACK(_dropDataIn), -// this ); - } - - g_signal_connect( G_OBJECT(newBlot->gobj()), - "destroy", - G_CALLBACK(dieDieDie), - this); - - - widget = newBlot; - } - - _previews.push_back( widget ); - - return widget; -} - -void ColorItem::buttonClicked(bool secondary) -{ - SPDesktop *desktop = SP_ACTIVE_DESKTOP; - if (desktop) { - char const * attrName = secondary ? "stroke" : "fill"; - - SPCSSAttr *css = sp_repr_css_attr_new(); - Glib::ustring descr; - switch (def.getType()) { - case ege::PaintDef::CLEAR: { - // TODO actually make this clear - sp_repr_css_set_property( css, attrName, "none" ); - descr = secondary? _("Remove stroke color") : _("Remove fill color"); - break; - } - case ege::PaintDef::NONE: { - sp_repr_css_set_property( css, attrName, "none" ); - descr = secondary? _("Set stroke color to none") : _("Set fill color to none"); - break; - } - case ege::PaintDef::RGB: { - Glib::ustring colorspec; - if ( gradMap.find(this) == gradMap.end() ){ - gchar c[64]; - guint32 rgba = (def.getR() << 24) | (def.getG() << 16) | (def.getB() << 8) | 0xff; - sp_svg_write_color(c, sizeof(c), rgba); - colorspec = c; - } else { - SPGradient* grad = gradMap[this]; - colorspec = "url(#"; - colorspec += grad->id; - colorspec += ")"; - } - sp_repr_css_set_property( css, attrName, colorspec.c_str() ); - descr = secondary? _("Set stroke color from swatch") : _("Set fill color from swatch"); - break; - } - } - sp_desktop_set_style(desktop, css); - sp_repr_css_attr_unref(css); - - sp_document_done( sp_desktop_document(desktop), SP_VERB_DIALOG_SWATCHES, descr.c_str() ); - } -} static char* trim( char* str ) { char* ret = str; @@ -915,149 +295,6 @@ bool parseNum( char*& str, int& val ) { } -static bool getBlock( std::string& dst, guchar ch, std::string const str ) -{ - bool good = false; - std::string::size_type pos = str.find(ch); - if ( pos != std::string::npos ) - { - std::string::size_type pos2 = str.find( '(', pos ); - if ( pos2 != std::string::npos ) { - std::string::size_type endPos = str.find( ')', pos2 ); - if ( endPos != std::string::npos ) { - dst = str.substr( pos2 + 1, (endPos - pos2 - 1) ); - good = true; - } - } - } - return good; -} - -static bool popVal( guint64& numVal, std::string& str ) -{ - bool good = false; - std::string::size_type endPos = str.find(','); - if ( endPos == std::string::npos ) { - endPos = str.length(); - } - - if ( endPos != std::string::npos && endPos > 0 ) { - std::string xxx = str.substr( 0, endPos ); - const gchar* ptr = xxx.c_str(); - gchar* endPtr = 0; - numVal = g_ascii_strtoull( ptr, &endPtr, 10 ); - if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) { - // overflow - } else if ( (numVal == 0) && (endPtr == ptr) ) { - // failed conversion - } else { - good = true; - str.erase( 0, endPos + 1 ); - } - } - - return good; -} - -void ColorItem::_wireMagicColors( void* p ) -{ - JustForNow* onceMore = reinterpret_cast<JustForNow*>(p); - if ( onceMore ) - { - for ( std::vector<ColorItem*>::iterator it = onceMore->_colors.begin(); it != onceMore->_colors.end(); ++it ) - { - std::string::size_type pos = (*it)->def.descr.find("*{"); - if ( pos != std::string::npos ) - { - std::string subby = (*it)->def.descr.substr( pos + 2 ); - std::string::size_type endPos = subby.find("}*"); - if ( endPos != std::string::npos ) - { - subby.erase( endPos ); - //g_message("FOUND MAGIC at '%s'", (*it)->def.descr.c_str()); - //g_message(" '%s'", subby.c_str()); - - if ( subby.find('E') != std::string::npos ) - { - (*it)->def.setEditable( true ); - } - - if ( subby.find('L') != std::string::npos ) - { - (*it)->_isLive = true; - } - - std::string part; - // Tint. index + 1 more val. - if ( getBlock( part, 'T', subby ) ) { - guint64 colorIndex = 0; - if ( popVal( colorIndex, part ) ) { - guint64 percent = 0; - if ( popVal( percent, part ) ) { - (*it)->_linkTint( *(onceMore->_colors[colorIndex]), percent ); - } - } - } - - // Shade/tone. index + 1 or 2 more val. - if ( getBlock( part, 'S', subby ) ) { - guint64 colorIndex = 0; - if ( popVal( colorIndex, part ) ) { - guint64 percent = 0; - if ( popVal( percent, part ) ) { - guint64 grayLevel = 0; - if ( !popVal( grayLevel, part ) ) { - grayLevel = 0; - } - (*it)->_linkTone( *(onceMore->_colors[colorIndex]), percent, grayLevel ); - } - } - } - - } - } - } - } -} - - -void ColorItem::_linkTint( ColorItem& other, int percent ) -{ - if ( !_linkSrc ) - { - other._listeners.push_back(this); - _linkIsTone = false; - _linkPercent = percent; - if ( _linkPercent > 100 ) - _linkPercent = 100; - if ( _linkPercent < 0 ) - _linkPercent = 0; - _linkGray = 0; - _linkSrc = &other; - - ColorItem::_colorDefChanged(&other); - } -} - -void ColorItem::_linkTone( ColorItem& other, int percent, int grayLevel ) -{ - if ( !_linkSrc ) - { - other._listeners.push_back(this); - _linkIsTone = true; - _linkPercent = percent; - if ( _linkPercent > 100 ) - _linkPercent = 100; - if ( _linkPercent < 0 ) - _linkPercent = 0; - _linkGray = grayLevel; - _linkSrc = &other; - - ColorItem::_colorDefChanged(&other); - } -} - - void _loadPaletteFile( gchar const *filename ) { char block[1024]; @@ -1069,7 +306,7 @@ void _loadPaletteFile( gchar const *filename ) bool inHeader = true; bool hasErr = false; - JustForNow *onceMore = new JustForNow(); + SwatchPage *onceMore = new SwatchPage(); do { result = fgets( block, sizeof(block), f ); @@ -1245,46 +482,45 @@ SwatchesPanel::SwatchesPanel(gchar const* prefsPath) : _remove(0), _currentIndex(0), _currentDesktop(0), - _currentDocument(0), - _ptr(0) + _currentDocument(0) { Gtk::RadioMenuItem* hotItem = 0; _holder = new PreviewHolder(); _clear = new ColorItem( ege::PaintDef::CLEAR ); - _clear->ptr = this; _remove = new ColorItem( ege::PaintDef::NONE ); - _remove->ptr = this; -#if USE_DOCUMENT_PALETTE - { - JustForNow *docPalette = new JustForNow(); + if (docPalettes.empty()) { + SwatchPage *docPalette = new SwatchPage(); docPalette->_name = "Auto"; - possible.push_back(docPalette); - - _ptr = docPalette; + docPalettes[0] = docPalette; } -#endif // USE_DOCUMENT_PALETTE + loadEmUp(); if ( !possible.empty() ) { - JustForNow* first = 0; + SwatchPage* first = 0; int index = 0; Glib::ustring targetName; if ( !_prefs_path.empty() ) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); targetName = prefs->getString(_prefs_path + "/palette"); if (!targetName.empty()) { - for ( std::vector<JustForNow*>::iterator iter = possible.begin(); iter != possible.end(); ++iter ) { - if ( (*iter)->_name == targetName ) { - first = *iter; - break; - } + if (targetName == "Auto") { + first = docPalettes[0]; + } else { index++; + for ( std::vector<SwatchPage*>::iterator iter = possible.begin(); iter != possible.end(); ++iter ) { + if ( (*iter)->_name == targetName ) { + first = *iter; + break; + } + index++; + } } } } if ( !first ) { - first = possible.front(); + first = docPalettes[0]; _currentIndex = 0; } else { _currentIndex = index; @@ -1295,8 +531,9 @@ SwatchesPanel::SwatchesPanel(gchar const* prefsPath) : Gtk::RadioMenuItem::Group groupOne; int i = 0; - for ( std::vector<JustForNow*>::iterator it = possible.begin(); it != possible.end(); it++ ) { - JustForNow* curr = *it; + std::vector<SwatchPage*> swatchSets = _getSwatchSets(); + for ( std::vector<SwatchPage*>::iterator it = swatchSets.begin(); it != swatchSets.end(); it++ ) { + SwatchPage* curr = *it; Gtk::RadioMenuItem* single = manage(new Gtk::RadioMenuItem(groupOne, curr->_name)); if ( curr == first ) { hotItem = single; @@ -1320,11 +557,10 @@ SwatchesPanel::SwatchesPanel(gchar const* prefsPath) : SwatchesPanel::~SwatchesPanel() { + _trackDocument( this, 0 ); + _documentConnection.disconnect(); - _resourceConnection.disconnect(); _selChanged.disconnect(); - _setModified.disconnect(); - _subselChanged.disconnect(); if ( _clear ) { delete _clear; @@ -1354,8 +590,6 @@ void SwatchesPanel::setDesktop( SPDesktop* desktop ) if ( _currentDesktop ) { _documentConnection.disconnect(); _selChanged.disconnect(); - _setModified.disconnect(); - _subselChanged.disconnect(); } _currentDesktop = desktop; @@ -1382,104 +616,235 @@ void SwatchesPanel::setDesktop( SPDesktop* desktop ) } } -void SwatchesPanel::_setDocument( SPDocument *document ) + +class DocTrack { - if ( document != _currentDocument ) { - if ( _currentDocument ) { - _resourceConnection.disconnect(); +public: + DocTrack(SPDocument *doc, sigc::connection &gradientRsrcChanged, sigc::connection &defsChanged, sigc::connection &defsModified) : + doc(doc), + gradientRsrcChanged(gradientRsrcChanged), + defsChanged(defsChanged), + defsModified(defsModified) + { + } + + ~DocTrack() + { + if (doc) { + gradientRsrcChanged.disconnect(); + defsChanged.disconnect(); + defsModified.disconnect(); } - _currentDocument = document; - if ( _currentDocument ) { - _resourceConnection = sp_document_resources_changed_connect(document, - "gradient", - sigc::mem_fun(*this, &SwatchesPanel::handleGradientsChange)); + } + + SPDocument *doc; + sigc::connection gradientRsrcChanged; + sigc::connection defsChanged; + sigc::connection defsModified; + +private: + DocTrack(DocTrack const &); // no copy + DocTrack &operator=(DocTrack const &); // no assign +}; + +void SwatchesPanel::_trackDocument( SwatchesPanel *panel, SPDocument *document ) +{ + SPDocument *oldDoc = 0; + if (docPerPanel.find(panel) != docPerPanel.end()) { + oldDoc = docPerPanel[panel]; + if (!oldDoc) { + docPerPanel.erase(panel); // Should not be needed, but clean up just in case. + } + } + if (oldDoc != document) { + if (oldDoc) { + docPerPanel[panel] = 0; + bool found = false; + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) { + found = (it->second == document); + } + if (!found) { + for (std::vector<DocTrack*>::iterator it = docTrackings.begin(); it != docTrackings.end(); ++it){ + if ((*it)->doc == oldDoc) { + delete *it; + docTrackings.erase(it); + break; + } + } + } + } + + if (document) { + bool found = false; + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) { + found = (it->second == document); + } + docPerPanel[panel] = document; + if (!found) { + sigc::connection conn1 = sp_document_resources_changed_connect( document, "gradient", sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleGradientsChange), document) ); + sigc::connection conn2 = SP_DOCUMENT_DEFS(document)->connectRelease( sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document)) ); + sigc::connection conn3 = SP_DOCUMENT_DEFS(document)->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document))) ); + + DocTrack *dt = new DocTrack(document, conn1, conn2, conn3); + docTrackings.push_back(dt); + + if (docPalettes.find(document) == docPalettes.end()) { + SwatchPage *docPalette = new SwatchPage(); + docPalette->_name = "Auto"; + docPalettes[document] = docPalette; + } + } } + } - handleGradientsChange(); + std::set<SPDocument*> docs; + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); it != docPerPanel.end(); ++it) { + docs.insert(it->second); } } -void SwatchesPanel::handleGradientsChange() +void SwatchesPanel::_setDocument( SPDocument *document ) +{ + if ( document != _currentDocument ) { + _trackDocument(this, document); + _currentDocument = document; + handleGradientsChange( document ); + } +} + +static void recalcSwatchContents(SPDocument* doc, + std::vector<ColorItem*> &tmpColors, + std::map<ColorItem*, guchar*> &previewMappings, + std::map<ColorItem*, SPGradient*> &gradMappings) { std::vector<SPGradient*> newList; - const GSList *gradients = sp_document_get_resource_list(_currentDocument, "gradient"); + const GSList *gradients = sp_document_get_resource_list(doc, "gradient"); for (const GSList *item = gradients; item; item = item->next) { SPGradient* grad = SP_GRADIENT(item->data); - if ( grad->has_stops ) { + if ( grad->isSwatch() ) { newList.push_back(SP_GRADIENT(item->data)); } } -#if USE_DOCUMENT_PALETTE - if ( _ptr ) { - JustForNow *docPalette = reinterpret_cast<JustForNow *>(_ptr); - // TODO delete pointed to objects - docPalette->_colors.clear(); - if ( !newList.empty() ) { - for ( std::vector<SPGradient*>::iterator it = newList.begin(); it != newList.end(); ++it ) - { - SPGradient* grad = *it; - if ( grad->repr->attribute("osb:paint") ) { - sp_gradient_ensure_vector( grad ); - SPGradientStop first = grad->vector.stops[0]; - SPColor color = first.color; - guint32 together = color.toRGBA32(first.opacity); - - // At the moment we can't trust the count of 1 vs 2 stops. - SPGradientStop second = (*it)->vector.stops[1]; - SPColor color2 = second.color; - guint32 together2 = color2.toRGBA32(second.opacity); - - if ( (grad->vector.stops.size() <= 2) && (together == together2) ) { - // Treat as solid-color - Glib::ustring name( grad->id ); - unsigned int r = SP_RGBA32_R_U(together); - unsigned int g = SP_RGBA32_G_U(together); - unsigned int b = SP_RGBA32_B_U(together); - ColorItem* item = new ColorItem( r, g, b, name ); - item->ptr = this; - docPalette->_colors.push_back(item); - gradMap[item] = grad; - } else { - // Treat as gradient - Glib::ustring name( grad->id ); - unsigned int r = SP_RGBA32_R_U(together); - unsigned int g = SP_RGBA32_G_U(together); - unsigned int b = SP_RGBA32_B_U(together); - ColorItem* item = new ColorItem( r, g, b, name ); - item->ptr = this; - docPalette->_colors.push_back(item); - - gint width = 128; - gint height = VBLOCK; - guchar* px = g_new( guchar, 3 * height * width ); - nr_render_checkerboard_rgb( px, width, VBLOCK, 3 * width, 0, 0 ); - - sp_gradient_render_vector_block_rgb( grad, - px, width, height, 3 * width, - 0, width, TRUE ); - - previewMap[item] = px; - gradMap[item] = grad; - } + if ( !newList.empty() ) { + for ( std::vector<SPGradient*>::iterator it = newList.begin(); it != newList.end(); ++it ) + { + SPGradient* grad = *it; + sp_gradient_ensure_vector( grad ); + SPGradientStop first = grad->vector.stops[0]; + SPColor color = first.color; + guint32 together = color.toRGBA32(first.opacity); + + SPGradientStop second = (*it)->vector.stops[1]; + SPColor color2 = second.color; + + Glib::ustring name( grad->getId() ); + unsigned int r = SP_RGBA32_R_U(together); + unsigned int g = SP_RGBA32_G_U(together); + unsigned int b = SP_RGBA32_B_U(together); + ColorItem* item = new ColorItem( r, g, b, name ); + + gint width = PREVIEW_PIXBUF_WIDTH; + gint height = VBLOCK; + guchar* px = g_new( guchar, 3 * height * width ); + nr_render_checkerboard_rgb( px, width, height, 3 * width, 0, 0 ); + + sp_gradient_render_vector_block_rgb( grad, + px, width, height, 3 * width, + 0, width, TRUE ); + + previewMappings[item] = px; + + tmpColors.push_back(item); + gradMappings[item] = grad; + } + } +} + +void SwatchesPanel::handleGradientsChange(SPDocument *document) +{ + SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : 0; + if (docPalette) { + std::vector<ColorItem*> tmpColors; + std::map<ColorItem*, guchar*> tmpPrevs; + std::map<ColorItem*, SPGradient*> tmpGrads; + recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads); + + for (std::map<ColorItem*, guchar*>::iterator it = tmpPrevs.begin(); it != tmpPrevs.end(); ++it) { + it->first->setPixData(it->second, PREVIEW_PIXBUF_WIDTH, VBLOCK); + } + + for (std::map<ColorItem*, SPGradient*>::iterator it = tmpGrads.begin(); it != tmpGrads.end(); ++it) { + it->first->setGradient(it->second); + } + + docPalette->_colors.swap(tmpColors); + for (std::vector<ColorItem*>::iterator it = tmpColors.begin(); it != tmpColors.end(); ++it) { + delete *it; + } + + + // Figure out which SwatchesPanel instances are affected and update them. + + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); it != docPerPanel.end(); ++it) { + if (it->second == document) { + SwatchesPanel* swp = it->first; + std::vector<SwatchPage*> pages = swp->_getSwatchSets(); + SwatchPage* curr = pages[swp->_currentIndex]; + if (curr == docPalette) { + swp->_rebuild(); } } } - JustForNow* curr = possible[_currentIndex]; - if (curr == docPalette) { - _rebuild(); + } +} + +void SwatchesPanel::handleDefsModified(SPDocument *document) +{ + SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : 0; + if (docPalette) { + std::vector<ColorItem*> tmpColors; + std::map<ColorItem*, guchar*> tmpPrevs; + std::map<ColorItem*, SPGradient*> tmpGrads; + recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads); + + int cap = std::min(docPalette->_colors.size(), tmpColors.size()); + for (int i = 0; i < cap; i++) { + ColorItem* newColor = tmpColors[i]; + ColorItem* oldColor = docPalette->_colors[i]; + if ( (newColor->def.getType() != oldColor->def.getType()) || + (newColor->def.getR() != oldColor->def.getR()) || + (newColor->def.getG() != oldColor->def.getG()) || + (newColor->def.getB() != oldColor->def.getB()) ) { + oldColor->def.setRGB(newColor->def.getR(), newColor->def.getG(), newColor->def.getB()); + } + if (tmpGrads.find(newColor) != tmpGrads.end()) { + oldColor->setGradient(tmpGrads[newColor]); + } + if ( tmpPrevs.find(newColor) != tmpPrevs.end() ) { + oldColor->setPixData(tmpPrevs[newColor], PREVIEW_PIXBUF_WIDTH, VBLOCK); + } } } -#endif // USE_DOCUMENT_PALETTE } -void SwatchesPanel::_updateFromSelection() +std::vector<SwatchPage*> SwatchesPanel::_getSwatchSets() const { -#if USE_DOCUMENT_PALETTE - if ( _ptr ) { - JustForNow *docPalette = reinterpret_cast<JustForNow *>(_ptr); + std::vector<SwatchPage*> tmp; + if (docPalettes.find(_currentDocument) != docPalettes.end()) { + tmp.push_back(docPalettes[_currentDocument]); + } + tmp.insert(tmp.end(), possible.begin(), possible.end()); + + return tmp; +} + +void SwatchesPanel::_updateFromSelection() +{ + SwatchPage *docPalette = (docPalettes.find(_currentDocument) != docPalettes.end()) ? docPalettes[_currentDocument] : 0; + if ( docPalette ) { Glib::ustring fillId; Glib::ustring strokeId; @@ -1554,7 +919,6 @@ void SwatchesPanel::_updateFromSelection() item->setState( isFill, isStroke ); } } -#endif // USE_DOCUMENT_PALETTE } void SwatchesPanel::_handleAction( int setId, int itemId ) @@ -1562,12 +926,13 @@ void SwatchesPanel::_handleAction( int setId, int itemId ) switch( setId ) { case 3: { - if ( itemId >= 0 && itemId < static_cast<int>(possible.size()) ) { + std::vector<SwatchPage*> pages = _getSwatchSets(); + if ( itemId >= 0 && itemId < static_cast<int>(pages.size()) ) { _currentIndex = itemId; if ( !_prefs_path.empty() ) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->setString(_prefs_path + "/palette", possible[_currentIndex]->_name); + prefs->setString(_prefs_path + "/palette", pages[_currentIndex]->_name); } _rebuild(); @@ -1579,7 +944,8 @@ void SwatchesPanel::_handleAction( int setId, int itemId ) void SwatchesPanel::_rebuild() { - JustForNow* curr = possible[_currentIndex]; + std::vector<SwatchPage*> pages = _getSwatchSets(); + SwatchPage* curr = pages[_currentIndex]; _holder->clear(); if ( curr->_prefWidth > 0 ) { diff --git a/src/ui/dialog/swatches.h b/src/ui/dialog/swatches.h index 35bcd8c78..b18fd6cad 100644 --- a/src/ui/dialog/swatches.h +++ b/src/ui/dialog/swatches.h @@ -11,17 +11,18 @@ #define SEEN_DIALOGS_SWATCHES_H #include <gtkmm/textview.h> -#include <gtkmm/tooltips.h> #include "ui/widget/panel.h" -#include "ui/previewholder.h" -#include "widgets/ege-paint-def.h" namespace Inkscape { namespace UI { + +class PreviewHolder; + namespace Dialogs { class ColorItem; +class SwatchPage; /** * A panel that displays paint swatches. @@ -40,7 +41,6 @@ public: virtual SPDesktop* getDesktop() {return _currentDesktop;} virtual int getSelectedIndex() {return _currentIndex;} // temporary - virtual void handleGradientsChange(); // temporary protected: virtual void _updateFromSelection(); @@ -48,23 +48,25 @@ protected: virtual void _setDocument( SPDocument *document ); virtual void _rebuild(); + virtual std::vector<SwatchPage*> _getSwatchSets() const; + private: SwatchesPanel(SwatchesPanel const &); // no copy SwatchesPanel &operator=(SwatchesPanel const &); // no assign + static void _trackDocument( SwatchesPanel *panel, SPDocument *document ); + static void handleGradientsChange(SPDocument *document); + static void handleDefsModified(SPDocument *document); + PreviewHolder* _holder; ColorItem* _clear; ColorItem* _remove; int _currentIndex; SPDesktop* _currentDesktop; SPDocument* _currentDocument; - void* _ptr; sigc::connection _documentConnection; - sigc::connection _resourceConnection; sigc::connection _selChanged; - sigc::connection _setModified; - sigc::connection _subselChanged; }; } //namespace Dialogs diff --git a/src/ui/icon-names.h b/src/ui/icon-names.h index 76e76ea34..d36d4a41a 100644 --- a/src/ui/icon-names.h +++ b/src/ui/icon-names.h @@ -350,6 +350,8 @@ "paint-pattern" #define INKSCAPE_ICON_PAINT_SOLID \ "paint-solid" +#define INKSCAPE_ICON_PAINT_SWATCH \ + "paint-swatch" #define INKSCAPE_ICON_PAINT_UNKNOWN \ "paint-unknown" #define INKSCAPE_ICON_PATH_BREAK_APART \ diff --git a/src/ui/tool/Makefile_insert b/src/ui/tool/Makefile_insert new file mode 100644 index 000000000..2a72dd645 --- /dev/null +++ b/src/ui/tool/Makefile_insert @@ -0,0 +1,32 @@ +## Makefile.am fragment sourced by src/Makefile.am. + +ink_common_sources += \ + ui/tool/control-point.cpp \ + ui/tool/control-point.h \ + ui/tool/control-point-selection.cpp \ + ui/tool/control-point-selection.h \ + ui/tool/commit-events.h \ + ui/tool/curve-drag-point.cpp \ + ui/tool/curve-drag-point.h \ + ui/tool/event-utils.cpp \ + ui/tool/event-utils.h \ + ui/tool/manipulator.cpp \ + ui/tool/manipulator.h \ + ui/tool/modifier-tracker.cpp \ + ui/tool/modifier-tracker.h \ + ui/tool/multi-path-manipulator.cpp \ + ui/tool/multi-path-manipulator.h \ + 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 \ + ui/tool/selectable-control-point.h \ + ui/tool/selector.cpp \ + ui/tool/selector.h \ + ui/tool/shape-record.h \ + ui/tool/transform-handle-set.cpp \ + ui/tool/transform-handle-set.h diff --git a/src/ui/tool/commit-events.h b/src/ui/tool/commit-events.h new file mode 100644 index 000000000..d99872766 --- /dev/null +++ b/src/ui/tool/commit-events.h @@ -0,0 +1,51 @@ +/** @file + * Commit events. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_COMMIT_EVENTS_H +#define SEEN_UI_TOOL_COMMIT_EVENTS_H + +namespace Inkscape { +namespace UI { + +/// This is used to provide sensible messages on the undo stack. +enum CommitEvent { + COMMIT_MOUSE_MOVE, + COMMIT_KEYBOARD_MOVE_X, + COMMIT_KEYBOARD_MOVE_Y, + COMMIT_MOUSE_SCALE, + COMMIT_MOUSE_SCALE_UNIFORM, + COMMIT_KEYBOARD_SCALE_UNIFORM, + COMMIT_KEYBOARD_SCALE_X, + COMMIT_KEYBOARD_SCALE_Y, + COMMIT_MOUSE_ROTATE, + COMMIT_KEYBOARD_ROTATE, + COMMIT_MOUSE_SKEW_X, + COMMIT_MOUSE_SKEW_Y, + COMMIT_KEYBOARD_SKEW_X, + COMMIT_KEYBOARD_SKEW_Y, + COMMIT_FLIP_X, + COMMIT_FLIP_Y +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/control-point-selection.cpp b/src/ui/tool/control-point-selection.cpp new file mode 100644 index 000000000..f880d2ddf --- /dev/null +++ b/src/ui/tool/control-point-selection.cpp @@ -0,0 +1,613 @@ +/** @file + * Node selection - implementation + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <boost/none.hpp> +#include <2geom/transforms.h> +#include "desktop.h" +#include "preferences.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/selectable-control-point.h" +#include "ui/tool/transform-handle-set.h" + +namespace Inkscape { +namespace UI { + +/** + * @class ControlPointSelection + * @brief Group of selected control points. + * + * Some operations can be performed on all selected points regardless of their type, therefore + * this class is also a Manipulator. It handles the transformations of points using + * the keyboard. + * + * The exposed interface is similar to that of an STL set. Internally, a hash map is used. + * @todo Correct iterators (that don't expose the connection list) + */ + +/** @var ControlPointSelection::signal_update + * Fires when the display needs to be updated to reflect changes. + */ +/** @var ControlPointSelection::signal_point_changed + * Fires when a control point is added to or removed from the selection. + * The first param contains a pointer to the control point that changed sel. state. + * The second says whether the point is currently selected. + */ +/** @var ControlPointSelection::signal_commit + * Fires when a change that needs to be committed to XML happens. + */ + +ControlPointSelection::ControlPointSelection(SPDesktop *d, SPCanvasGroup *th_group) + : Manipulator(d) + , _handles(new TransformHandleSet(d, th_group)) + , _dragging(false) + , _handles_visible(true) + , _one_node_handles(false) +{ + signal_update.connect( sigc::bind( + sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles), + true)); + ControlPoint::signal_mouseover_change.connect( + sigc::hide( + sigc::mem_fun(*this, &ControlPointSelection::_mouseoverChanged))); + _handles->signal_transform.connect( + sigc::mem_fun(*this, &ControlPointSelection::transform)); + _handles->signal_commit.connect( + sigc::mem_fun(*this, &ControlPointSelection::_commitHandlesTransform)); +} + +ControlPointSelection::~ControlPointSelection() +{ + clear(); + delete _handles; +} + +/** Add a control point to the selection. */ +std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(const value_type &x) +{ + iterator found = _points.find(x); + if (found != _points.end()) { + return std::pair<iterator, bool>(found, false); + } + + found = _points.insert(x).first; + + x->updateState(); + _pointChanged(x, true); + + return std::pair<iterator, bool>(found, true); +} + +/** Remove a point from the selection. */ +void ControlPointSelection::erase(iterator pos) +{ + SelectableControlPoint *erased = *pos; + _points.erase(pos); + erased->updateState(); + _pointChanged(erased, false); +} +ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k) +{ + iterator pos = _points.find(k); + if (pos == _points.end()) return 0; + erase(pos); + return 1; +} +void ControlPointSelection::erase(iterator first, iterator last) +{ + while (first != last) erase(first++); +} + +/** Remove all points from the selection, making it empty. */ +void ControlPointSelection::clear() +{ + for (iterator i = begin(); i != end(); ) + erase(i++); +} + +/** Select all points that this selection can contain. */ +void ControlPointSelection::selectAll() +{ + for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) { + insert(*i); + } +} +/** Select all points inside the given rectangle (in desktop coordinates). */ +void ControlPointSelection::selectArea(Geom::Rect const &r) +{ + for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) { + if (r.contains(**i)) + insert(*i); + } +} +/** Unselect all selected points and select all unselected points. */ +void ControlPointSelection::invertSelection() +{ + for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) { + if ((*i)->selected()) erase(*i); + else insert(*i); + } +} +void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir) +{ + bool grow = (dir > 0); + Geom::Point p = origin->position(); + double best_dist = grow ? HUGE_VAL : 0; + SelectableControlPoint *match = NULL; + for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) { + bool selected = (*i)->selected(); + if (grow && !selected) { + double dist = Geom::distance((*i)->position(), p); + if (dist < best_dist) { + best_dist = dist; + match = *i; + } + } + if (!grow && selected) { + double dist = Geom::distance((*i)->position(), p); + // use >= to also deselect the origin node when it's the last one selected + if (dist >= best_dist) { + best_dist = dist; + match = *i; + } + } + } + if (match) { + if (grow) insert(match); + else erase(match); + } +} + +/** Transform all selected control points by the given affine transformation. */ +void ControlPointSelection::transform(Geom::Matrix const &m) +{ + for (iterator i = _points.begin(); i != _points.end(); ++i) { + SelectableControlPoint *cur = *i; + cur->transform(m); + } + _updateBounds(); + // TODO preserving the rotation radius needs some rethinking... + if (_rot_radius) (*_rot_radius) *= m.descrim(); + if (_mouseover_rot_radius) (*_mouseover_rot_radius) *= m.descrim(); + signal_update.emit(); +} + +/** Align control points on the specified axis. */ +void ControlPointSelection::align(Geom::Dim2 axis) +{ + if (empty()) return; + Geom::Dim2 d = static_cast<Geom::Dim2>((axis + 1) % 2); + + Geom::OptInterval bound; + for (iterator i = _points.begin(); i != _points.end(); ++i) { + bound.unionWith(Geom::OptInterval((*i)->position()[d])); + } + + double new_coord = bound->middle(); + for (iterator i = _points.begin(); i != _points.end(); ++i) { + Geom::Point pos = (*i)->position(); + pos[d] = new_coord; + (*i)->move(pos); + } +} + +/** Equdistantly distribute control points by moving them in the specified dimension. */ +void ControlPointSelection::distribute(Geom::Dim2 d) +{ + if (empty()) return; + + // this needs to be a multimap, otherwise it will fail when some points have the same coord + typedef std::multimap<double, SelectableControlPoint*> SortMap; + + SortMap sm; + Geom::OptInterval bound; + // first we insert all points into a multimap keyed by the aligned coord to sort them + // simultaneously we compute the extent of selection + for (iterator i = _points.begin(); i != _points.end(); ++i) { + Geom::Point pos = (*i)->position(); + sm.insert(std::make_pair(pos[d], (*i))); + bound.unionWith(Geom::OptInterval(pos[d])); + } + + // now we iterate over the multimap and set aligned positions. + double step = size() == 1 ? 0 : bound->extent() / (size() - 1); + double start = bound->min(); + unsigned num = 0; + for (SortMap::iterator i = sm.begin(); i != sm.end(); ++i, ++num) { + Geom::Point pos = i->second->position(); + pos[d] = start + num * step; + i->second->move(pos); + } +} + +/** Get the bounds of the selection. + * @return Smallest rectangle containing the positions of all selected points, + * or nothing if the selection is empty */ +Geom::OptRect ControlPointSelection::pointwiseBounds() +{ + return _bounds; +} + +Geom::OptRect ControlPointSelection::bounds() +{ + return size() == 1 ? (*_points.begin())->bounds() : _bounds; +} + +void ControlPointSelection::showTransformHandles(bool v, bool one_node) +{ + _one_node_handles = one_node; + _handles_visible = v; + _updateTransformHandles(false); +} + +void ControlPointSelection::hideTransformHandles() +{ + _handles->setVisible(false); +} +void ControlPointSelection::restoreTransformHandles() +{ + _updateTransformHandles(true); +} + +void ControlPointSelection::toggleTransformHandlesMode() +{ + if (_handles->mode() == TransformHandleSet::MODE_SCALE) { + _handles->setMode(TransformHandleSet::MODE_ROTATE_SKEW); + if (size() == 1) _handles->rotationCenter().setVisible(false); + } else { + _handles->setMode(TransformHandleSet::MODE_SCALE); + } +} + +void ControlPointSelection::_pointGrabbed(SelectableControlPoint *point) +{ + hideTransformHandles(); + _dragging = true; + _grabbed_point = point; + _farthest_point = point; + double maxdist = 0; + for (iterator i = _points.begin(); i != _points.end(); ++i) { + _original_positions.insert(std::make_pair(*i, (*i)->position())); + double dist = Geom::distance(*_grabbed_point, **i); + if (dist > maxdist) { + maxdist = dist; + _farthest_point = *i; + } + } +} + +void ControlPointSelection::_pointDragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + Geom::Point abs_delta = new_pos - _original_positions[_grabbed_point]; + double fdist = Geom::distance(_original_positions[_grabbed_point], _original_positions[_farthest_point]); + if (held_alt(*event) && fdist > 0) { + // sculpting + for (iterator i = _points.begin(); i != _points.end(); ++i) { + SelectableControlPoint *cur = (*i); + double dist = Geom::distance(_original_positions[cur], _original_positions[_grabbed_point]); + double deltafrac = 0.5 + 0.5 * cos(M_PI * dist/fdist); + cur->move(_original_positions[cur] + abs_delta * deltafrac); + } + } else { + Geom::Point delta = new_pos - _grabbed_point->position(); + for (iterator i = _points.begin(); i != _points.end(); ++i) { + SelectableControlPoint *cur = (*i); + cur->move(_original_positions[cur] + abs_delta); + } + _handles->rotationCenter().move(_handles->rotationCenter().position() + delta); + } + signal_update.emit(); +} + +void ControlPointSelection::_pointUngrabbed() +{ + _original_positions.clear(); + _dragging = false; + _grabbed_point = _farthest_point = NULL; + _updateBounds(); + restoreTransformHandles(); + signal_commit.emit(COMMIT_MOUSE_MOVE); +} + +bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventButton *event) +{ + // clicking a selected node should toggle the transform handles between rotate and scale mode, + // if they are visible + if (held_no_modifiers(*event) && _handles_visible && p->selected()) { + toggleTransformHandlesMode(); + return true; + } + return false; +} + +void ControlPointSelection::_pointChanged(SelectableControlPoint *p, bool selected) +{ + _updateBounds(); + _updateTransformHandles(false); + if (_bounds) + _handles->rotationCenter().move(_bounds->midpoint()); + + signal_point_changed.emit(p, selected); +} + +void ControlPointSelection::_mouseoverChanged() +{ + _mouseover_rot_radius = boost::none; +} + +void ControlPointSelection::_updateBounds() +{ + _rot_radius = boost::none; + _bounds = Geom::OptRect(); + for (iterator i = _points.begin(); i != _points.end(); ++i) { + SelectableControlPoint *cur = (*i); + Geom::Point p = cur->position(); + if (!_bounds) { + _bounds = Geom::Rect(p, p); + } else { + _bounds->expandTo(p); + } + } +} + +void ControlPointSelection::_updateTransformHandles(bool preserve_center) +{ + if (_dragging) return; + + if (_handles_visible && size() > 1) { + _handles->setBounds(*bounds(), preserve_center); + _handles->setVisible(true); + } else if (_one_node_handles && size() == 1) { // only one control point in selection + SelectableControlPoint *p = *begin(); + _handles->setBounds(p->bounds()); + _handles->rotationCenter().move(p->position()); + _handles->rotationCenter().setVisible(false); + _handles->setVisible(true); + } else { + _handles->setVisible(false); + } +} + +/** Moves the selected points along the supplied unit vector according to + * the modifier state of the supplied event. */ +bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir) +{ + if (held_control(event)) return false; + unsigned num = 1 + combine_key_events(shortcut_key(event), 0); + + Geom::Point delta = dir * num; + if (held_shift(event)) delta *= 10; + if (held_alt(event)) { + delta /= _desktop->current_zoom(); + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000); + delta *= nudge; + } + + transform(Geom::Translate(delta)); + if (fabs(dir[Geom::X]) > 0) { + signal_commit.emit(COMMIT_KEYBOARD_MOVE_X); + } else { + signal_commit.emit(COMMIT_KEYBOARD_MOVE_Y); + } + return true; +} + +/** @brief Computes the distance to the farthest corner of the bounding box. + * Used to determine what it means to "rotate by one pixel". */ +double ControlPointSelection::_rotationRadius(Geom::Point const &rc) +{ + if (empty()) return 1.0; // some safe value + Geom::Rect b = *bounds(); + double maxlen = 0; + for (unsigned i = 0; i < 4; ++i) { + double len = Geom::distance(b.corner(i), rc); + if (len > maxlen) maxlen = len; + } + return maxlen; +} + +/** Rotates the selected points in the given direction according to the modifier state + * from the supplied event. + * @param event Key event to take modifier state from + * @param dir Direction of rotation (math convention: 1 = counterclockwise, -1 = clockwise) + */ +bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir) +{ + if (empty()) return false; + + Geom::Point rc; + + // rotate around the mouseovered point, or the selection's rotation center + // if nothing is mouseovered + double radius; + SelectableControlPoint *scp = + dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point); + if (scp) { + rc = scp->position(); + if (!_mouseover_rot_radius) { + _mouseover_rot_radius = _rotationRadius(rc); + } + radius = *_mouseover_rot_radius; + } else { + rc = _handles->rotationCenter(); + if (!_rot_radius) { + _rot_radius = _rotationRadius(rc); + } + radius = *_rot_radius; + } + + double angle; + if (held_alt(event)) { + // Rotate by "one pixel". We interpret this as rotating by an angle that causes + // the topmost point of a circle circumscribed about the selection's bounding box + // to move on an arc 1 screen pixel long. + angle = atan2(1.0 / _desktop->current_zoom(), radius) * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + angle = M_PI * dir / snaps; + } + + // translate to origin, rotate, translate back to original position + Geom::Matrix m = Geom::Translate(-rc) + * Geom::Rotate(angle) * Geom::Translate(rc); + transform(m); + signal_commit.emit(COMMIT_KEYBOARD_ROTATE); + return true; +} + + +bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir) +{ + if (empty()) return false; + + double maxext = bounds()->maxExtent(); + if (Geom::are_near(maxext, 0)) return false; + + Geom::Point center; + SelectableControlPoint *scp = + dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point); + if (scp) { + center = scp->position(); + } else { + center = _handles->rotationCenter().position(); + } + + double length_change; + if (held_alt(event)) { + // Scale by "one pixel". It means shrink/grow 1px for the larger dimension + // of the bounding box. + length_change = 1.0 / _desktop->current_zoom() * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000); + length_change *= dir; + } + double scale = (maxext + length_change) / maxext; + + Geom::Matrix m = Geom::Translate(-center) * Geom::Scale(scale) * Geom::Translate(center); + transform(m); + signal_commit.emit(COMMIT_KEYBOARD_SCALE_UNIFORM); + return true; +} + +bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d) +{ + if (empty()) return false; + + Geom::Scale scale_transform(1, 1); + if (d == Geom::X) { + scale_transform = Geom::Scale(-1, 1); + } else { + scale_transform = Geom::Scale(1, -1); + } + + SelectableControlPoint *scp = + dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point); + Geom::Point center = scp ? scp->position() : _handles->rotationCenter().position(); + + Geom::Matrix m = Geom::Translate(-center) * scale_transform * Geom::Translate(center); + transform(m); + signal_commit.emit(d == Geom::X ? COMMIT_FLIP_X : COMMIT_FLIP_Y); + return true; +} + +void ControlPointSelection::_commitHandlesTransform(CommitEvent ce) +{ + _updateBounds(); + _updateTransformHandles(true); + signal_commit.emit(ce); +} + +bool ControlPointSelection::event(GdkEvent *event) +{ + // implement generic event handling that should apply for all control point selections here; + // for example, keyboard moves and transformations. This way this functionality doesn't need + // to be duplicated in many places + // Later split out so that it can be reused in object selection + + switch (event->type) { + case GDK_KEY_PRESS: + // do not handle key events if the selection is empty + if (empty()) break; + + switch(shortcut_key(event->key)) { + // moves + case GDK_Up: + case GDK_KP_Up: + case GDK_KP_8: + return _keyboardMove(event->key, Geom::Point(0, 1)); + case GDK_Down: + case GDK_KP_Down: + case GDK_KP_2: + return _keyboardMove(event->key, Geom::Point(0, -1)); + case GDK_Right: + case GDK_KP_Right: + case GDK_KP_6: + return _keyboardMove(event->key, Geom::Point(1, 0)); + case GDK_Left: + case GDK_KP_Left: + case GDK_KP_4: + return _keyboardMove(event->key, Geom::Point(-1, 0)); + + // rotates + case GDK_bracketleft: + return _keyboardRotate(event->key, 1); + case GDK_bracketright: + return _keyboardRotate(event->key, -1); + + // scaling + case GDK_less: + case GDK_comma: + return _keyboardScale(event->key, -1); + case GDK_greater: + case GDK_period: + return _keyboardScale(event->key, 1); + + // TODO: skewing + + // flipping + // NOTE: H is horizontal flip, while Shift+H switches transform handle mode! + case GDK_h: + case GDK_H: + if (held_shift(event->key)) { + toggleTransformHandlesMode(); + return true; + } + // any modifiers except shift should cause no action + if (held_any_modifiers(event->key)) break; + return _keyboardFlip(Geom::X); + case GDK_v: + case GDK_V: + if (held_any_modifiers(event->key)) break; + return _keyboardFlip(Geom::Y); + default: break; + } + break; + default: break; + } + return false; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/control-point-selection.h b/src/ui/tool/control-point-selection.h new file mode 100644 index 000000000..514ecb2e3 --- /dev/null +++ b/src/ui/tool/control-point-selection.h @@ -0,0 +1,161 @@ +/** @file + * Node selection - stores a set of nodes and applies transformations + * to them + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_NODE_SELECTION_H +#define SEEN_UI_TOOL_NODE_SELECTION_H + +#include <memory> +#include <boost/optional.hpp> +#include <sigc++/sigc++.h> +#include <2geom/forward.h> +#include <2geom/point.h> +#include <2geom/rect.h> +#include "display/display-forward.h" +#include "util/accumulators.h" +#include "util/unordered-containers.h" +#include "ui/tool/commit-events.h" +#include "ui/tool/manipulator.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +class TransformHandleSet; +class SelectableControlPoint; +} +} + +namespace Inkscape { +namespace UI { + +class ControlPointSelection : public Manipulator, public sigc::trackable { +public: + ControlPointSelection(SPDesktop *d, SPCanvasGroup *th_group); + ~ControlPointSelection(); + typedef INK_UNORDERED_SET<SelectableControlPoint *> set_type; + typedef set_type Set; // convenience alias + + typedef set_type::iterator iterator; + typedef set_type::const_iterator const_iterator; + typedef set_type::size_type size_type; + typedef SelectableControlPoint *value_type; + typedef SelectableControlPoint *key_type; + + // size + bool empty() { return _points.empty(); } + size_type size() { return _points.size(); } + + // iterators + iterator begin() { return _points.begin(); } + const_iterator begin() const { return _points.begin(); } + iterator end() { return _points.end(); } + const_iterator end() const { return _points.end(); } + + // insert + std::pair<iterator, bool> insert(const value_type& x); + template <class InputIterator> + void insert(InputIterator first, InputIterator last) { + for (; first != last; ++first) { + insert(*first); + } + } + + // erase + void clear(); + void erase(iterator pos); + size_type erase(const key_type& k); + void erase(iterator first, iterator last); + + // find + iterator find(const key_type &k) { + return _points.find(k); + } + + // Sometimes it is very useful to keep a list of all selectable points. + set_type const &allPoints() const { return _all_points; } + set_type &allPoints() { return _all_points; } + // ...for example in these methods. Another useful case is snapping. + void selectAll(); + void selectArea(Geom::Rect const &); + void invertSelection(); + void spatialGrow(SelectableControlPoint *origin, int dir); + + virtual bool event(GdkEvent *); + + void transform(Geom::Matrix const &m); + void align(Geom::Dim2 d); + void distribute(Geom::Dim2 d); + + Geom::OptRect pointwiseBounds(); + Geom::OptRect bounds(); + + bool transformHandlesEnabled() { return _handles_visible; } + void showTransformHandles(bool v, bool one_node); + // the two methods below do not modify the state; they are for use in manipulators + // that need to temporarily hide the handles, for example when moving a node + void hideTransformHandles(); + void restoreTransformHandles(); + void toggleTransformHandlesMode(); + + sigc::signal<void> signal_update; + sigc::signal<void, SelectableControlPoint *, bool> signal_point_changed; + sigc::signal<void, CommitEvent> signal_commit; +private: + // The functions below are invoked from SelectableControlPoint. + // Previously they were connected to handlers when selecting, but this + // creates problems when dragging a point that was not selected. + void _pointGrabbed(SelectableControlPoint *); + void _pointDragged(Geom::Point &, GdkEventMotion *); + void _pointUngrabbed(); + bool _pointClicked(SelectableControlPoint *, GdkEventButton *); + void _pointChanged(SelectableControlPoint *, bool); + void _mouseoverChanged(); + + void _updateTransformHandles(bool preserve_center); + void _updateBounds(); + bool _keyboardMove(GdkEventKey const &, Geom::Point const &); + bool _keyboardRotate(GdkEventKey const &, int); + bool _keyboardScale(GdkEventKey const &, int); + bool _keyboardFlip(Geom::Dim2); + void _keyboardTransform(Geom::Matrix const &); + void _commitHandlesTransform(CommitEvent ce); + double _rotationRadius(Geom::Point const &); + + set_type _points; + set_type _all_points; + INK_UNORDERED_MAP<SelectableControlPoint *, Geom::Point> _original_positions; + boost::optional<double> _rot_radius; + boost::optional<double> _mouseover_rot_radius; + Geom::OptRect _bounds; + TransformHandleSet *_handles; + SelectableControlPoint *_grabbed_point, *_farthest_point; + unsigned _dragging : 1; + unsigned _handles_visible : 1; + unsigned _one_node_handles : 1; + + friend class SelectableControlPoint; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/control-point.cpp b/src/ui/tool/control-point.cpp new file mode 100644 index 000000000..b74e3bc9c --- /dev/null +++ b/src/ui/tool/control-point.cpp @@ -0,0 +1,581 @@ +/** @file + * Desktop-bound visual control object - implementation + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <iostream> +#include <gdkmm.h> +#include <gtkmm.h> +#include <2geom/point.h> +#include "desktop.h" +#include "desktop-handles.h" +#include "display/snap-indicator.h" +#include "event-context.h" +#include "message-context.h" +#include "preferences.h" +#include "ui/tool/control-point.h" +#include "ui/tool/event-utils.h" + +namespace Inkscape { +namespace UI { + +// class and member documentation goes here... + +/** + * @class ControlPoint + * @brief Draggable point, the workhorse of on-canvas editing. + * + * Control points (formerly known as knots) are graphical representations of some significant + * point in the drawing. The drawing can be changed by dragging the point and the things that are + * attached to it with the mouse. Example things that could be edited with draggable points + * are gradient stops, the place where text is attached to a path, text kerns, nodes and handles + * in a path, and many more. + * + * @par Control point event handlers + * @par + * The control point has several virtual methods which allow you to react to things that + * happen to it. The most important ones are the grabbed, dragged, ungrabbed and moved functions. + * When a drag happens, the order of calls is as follows: + * - <tt>grabbed()</tt> + * - <tt>dragged()</tt> + * - <tt>dragged()</tt> + * - <tt>dragged()</tt> + * - ... + * - <tt>dragged()</tt> + * - <tt>ungrabbed()</tt> + * + * The control point can also respond to clicks and double clicks. On a double click, + * clicked() is called, followed by doubleclicked(). When deriving from SelectableControlPoint, + * you need to manually call the superclass version at the appropriate point in your handler. + * + * @par Which method to override? + * @par + * You might wonder which hook to use when you want to do things when the point is relocated. + * Here are some tips: + * - If the point is used to edit an object, override the move() method. + * - If the point can usually be dragged wherever you like but can optionally be constrained + * to axes or the like, add a handler for <tt>signal_dragged</tt> that modifies its new + * position argument. + * - If the point has additional canvas items tied to it (like handle lines), override + * the setPosition() method. + */ + +/** + * @enum ControlPoint::State + * Enumeration representing the possible states of the control point, used to determine + * its appearance. + * @var ControlPoint::STATE_NORMAL + * Normal state + * @var ControlPoint::STATE_MOUSEOVER + * Mouse is hovering over the control point + * @var ControlPoint::STATE_CLICKED + * First mouse button pressed over the control point + */ + +// Default colors for control points +static ControlPoint::ColorSet default_color_set = { + {0xffffff00, 0x01000000}, // normal fill, stroke + {0xff0000ff, 0x01000000}, // mouseover fill, stroke + {0x0000ffff, 0x01000000} // clicked fill, stroke +}; + +/** Holds the currently mouseovered control point. */ +ControlPoint *ControlPoint::mouseovered_point = 0; + +/** Emitted when the mouseovered point changes. The parameter is the new mouseovered point. + * When a point ceases to be mouseovered, the parameter will be NULL. */ +sigc::signal<void, ControlPoint*> ControlPoint::signal_mouseover_change; + +/** Stores the window point over which the cursor was during the last mouse button press */ +Geom::Point ControlPoint::_drag_event_origin(Geom::infinity(), Geom::infinity()); + +/** Stores the desktop point from which the last drag was initiated */ +Geom::Point ControlPoint::_drag_origin(Geom::infinity(), Geom::infinity()); + +/** Events which should be captured when a handle is being dragged. */ +int const ControlPoint::_grab_event_mask = (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_KEY_PRESS_MASK | + GDK_KEY_RELEASE_MASK); + +bool ControlPoint::_drag_initiated = false; +bool ControlPoint::_event_grab = false; + +/** A color set which you can use to create an invisible control that can still receive events. + * @relates ControlPoint */ +ControlPoint::ColorSet invisible_cset = { + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000} +}; + +/** + * Create a regular control point. + * Derive to have constructors with a reasonable number of parameters. + * + * @param d Desktop for this control + * @param initial_pos Initial position of the control point in desktop coordinates + * @param anchor Where is the control point rendered relative to its desktop coordinates + * @param shape Shape of the control point: square, diamond, circle... + * @param size Pixel size of the visual representation + * @param cset Colors of the point + * @param group The canvas group the point's canvas item should be created in + */ +ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, + Gtk::AnchorType anchor, SPCtrlShapeType shape, + unsigned int size, ColorSet *cset, SPCanvasGroup *group) + : _desktop (d) + , _canvas_item (NULL) + , _cset (cset ? cset : &default_color_set) + , _state (STATE_NORMAL) + , _position (initial_pos) +{ + _canvas_item = sp_canvas_item_new( + group ? group : sp_desktop_controls (_desktop), SP_TYPE_CTRL, + "anchor", (GtkAnchorType) anchor, "size", (gdouble) size, "shape", shape, + "filled", TRUE, "fill_color", _cset->normal.fill, + "stroked", TRUE, "stroke_color", _cset->normal.stroke, + "mode", SP_CTRL_MODE_XOR, NULL); + _commonInit(); +} + +/** + * Create a control point with a pixbuf-based visual representation. + * + * @param d Desktop for this control + * @param initial_pos Initial position of the control point in desktop coordinates + * @param anchor Where is the control point rendered relative to its desktop coordinates + * @param pixbuf Pixbuf to be used as the visual representation + * @param cset Colors of the point + * @param group The canvas group the point's canvas item should be created in + */ +ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, + Gtk::AnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pixbuf, + ColorSet *cset, SPCanvasGroup *group) + : _desktop (d) + , _canvas_item (NULL) + , _cset(cset ? cset : &default_color_set) + , _position (initial_pos) +{ + _canvas_item = sp_canvas_item_new( + group ? group : sp_desktop_controls(_desktop), SP_TYPE_CTRL, + "anchor", (GtkAnchorType) anchor, "size", (gdouble) pixbuf->get_width(), + "shape", SP_CTRL_SHAPE_BITMAP, "pixbuf", pixbuf->gobj(), + "filled", TRUE, "fill_color", _cset->normal.fill, + "stroked", TRUE, "stroke_color", _cset->normal.stroke, + "mode", SP_CTRL_MODE_XOR, NULL); + _commonInit(); +} + +ControlPoint::~ControlPoint() +{ + // avoid storing invalid points in mouseovered_point + if (this == mouseovered_point) { + _clearMouseover(); + } + + g_signal_handler_disconnect(G_OBJECT(_canvas_item), _event_handler_connection); + //sp_canvas_item_hide(_canvas_item); + gtk_object_destroy(_canvas_item); +} + +void ControlPoint::_commonInit() +{ + SP_CTRL(_canvas_item)->moveto(_position); + _event_handler_connection = g_signal_connect(G_OBJECT(_canvas_item), "event", + G_CALLBACK(_event_handler), this); +} + +/** Relocate the control point without side effects. + * Overload this method only if there is an additional graphical representation + * that must be updated (like the lines that connect handles to nodes). If you override it, + * you must also call the superclass implementation of the method. + * @todo Investigate whether this method should be protected */ +void ControlPoint::setPosition(Geom::Point const &pos) +{ + _position = pos; + SP_CTRL(_canvas_item)->moveto(pos); +} + +/** Move the control point to new position with side effects. + * This is called after each drag. Override this method if only some positions make sense + * for a control point (like a point that must always be on a path and can't modify it), + * or when moving a control point changes the positions of other points. */ +void ControlPoint::move(Geom::Point const &pos) +{ + setPosition(pos); +} + +/** Apply an arbitrary affine transformation to a control point. This is used + * by ControlPointSelection, and is important for things like nodes with handles. + * The default implementation simply moves the point according to the transform. */ +void ControlPoint::transform(Geom::Matrix const &m) { + move(position() * m); +} + +bool ControlPoint::visible() const +{ + return sp_canvas_item_is_visible(_canvas_item); +} + +/** Set the visibility of the control point. An invisible point is not drawn on the canvas + * and cannot receive any events. If you want to have an invisible point that can respond + * to events, use <tt>invisible_cset</tt> as its color set. */ +void ControlPoint::setVisible(bool v) +{ + if (v) sp_canvas_item_show(_canvas_item); + else sp_canvas_item_hide(_canvas_item); +} + +Glib::ustring ControlPoint::format_tip(char const *format, ...) +{ + va_list args; + va_start(args, format); + char *dyntip = g_strdup_vprintf(format, args); + va_end(args); + Glib::ustring ret = dyntip; + g_free(dyntip); + return ret; +} + +unsigned int ControlPoint::_size() const +{ + double ret; + g_object_get(_canvas_item, "size", &ret, NULL); + return static_cast<unsigned int>(ret); +} + +SPCtrlShapeType ControlPoint::_shape() const +{ + SPCtrlShapeType ret; + g_object_get(_canvas_item, "shape", &ret, NULL); + return ret; +} + +GtkAnchorType ControlPoint::_anchor() const +{ + GtkAnchorType ret; + g_object_get(_canvas_item, "anchor", &ret, NULL); + return ret; +} + +Glib::RefPtr<Gdk::Pixbuf> ControlPoint::_pixbuf() +{ + GdkPixbuf *ret; + g_object_get(_canvas_item, "pixbuf", &ret, NULL); + return Glib::wrap(ret); +} + +// Same for setters. + +void ControlPoint::_setSize(unsigned int size) +{ + g_object_set(_canvas_item, "size", (gdouble) size, NULL); +} + +void ControlPoint::_setShape(SPCtrlShapeType shape) +{ + g_object_set(_canvas_item, "shape", shape, NULL); +} + +void ControlPoint::_setAnchor(GtkAnchorType anchor) +{ + g_object_set(_canvas_item, "anchor", anchor, NULL); +} + +void ControlPoint::_setPixbuf(Glib::RefPtr<Gdk::Pixbuf> p) +{ + g_object_set(_canvas_item, "pixbuf", Glib::unwrap(p), NULL); +} + +// re-routes events into the virtual function +int ControlPoint::_event_handler(SPCanvasItem */*item*/, GdkEvent *event, ControlPoint *point) +{ + return point->_eventHandler(event) ? TRUE : FALSE; +} + +// main event callback, which emits all other callbacks. +bool ControlPoint::_eventHandler(GdkEvent *event) +{ + // NOTE the static variables below are shared for all points! + // TODO handle clicks and drags from other buttons too + + // offset from the pointer hotspot to the center of the grabbed knot in desktop coords + static Geom::Point pointer_offset; + // number of last doubleclicked button + static unsigned next_release_doubleclick = 0; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + switch(event->type) + { + case GDK_BUTTON_PRESS: + next_release_doubleclick = 0; + if (event->button.button == 1) { + // 1st mouse button click. internally, start dragging, but do not emit signals + // or change position until drag tolerance is exceeded. + _drag_event_origin[Geom::X] = event->button.x; + _drag_event_origin[Geom::Y] = event->button.y; + pointer_offset = _position - _desktop->w2d(_drag_event_origin); + _drag_initiated = false; + // route all events to this handler + sp_canvas_item_grab(_canvas_item, _grab_event_mask, NULL, event->button.time); + _event_grab = true; + _setState(STATE_CLICKED); + return true; + } + return false; + + case GDK_2BUTTON_PRESS: + // store the button number for next release + next_release_doubleclick = event->button.button; + return true; + + case GDK_MOTION_NOTIFY: + combine_motion_events(_desktop->canvas, event->motion, 0); + if (_event_grab && !_desktop->event_context->space_panning) { + _desktop->snapindicator->remove_snaptarget(); + bool transferred = false; + if (!_drag_initiated) { + bool t = fabs(event->motion.x - _drag_event_origin[Geom::X]) <= drag_tolerance && + fabs(event->motion.y - _drag_event_origin[Geom::Y]) <= drag_tolerance; + if (t) return true; + + // if we are here, it means the tolerance was just exceeded. + _drag_origin = _position; + transferred = grabbed(&event->motion); + // _drag_initiated might change during the above virtual call + if (!_drag_initiated) { + // this guarantees smooth redraws while dragging + sp_canvas_force_full_redraw_after_interruptions(_desktop->canvas, 5); + _drag_initiated = true; + } + } + if (!transferred) { + // dragging in progress + Geom::Point new_pos = _desktop->w2d(event_point(event->motion)) + pointer_offset; + + // the new position is passed by reference and can be changed in the handlers. + dragged(new_pos, &event->motion); + move(new_pos); + _updateDragTip(&event->motion); // update dragging tip after moving to new position + + _desktop->scroll_to_point(new_pos); + _desktop->set_coordinate_status(_position); + sp_event_context_snap_delay_handler(_desktop->event_context, NULL, + (gpointer) this, &event->motion, + DelayedSnapEvent::CONTROL_POINT_HANDLER); + } + return true; + } + break; + + case GDK_BUTTON_RELEASE: + if (_event_grab && event->button.button == 1) { + // If we have any pending snap event, then invoke it now! + // (This is needed because we might not have snapped on the latest GDK_MOTION_NOTIFY event + // if the mouse speed was too high. This is inherent to the snap-delay mechanism. + // We must snap at some point in time though, and this is our last chance) + // PS: For other contexts this is handled already in sp_event_context_item_handler or + // sp_event_context_root_handler + if (_desktop->event_context->_delayed_snap_event) { + sp_event_context_snap_watchdog_callback(_desktop->event_context->_delayed_snap_event); + } + + sp_canvas_item_ungrab(_canvas_item, event->button.time); + _setMouseover(this, event->button.state); + _event_grab = false; + + if (_drag_initiated) { + sp_canvas_end_forced_full_redraws(_desktop->canvas); + } + + if (_drag_initiated) { + // it is the end of a drag + _drag_initiated = false; + ungrabbed(&event->button); + return true; + } else { + // it is the end of a click + if (next_release_doubleclick) { + return doubleclicked(&event->button); + } else { + return clicked(&event->button); + } + } + } + break; + + case GDK_ENTER_NOTIFY: + _setMouseover(this, event->crossing.state); + return true; + case GDK_LEAVE_NOTIFY: + _clearMouseover(); + return true; + + case GDK_GRAB_BROKEN: + if (_event_grab && !event->grab_broken.keyboard) { + { + ungrabbed(NULL); + if (_drag_initiated) + sp_canvas_end_forced_full_redraws(_desktop->canvas); + } + _setState(STATE_NORMAL); + _event_grab = false; + _drag_initiated = false; + return true; + } + break; + + // update tips on modifier state change + // TODO add ESC keybinding as drag cancel + case GDK_KEY_PRESS: + case GDK_KEY_RELEASE: + if (mouseovered_point != this) return false; + if (_drag_initiated) { + return true; // this prevents the tool from overwriting the drag tip + } else { + unsigned state = state_after_event(event); + if (state != event->key.state) { + // we need to return true if there was a tip available, otherwise the tool's + // handler will process this event and set the tool's message, overwriting + // the point's message + return _updateTip(state); + } + } + break; + + default: break; + } + + return false; +} + +void ControlPoint::_setMouseover(ControlPoint *p, unsigned state) +{ + bool visible = p->visible(); + if (visible) { // invisible points shouldn't get mouseovered + p->_setState(STATE_MOUSEOVER); + } + p->_updateTip(state); + + if (visible && mouseovered_point != p) { + mouseovered_point = p; + signal_mouseover_change.emit(mouseovered_point); + } +} + +bool ControlPoint::_updateTip(unsigned state) +{ + Glib::ustring tip = _getTip(state); + if (!tip.empty()) { + _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, + tip.data()); + return true; + } else { + _desktop->event_context->defaultMessageContext()->clear(); + return false; + } +} + +bool ControlPoint::_updateDragTip(GdkEventMotion *event) +{ + if (!_hasDragTips()) return false; + Glib::ustring tip = _getDragTip(event); + if (!tip.empty()) { + _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, + tip.data()); + return true; + } else { + _desktop->event_context->defaultMessageContext()->clear(); + return false; + } +} + +void ControlPoint::_clearMouseover() +{ + if (mouseovered_point) { + mouseovered_point->_desktop->event_context->defaultMessageContext()->clear(); + mouseovered_point->_setState(STATE_NORMAL); + mouseovered_point = 0; + signal_mouseover_change.emit(mouseovered_point); + } +} + +/** Transfer the grab to another point. This method allows one to create a draggable point + * that should be dragged instead of the one that received the grabbed signal. + * This is used to implement dragging out handles in the new node tool, for example. + * + * This method will NOT emit the ungrab signal of @c prev_point, because this would complicate + * using it with selectable control points. If you use this method while dragging, you must emit + * the ungrab signal yourself. + * + * Note that this will break horribly if you try to transfer grab between points in different + * desktops, which doesn't make much sense anyway. */ +void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event) +{ + if (!_event_grab) return; + + grabbed(event); + sp_canvas_item_ungrab(prev_point->_canvas_item, event->time); + sp_canvas_item_grab(_canvas_item, _grab_event_mask, NULL, event->time); + + if (!_drag_initiated) { + sp_canvas_force_full_redraw_after_interruptions(_desktop->canvas, 5); + _drag_initiated = true; + } + + prev_point->_setState(STATE_NORMAL); + _setMouseover(this, event->state); +} + +/** + * @brief Change the state of the knot + * Alters the appearance of the knot to match one of the states: normal, mouseover + * or clicked. + */ +void ControlPoint::_setState(State state) +{ + ColorEntry current = {0, 0}; + switch(state) { + case STATE_NORMAL: + current = _cset->normal; break; + case STATE_MOUSEOVER: + current = _cset->mouseover; break; + case STATE_CLICKED: + current = _cset->clicked; break; + }; + _setColors(current); + _state = state; +} +void ControlPoint::_setColors(ColorEntry colors) +{ + g_object_set(_canvas_item, "fill_color", colors.fill, "stroke_color", colors.stroke, NULL); +} + +// dummy implementations for handlers +// they are here to avoid unused param warnings +bool ControlPoint::grabbed(GdkEventMotion *) { return false; } +void ControlPoint::dragged(Geom::Point &, GdkEventMotion *) {} +void ControlPoint::ungrabbed(GdkEventButton *) {} +bool ControlPoint::clicked(GdkEventButton *) { return false; } +bool ControlPoint::doubleclicked(GdkEventButton *) { return false; } + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/control-point.h b/src/ui/tool/control-point.h new file mode 100644 index 000000000..48c70748b --- /dev/null +++ b/src/ui/tool/control-point.h @@ -0,0 +1,202 @@ +/** @file + * Desktop-bound visual control object + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_CONTROL_POINT_H +#define SEEN_UI_TOOL_CONTROL_POINT_H + +#include <boost/utility.hpp> +#include <sigc++/sigc++.h> +#include <gdkmm.h> +#include <gtkmm.h> +#include <2geom/point.h> + +#include "display/display-forward.h" +#include "forward.h" +#include "util/accumulators.h" +#include "display/sodipodi-ctrl.h" + +namespace Inkscape { +namespace UI { + +// most of the documentation is in the .cpp file + +class ControlPoint : boost::noncopyable, public sigc::trackable { +public: + typedef Inkscape::Util::ReverseInterruptible RInt; + typedef Inkscape::Util::Interruptible Int; + // these have to be public, because GCC doesn't allow protected types in constructors, + // even if the constructors are protected themselves. + struct ColorEntry { + guint32 fill; + guint32 stroke; + }; + struct ColorSet { + ColorEntry normal; + ColorEntry mouseover; + ColorEntry clicked; + }; + enum State { + STATE_NORMAL, + STATE_MOUSEOVER, + STATE_CLICKED + }; + + virtual ~ControlPoint(); + + /// @name Adjust the position of the control point + /// @{ + /** Current position of the control point. */ + Geom::Point const &position() const { return _position; } + operator Geom::Point const &() { return _position; } + virtual void move(Geom::Point const &pos); + virtual void setPosition(Geom::Point const &pos); + virtual void transform(Geom::Matrix const &m); + /// @} + + /// @name Toggle the point's visibility + /// @{ + bool visible() const; + virtual void setVisible(bool v); + /// @} + + /// @name Transfer grab from another event handler + /// @{ + void transferGrab(ControlPoint *from, GdkEventMotion *event); + /// @} + + /// @name Receive notifications about control point events + /// @{ + /*sigc::signal<void, Geom::Point const &, Geom::Point &, GdkEventMotion*> signal_dragged; + sigc::signal<bool, GdkEventButton*>::accumulated<RInt> signal_clicked; + sigc::signal<bool, GdkEventButton*>::accumulated<RInt> signal_doubleclicked; + sigc::signal<bool, GdkEventMotion*>::accumulated<Int> signal_grabbed; + sigc::signal<void, GdkEventButton*> signal_ungrabbed;*/ + /// @} + + /// @name Inspect the state of the control point + /// @{ + State state() { return _state; } + bool mouseovered() { return this == mouseovered_point; } + /// @} + + static ControlPoint *mouseovered_point; + static sigc::signal<void, ControlPoint*> signal_mouseover_change; + static Glib::ustring format_tip(char const *format, ...) G_GNUC_PRINTF(1,2); + + // temporarily public, until snap delay is refactored a little + virtual bool _eventHandler(GdkEvent *event); + +protected: + ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, Gtk::AnchorType anchor, + SPCtrlShapeType shape, unsigned int size, ColorSet *cset = 0, SPCanvasGroup *group = 0); + ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, Gtk::AnchorType anchor, + Glib::RefPtr<Gdk::Pixbuf> pixbuf, ColorSet *cset = 0, SPCanvasGroup *group = 0); + + /// @name Handle control point events in subclasses + /// @{ + /** + * Called when the user moves the point beyond the drag tolerance with the first button held + * down. Return true if you called transferGrab() during this method. + * @param event Motion event when drag tolerance was exceeded */ + virtual bool grabbed(GdkEventMotion *event); + /** + * Called while dragging, but before moving the knot to new position. + * @param pos Old position, always equal to position() + * @param new_pos New position (after drag). This is passed as a non-const reference, + * so you can change it from the handler - that's how constrained dragging is implemented. + * @param event Motion event */ + virtual void dragged(Geom::Point &new_pos, GdkEventMotion *event); + /** + * @var ControlPoint::signal_ungrabbed + * Emitted when the control point finishes a drag. + * @param event Button release event + */ + virtual void ungrabbed(GdkEventButton *event); + /** + * Called when the control point is clicked, at mouse button release. Your override should + * return true if the click had some effect. If it did nothing, return false. Improperly + * implementing this method can cause the default context menu not to appear when a control + * point is right-clicked. + * @param event Button release event */ + virtual bool clicked(GdkEventButton *event); + /** + * Called when the control point is doubleclicked, at mouse button release. + * @param event Button release event */ + virtual bool doubleclicked(GdkEventButton *); + /// @} + + /// @name Manipulate the control point's appearance in subclasses + /// @{ + virtual void _setState(State state); + void _setColors(ColorEntry c); + + unsigned int _size() const; + SPCtrlShapeType _shape() const; + GtkAnchorType _anchor() const; + Glib::RefPtr<Gdk::Pixbuf> _pixbuf(); + + void _setSize(unsigned int size); + void _setShape(SPCtrlShapeType shape); + void _setAnchor(GtkAnchorType anchor); + void _setPixbuf(Glib::RefPtr<Gdk::Pixbuf>); + /// @} + + virtual Glib::ustring _getTip(unsigned /*state*/) { return ""; } + virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) { return ""; } + virtual bool _hasDragTips() { return false; } + + SPDesktop *const _desktop; ///< The desktop this control point resides on. + SPCanvasItem * _canvas_item; ///< Visual representation of the control point. + ColorSet *_cset; ///< Colors used to represent the point + State _state; + + static int const _grab_event_mask; + static Geom::Point const &_last_click_event_point() { return _drag_event_origin; } + static Geom::Point const &_last_drag_origin() { return _drag_origin; } + +private: + ControlPoint(ControlPoint const &other); + void operator=(ControlPoint const &other); + + static int _event_handler(SPCanvasItem *item, GdkEvent *event, ControlPoint *point); + static void _setMouseover(ControlPoint *, unsigned state); + static void _clearMouseover(); + bool _updateTip(unsigned state); + bool _updateDragTip(GdkEventMotion *event); + void _setDefaultColors(); + void _commonInit(); + + Geom::Point _position; ///< Current position in desktop coordinates + gulong _event_handler_connection; + + static Geom::Point _drag_event_origin; + static Geom::Point _drag_origin; + static bool _event_grab; + static bool _drag_initiated; +}; + +extern ControlPoint::ColorSet invisible_cset; + + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/curve-drag-point.cpp b/src/ui/tool/curve-drag-point.cpp new file mode 100644 index 000000000..e761daf20 --- /dev/null +++ b/src/ui/tool/curve-drag-point.cpp @@ -0,0 +1,196 @@ +/** @file + * Control point that is dragged during path drag + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <glib/gi18n.h> +#include <2geom/bezier-curve.h> +#include "desktop.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/curve-drag-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tool/node.h" + +namespace Inkscape { +namespace UI { + +/** + * @class CurveDragPoint + * An invisible point used to drag curves. This point is used by PathManipulator to allow editing + * of path segments by dragging them. It is defined in a separate file so that the node tool + * can check if the mouseovered control point is a curve drag point and update the cursor + * accordingly, without the need to drag in the full PathManipulator header. + */ + +// This point should be invisible to the user - use the invisible_cset from control-point.h +// TODO make some methods from path-manipulator.cpp public so that this point doesn't have +// to be declared as a friend + +bool CurveDragPoint::_drags_stroke = false; + +CurveDragPoint::CurveDragPoint(PathManipulator &pm) + : ControlPoint(pm._multi_path_manipulator._path_data.node_data.desktop, Geom::Point(), + Gtk::ANCHOR_CENTER, SP_CTRL_SHAPE_CIRCLE, 1.0, &invisible_cset, + pm._multi_path_manipulator._path_data.dragpoint_group) + , _pm(pm) +{ + setVisible(false); +} + +bool CurveDragPoint::_eventHandler(GdkEvent *event) +{ + // do not process any events when the manipulator is empty + if (_pm.empty()) { + setVisible(false); + return false; + } + return ControlPoint::_eventHandler(event); +} + +bool CurveDragPoint::grabbed(GdkEventMotion */*event*/) +{ + _pm._selection.hideTransformHandles(); + NodeList::iterator second = first.next(); + + // move the handles to 1/3 the length of the segment for line segments + if (first->front()->isDegenerate() && second->back()->isDegenerate()) { + + // delta is a vector equal 1/3 of distance from first to second + Geom::Point delta = (second->position() - first->position()) / 3.0; + first->front()->move(first->front()->position() + delta); + second->back()->move(second->back()->position() - delta); + + _pm.update(); + } + return false; +} + +void CurveDragPoint::dragged(Geom::Point &new_pos, GdkEventMotion *) +{ + NodeList::iterator second = first.next(); + // Magic Bezier Drag Equations follow! + // "weight" describes how the influence of the drag should be distributed + // among the handles; 0 = front handle only, 1 = back handle only. + double weight, t = _t; + if (t <= 1.0 / 6.0) weight = 0; + else if (t <= 0.5) weight = (pow((6 * t - 1) / 2.0, 3)) / 2; + else if (t <= 5.0 / 6.0) weight = (1 - pow((6 * (1-t) - 1) / 2.0, 3)) / 2 + 0.5; + else weight = 1; + + Geom::Point delta = new_pos - position(); + Geom::Point offset0 = ((1-weight)/(3*t*(1-t)*(1-t))) * delta; + Geom::Point offset1 = (weight/(3*t*t*(1-t))) * delta; + + first->front()->move(first->front()->position() + offset0); + second->back()->move(second->back()->position() + offset1); + + _pm.update(); +} + +void CurveDragPoint::ungrabbed(GdkEventButton *) +{ + _pm._updateDragPoint(_desktop->d2w(position())); + _pm._commit(_("Drag curve")); + _pm._selection.restoreTransformHandles(); +} + +bool CurveDragPoint::clicked(GdkEventButton *event) +{ + // This check is probably redundant + if (!first || event->button != 1) return false; + // the next iterator can be invalid if we click very near the end of path + NodeList::iterator second = first.next(); + if (!second) return false; + + // insert nodes on Ctrl+Alt+click + if (held_control(*event) && held_alt(*event)) { + _insertNode(false); + return true; + } + + if (held_shift(*event)) { + // if both nodes of the segment are selected, deselect; + // otherwise add to selection + if (first->selected() && second->selected()) { + _pm._selection.erase(first.ptr()); + _pm._selection.erase(second.ptr()); + } else { + _pm._selection.insert(first.ptr()); + _pm._selection.insert(second.ptr()); + } + } else { + // without Shift, take selection + _pm._selection.clear(); + _pm._selection.insert(first.ptr()); + _pm._selection.insert(second.ptr()); + } + return true; +} + +bool CurveDragPoint::doubleclicked(GdkEventButton *event) +{ + if (event->button != 1 || !first || !first.next()) return false; + _insertNode(true); + return true; +} + +void CurveDragPoint::_insertNode(bool take_selection) +{ + // The purpose of this call is to make way for the just created node. + // Otherwise clicks on the new node would only work after the user moves the mouse a bit. + // PathManipulator will restore visibility when necessary. + setVisible(false); + NodeList::iterator inserted = _pm.subdivideSegment(first, _t); + if (take_selection) { + _pm._selection.clear(); + } + _pm._selection.insert(inserted.ptr()); + + _pm.update(); + _pm._commit(_("Add node")); +} + +Glib::ustring CurveDragPoint::_getTip(unsigned state) +{ + if (_pm.empty()) return ""; + if (!first || !first.next()) return ""; + bool linear = first->front()->isDegenerate() && first.next()->back()->isDegenerate(); + if (state_held_shift(state)) { + return C_("Path segment tip", + "<b>Shift:</b> click to toggle segment selection"); + } + if (state_held_control(state) && state_held_alt(state)) { + return C_("Path segment tip", + "<b>Ctrl+Alt:</b> click to insert a node"); + } + if (linear) { + return C_("Path segment tip", + "<b>Linear segment:</b> drag to convert to a Bezier segment, " + "doubleclick to insert node, click to select (more: Shift, Ctrl+Alt)"); + } else { + return C_("Path segment tip", + "<b>Bezier segment:</b> drag to shape the segment, doubleclick to insert node, " + "click to select (more: Shift, Ctrl+Alt)"); + } +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/curve-drag-point.h b/src/ui/tool/curve-drag-point.h new file mode 100644 index 000000000..288ae6a8e --- /dev/null +++ b/src/ui/tool/curve-drag-point.h @@ -0,0 +1,62 @@ +/** @file + * Control point that is dragged during path drag + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_CURVE_DRAG_POINT_H +#define SEEN_UI_TOOL_CURVE_DRAG_POINT_H + +#include "ui/tool/control-point.h" +#include "ui/tool/node.h" + +class SPDesktop; +namespace Inkscape { +namespace UI { + +class PathManipulator; +struct PathSharedData; + +class CurveDragPoint : public ControlPoint { +public: + CurveDragPoint(PathManipulator &pm); + void setSize(double sz) { _setSize(sz); } + void setTimeValue(double t) { _t = t; } + void setIterator(NodeList::iterator i) { first = i; } + virtual bool _eventHandler(GdkEvent *event); +protected: + virtual Glib::ustring _getTip(unsigned state); + virtual void dragged(Geom::Point &, GdkEventMotion *); + virtual bool grabbed(GdkEventMotion *); + virtual void ungrabbed(GdkEventButton *); + virtual bool clicked(GdkEventButton *); + virtual bool doubleclicked(GdkEventButton *); + +private: + void _insertNode(bool take_selection); + double _t; + PathManipulator &_pm; + NodeList::iterator first; + static bool _drags_stroke; + static Geom::Point _stroke_drag_origin; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/event-utils.cpp b/src/ui/tool/event-utils.cpp new file mode 100644 index 000000000..91b2cdb04 --- /dev/null +++ b/src/ui/tool/event-utils.cpp @@ -0,0 +1,156 @@ +/** @file + * Collection of shorthands to deal with GDK events. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <cstring> +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> +#include "display/sp-canvas.h" +#include "ui/tool/event-utils.h" + +namespace Inkscape { +namespace UI { + + +guint shortcut_key(GdkEventKey const &event) +{ + guint shortcut_key = 0; + gdk_keymap_translate_keyboard_state( + gdk_keymap_get_for_display(gdk_display_get_default()), + event.hardware_keycode, + (GdkModifierType) event.state, + 0 /*event->key.group*/, + &shortcut_key, NULL, NULL, NULL); + return shortcut_key; +} + +unsigned combine_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; +} + +unsigned combine_motion_events(SPCanvas *canvas, GdkEventMotion &event, gint mask) +{ + GdkEvent *event_next; + gint i = 0; + event.x -= canvas->x0; + event.y -= canvas->y0; + + event_next = gdk_event_get(); + // while the next event is also a motion notify + while (event_next && event_next->type == GDK_MOTION_NOTIFY + && (!mask || event_next->motion.state & mask)) + { + if (event_next->motion.device == event.device) { + GdkEventMotion &next = event_next->motion; + event.send_event = next.send_event; + event.time = next.time; + event.x = next.x; + event.y = next.y; + event.state = next.state; + event.is_hint = next.is_hint; + event.x_root = next.x_root; + event.y_root = next.y_root; + if (event.axes && next.axes) { + memcpy(event.axes, next.axes, event.device->num_axes); + } + } + + // kill it + gdk_event_free(event_next); + event_next = gdk_event_get(); + i++; + } + // otherwise, put it back onto the queue + if (event_next) + gdk_event_put(event_next); + event.x += canvas->x0; + event.y += canvas->y0; + + return i; +} + +/** Returns the modifier state valid after this event. Use this when you process events + * that change the modifier state. Currently handles only Shift, Ctrl, Alt. */ +unsigned state_after_event(GdkEvent *event) +{ + unsigned state = 0; + switch (event->type) { + case GDK_KEY_PRESS: + state = event->key.state; + switch(shortcut_key(event->key)) { + case GDK_Shift_L: + case GDK_Shift_R: + state |= GDK_SHIFT_MASK; + break; + case GDK_Control_L: + case GDK_Control_R: + state |= GDK_CONTROL_MASK; + break; + case GDK_Alt_L: + case GDK_Alt_R: + state |= GDK_MOD1_MASK; + break; + default: break; + } + break; + case GDK_KEY_RELEASE: + state = event->key.state; + switch(shortcut_key(event->key)) { + case GDK_Shift_L: + case GDK_Shift_R: + state &= ~GDK_SHIFT_MASK; + break; + case GDK_Control_L: + case GDK_Control_R: + state &= ~GDK_CONTROL_MASK; + break; + case GDK_Alt_L: + case GDK_Alt_R: + state &= ~GDK_MOD1_MASK; + break; + default: break; + } + break; + default: break; + } + return state; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/event-utils.h b/src/ui/tool/event-utils.h new file mode 100644 index 000000000..784855f56 --- /dev/null +++ b/src/ui/tool/event-utils.h @@ -0,0 +1,132 @@ +/** @file + * Collection of shorthands to deal with GDK events. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_EVENT_UTILS_H +#define SEEN_UI_TOOL_EVENT_UTILS_H + +#include <gdk/gdk.h> +#include <2geom/point.h> + +struct SPCanvas; + +namespace Inkscape { +namespace UI { + +inline bool state_held_shift(unsigned state) { + return state & GDK_SHIFT_MASK; +} +inline bool state_held_control(unsigned state) { + return state & GDK_CONTROL_MASK; +} +inline bool state_held_alt(unsigned state) { + return state & GDK_MOD1_MASK; +} +inline bool state_held_only_shift(unsigned state) { + return (state & GDK_SHIFT_MASK) && !(state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)); +} +inline bool state_held_only_control(unsigned state) { + return (state & GDK_CONTROL_MASK) && !(state & (GDK_SHIFT_MASK | GDK_MOD1_MASK)); +} +inline bool state_held_only_alt(unsigned state) { + return (state & GDK_MOD1_MASK) && !(state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)); +} +inline bool state_held_any_modifiers(unsigned state) { + return state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK); +} +inline bool state_held_no_modifiers(unsigned state) { + return !state_held_any_modifiers(state); +} +template <unsigned button> +inline bool state_held_button(unsigned state) { + return (button == 0 || button > 5) ? false : state & (GDK_BUTTON1_MASK << (button-1)); +} + + +/** Checks whether Shift was held when the event was generated. */ +template <typename E> +inline bool held_shift(E const &event) { + return state_held_shift(event.state); +} + +/** Checks whether Control was held when the event was generated. */ +template <typename E> +inline bool held_control(E const &event) { + return state_held_control(event.state); +} + +/** Checks whether Alt was held when the event was generated. */ +template <typename E> +inline bool held_alt(E const &event) { + return state_held_alt(event.state); +} + +/** True if from the set of Ctrl, Shift and Alt only Ctrl was held when the event + * was generated. */ +template <typename E> +inline bool held_only_control(E const &event) { + return state_held_only_control(event.state); +} + +/** True if from the set of Ctrl, Shift and Alt only Shift was held when the event + * was generated. */ +template <typename E> +inline bool held_only_shift(E const &event) { + return state_held_only_shift(event.state); +} + +/** True if from the set of Ctrl, Shift and Alt only Alt was held when the event + * was generated. */ +template <typename E> +inline bool held_only_alt(E const &event) { + return state_held_only_alt(event.state); +} + +template <typename E> +inline bool held_no_modifiers(E const &event) { + return state_held_no_modifiers(event.state); +} + +template <typename E> +inline bool held_any_modifiers(E const &event) { + return state_held_any_modifiers(event.state); +} + +template <typename E> +inline Geom::Point event_point(E const &event) { + return Geom::Point(event.x, event.y); +} + +/** Use like this: + * @code if (held_button<2>(event->motion)) { ... @endcode */ +template <unsigned button, typename E> +inline bool held_button(E const &event) { + return state_held_button<button>(event.state); +} + +guint shortcut_key(GdkEventKey const &event); +unsigned combine_key_events(guint keyval, gint mask); +unsigned combine_motion_events(SPCanvas *canvas, GdkEventMotion &event, gint mask); +unsigned state_after_event(GdkEvent *event); + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/manipulator.cpp b/src/ui/tool/manipulator.cpp new file mode 100644 index 000000000..b532fcab4 --- /dev/null +++ b/src/ui/tool/manipulator.cpp @@ -0,0 +1,89 @@ +/** @file + * Manipulator base class and manipulator group - implementation + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include "ui/tool/manipulator.h" +#include "ui/tool/node.h" + +namespace Inkscape { +namespace UI { + +/* +void Manipulator::_grabEvents() +{ + if (_group) _group->_grabEvents(boost::shared_ptr<Manipulator>(this)); +} +void Manipulator::_ungrabEvents() +{ + if (_group) _group->_ungrabEvents(boost::shared_ptr<Manipulator>(this)); +} + +ManipulatorGroup::ManipulatorGroup(SPDesktop *d) : + _desktop(d) +{ +} +ManipulatorGroup::~ManipulatorGroup() +{ +} + +void ManipulatorGroup::_grabEvents(boost::shared_ptr<Manipulator> m) +{ + if (!_grab) _grab = m; +} +void ManipulatorGroup::_ungrabEvents(boost::shared_ptr<Manipulator> m) +{ + if (_grab == m) _grab.reset(); +} + +void ManipulatorGroup::add(boost::shared_ptr<Manipulator> m) +{ + m->_group = this; + push_back(m); +} +void ManipulatorGroup::remove(boost::shared_ptr<Manipulator> m) +{ + for (std::list<boost::shared_ptr<Manipulator> >::iterator i = begin(); i != end(); ++i) { + if ((*i) == m) { + erase(i); + break; + } + } + m->_group = 0; +} + +void ManipulatorGroup::clear() +{ + std::list<boost::shared_ptr<Manipulator> >::clear(); +} + +bool ManipulatorGroup::event(GdkEvent *event) +{ + if (_grab) { + return _grab->event(event); + } + + for (std::list<boost::shared_ptr<Manipulator> >::iterator i = begin(); i != end(); ++i) { + if ((*i)->event(event) || _grab) return true; + } + return false; +}*/ + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/manipulator.h b/src/ui/tool/manipulator.h new file mode 100644 index 000000000..799dad0d3 --- /dev/null +++ b/src/ui/tool/manipulator.h @@ -0,0 +1,165 @@ +/** @file + * Manipulator - edits something on-canvas + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_MANIPULATOR_H +#define SEEN_UI_TOOL_MANIPULATOR_H + +#include <set> +#include <map> +#include <sigc++/sigc++.h> +#include <glib.h> +#include <gdk/gdk.h> +#include <boost/shared_ptr.hpp> + +class SPDesktop; +namespace Inkscape { +namespace UI { + +class ManipulatorGroup; +class ControlPointSelection; + +/** + * @brief Tool component that processes events and does something in response to them. + * Note: this class is probably redundant. + */ +class Manipulator { +friend class ManipulatorGroup; +public: + Manipulator(SPDesktop *d) + : _desktop(d) + {} + virtual ~Manipulator() {} + + /// Handle input event. Returns true if handled. + virtual bool event(GdkEvent *)=0; +protected: + SPDesktop *const _desktop; +}; + +/** + * @brief Tool component that edits something on the canvas using selectable control points. + * Note: this class is probably redundant. + */ +class PointManipulator : public Manipulator, public sigc::trackable { +public: + PointManipulator(SPDesktop *d, ControlPointSelection &sel) + : Manipulator(d) + , _selection(sel) + {} +protected: + ControlPointSelection &_selection; +}; + +/** Manipulator that aggregates several manipulators of the same type. + * The order of invoking events on the member manipulators is undefined. + * To make this class more useful, derive from it and add actions that can be performed + * on all manipulators in the set. + * + * This is not used at the moment and is probably useless. */ +template <typename T> +class MultiManipulator : public PointManipulator { +public: + //typedef typename T::ItemType ItemType; + typedef typename std::pair<void*, boost::shared_ptr<T> > MapPair; + typedef typename std::map<void*, boost::shared_ptr<T> > MapType; + + MultiManipulator(SPDesktop *d, ControlPointSelection &sel) + : PointManipulator(d, sel) + {} + void addItem(void *item) { + boost::shared_ptr<T> m(_createManipulator(item)); + _mmap.insert(MapPair(item, m)); + } + void removeItem(void *item) { + _mmap.erase(item); + } + void clear() { + _mmap.clear(); + } + bool contains(void *item) { + return _mmap.find(item) != _mmap.end(); + } + bool empty() { + return _mmap.empty(); + } + void setItems(GSList const *list) { + std::set<void*> to_remove; + for (typename MapType::iterator mi = _mmap.begin(); mi != _mmap.end(); ++mi) { + to_remove.insert(mi->first); + } + for (GSList *i = const_cast<GSList*>(list); i; i = i->next) { + if (_isItemType(i->data)) { + // erase returns the number of items removed + // if nothing was removed, it means this item did not have a manipulator - add it + if (!to_remove.erase(i->data)) addItem(i->data); + } + } + typedef typename std::set<void*>::iterator RmIter; + for (RmIter ri = to_remove.begin(); ri != to_remove.end(); ++ri) { + removeItem(*ri); + } + } + + /** Invoke a method on all managed manipulators. + * Example: + * @code m.invokeForAll(&SomeManipulator::someMethod); @endcode + */ + template <typename R> + void invokeForAll(R (T::*method)()) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(); + } + } + template <typename R, typename A> + void invokeForAll(R (T::*method)(A), A a) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a); + } + } + template <typename R, typename A> + void invokeForAll(R (T::*method)(A const &), A const &a) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a); + } + } + template <typename R, typename A, typename B> + void invokeForAll(R (T::*method)(A,B), A a, B b) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a, b); + } + } + + virtual bool event(GdkEvent *event) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + if ((*i).second->event(event)) return true; + } + return false; + } +protected: + virtual T *_createManipulator(void *item) = 0; + virtual bool _isItemType(void *item) = 0; + MapType _mmap; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/modifier-tracker.cpp b/src/ui/tool/modifier-tracker.cpp new file mode 100644 index 000000000..8c6033bc7 --- /dev/null +++ b/src/ui/tool/modifier-tracker.cpp @@ -0,0 +1,93 @@ +/** @file + * Fine-grained modifier tracker for event handling. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> +#include "ui/tool/event-utils.h" +#include "ui/tool/modifier-tracker.h" + +namespace Inkscape { +namespace UI { + +ModifierTracker::ModifierTracker() + : _left_shift(false) + , _right_shift(false) + , _left_ctrl(false) + , _right_ctrl(false) + , _left_alt(false) + , _right_alt(false) +{} + +bool ModifierTracker::event(GdkEvent *event) +{ + switch (event->type) { + case GDK_KEY_PRESS: + switch (shortcut_key(event->key)) { + case GDK_Shift_L: + _left_shift = true; + break; + case GDK_Shift_R: + _right_shift = true; + break; + case GDK_Control_L: + _left_ctrl = true; + break; + case GDK_Control_R: + _right_ctrl = true; + break; + case GDK_Alt_L: + _left_alt = true; + break; + case GDK_Alt_R: + _right_alt = true; + break; + } + break; + case GDK_KEY_RELEASE: + switch (shortcut_key(event->key)) { + case GDK_Shift_L: + _left_shift = false; + break; + case GDK_Shift_R: + _right_shift = false; + break; + case GDK_Control_L: + _left_ctrl = false; + break; + case GDK_Control_R: + _right_ctrl = false; + break; + case GDK_Alt_L: + _left_alt = false; + break; + case GDK_Alt_R: + _right_alt = false; + break; + } + break; + default: break; + } + + return false; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/modifier-tracker.h b/src/ui/tool/modifier-tracker.h new file mode 100644 index 000000000..55538ead6 --- /dev/null +++ b/src/ui/tool/modifier-tracker.h @@ -0,0 +1,54 @@ +/** @file + * Fine-grained modifier tracker for event handling. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_MODIFIER_TRACKER_H +#define SEEN_UI_TOOL_MODIFIER_TRACKER_H + +#include <gdk/gdk.h> + +namespace Inkscape { +namespace UI { + +class ModifierTracker { +public: + ModifierTracker(); + bool event(GdkEvent *); + + bool leftShift() const { return _left_shift; } + bool rightShift() const { return _right_shift; } + bool leftControl() const { return _left_ctrl; } + bool rightControl() const { return _right_ctrl; } + bool leftAlt() const { return _left_alt; } + bool rightAlt() const { return _right_alt; } + +private: + bool _left_shift; + bool _right_shift; + bool _left_ctrl; + bool _right_ctrl; + bool _left_alt; + bool _right_alt; +}; + +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_UI_TOOL_MODIFIER_TRACKER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/multi-path-manipulator.cpp b/src/ui/tool/multi-path-manipulator.cpp new file mode 100644 index 000000000..2025a12d7 --- /dev/null +++ b/src/ui/tool/multi-path-manipulator.cpp @@ -0,0 +1,717 @@ +/** @file + * Multi path manipulator - implementation + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <boost/shared_ptr.hpp> +#include <glib.h> +#include <glibmm/i18n.h> +#include "desktop.h" +#include "desktop-handles.h" +#include "document.h" +#include "live_effects/lpeobject.h" +#include "message-stack.h" +#include "preferences.h" +#include "sp-path.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/node.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "util/unordered-containers.h" + +#ifdef USE_GNU_HASHES +namespace __gnu_cxx { +template<> +struct hash<Inkscape::UI::NodeList::iterator> { + size_t operator()(Inkscape::UI::NodeList::iterator const &n) const { + return reinterpret_cast<size_t>(n.ptr()); + } +}; +} // namespace __gnu_cxx +#endif // USE_GNU_HASHES + +namespace Inkscape { +namespace UI { + +namespace { + +struct hash_nodelist_iterator + : public std::unary_function<NodeList::iterator, std::size_t> +{ + std::size_t operator()(NodeList::iterator i) const { + return INK_HASH<NodeList::iterator::pointer>()(&*i); + } +}; + +typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair; +typedef std::vector<IterPair> IterPairList; +typedef INK_UNORDERED_SET<NodeList::iterator, hash_nodelist_iterator> IterSet; +typedef std::multimap<double, IterPair> DistanceMap; +typedef std::pair<double, IterPair> DistanceMapItem; + +/** Find pairs of selected endnodes suitable for joining. */ +void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs) +{ + IterSet join_iters; + DistanceMap dists; + + // find all endnodes in selection + for (ControlPointSelection::iterator i = sel.begin(); i != sel.end(); ++i) { + Node *node = dynamic_cast<Node*>(*i); + if (!node) continue; + NodeList::iterator iter = NodeList::get_iterator(node); + if (!iter.next() || !iter.prev()) join_iters.insert(iter); + } + + if (join_iters.size() < 2) return; + + // Below we find the closest pairs. The algorithm is O(N^3). + // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs + // with their distances in a multimap (not worth it IMO). + while (join_iters.size() >= 2) { + double closest = DBL_MAX; + IterPair closest_pair; + for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) { + for (IterSet::iterator j = join_iters.begin(); j != i; ++j) { + double dist = Geom::distance(**i, **j); + if (dist < closest) { + closest = dist; + closest_pair = std::make_pair(*i, *j); + } + } + } + pairs.push_back(closest_pair); + join_iters.erase(closest_pair.first); + join_iters.erase(closest_pair.second); + } +} + +/** After this function, first should be at the end of path and second at the beginnning. + * @returns True if the nodes are in the same subpath */ +bool prepare_join(IterPair &join_iters) +{ + if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) { + if (join_iters.first.next()) // if first is begin, swap the iterators + std::swap(join_iters.first, join_iters.second); + return true; + } + + NodeList &sp_first = NodeList::get(join_iters.first); + NodeList &sp_second = NodeList::get(join_iters.second); + if (join_iters.first.next()) { // first is begin + if (join_iters.second.next()) { // second is begin + sp_first.reverse(); + } else { // second is end + std::swap(join_iters.first, join_iters.second); + } + } else { // first is end + if (join_iters.second.next()) { // second is begin + // do nothing + } else { // second is end + sp_second.reverse(); + } + } + return false; +} +} // anonymous namespace + + +MultiPathManipulator::MultiPathManipulator(PathSharedData &data, sigc::connection &chg) + : PointManipulator(data.node_data.desktop, *data.node_data.selection) + , _path_data(data) + , _changed(chg) +{ + _selection.signal_commit.connect( + sigc::mem_fun(*this, &MultiPathManipulator::_commit)); + _selection.signal_point_changed.connect( + sigc::hide( sigc::hide( + signal_coords_changed.make_slot()))); +} + +MultiPathManipulator::~MultiPathManipulator() +{ + _mmap.clear(); +} + +/** Remove empty manipulators. */ +void MultiPathManipulator::cleanup() +{ + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) { + if (i->second->empty()) _mmap.erase(i++); + else ++i; + } +} + +/** @brief Change the set of items to edit. + * + * This method attempts to preserve as much of the state as possible. */ +void MultiPathManipulator::setItems(std::set<ShapeRecord> const &s) +{ + std::set<ShapeRecord> shapes(s); + + // iterate over currently edited items, modifying / removing them as necessary + for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) { + std::set<ShapeRecord>::iterator si = shapes.find(i->first); + if (si == shapes.end()) { + // This item is no longer supposed to be edited - remove its manipulator + _mmap.erase(i++); + } else { + ShapeRecord const &sr = i->first; + ShapeRecord const &sr_new = *si; + // if the shape record differs, replace the key only and modify other values + if (sr.edit_transform != sr_new.edit_transform || + sr.role != sr_new.role) + { + boost::shared_ptr<PathManipulator> hold(i->second); + if (sr.edit_transform != sr_new.edit_transform) + hold->setControlsTransform(sr_new.edit_transform); + if (sr.role != sr_new.role) { + //hold->setOutlineColor(_getOutlineColor(sr_new.role)); + } + _mmap.erase(sr); + _mmap.insert(std::make_pair(sr_new, hold)); + } + shapes.erase(si); // remove the processed record + ++i; + } + } + + // add newly selected items + for (std::set<ShapeRecord>::iterator i = shapes.begin(); i != shapes.end(); ++i) { + ShapeRecord const &r = *i; + if (!SP_IS_PATH(r.item) && !IS_LIVEPATHEFFECT(r.item)) continue; + boost::shared_ptr<PathManipulator> newpm(new PathManipulator(*this, (SPPath*) r.item, + r.edit_transform, _getOutlineColor(r.role), r.lpe_key)); + newpm->showHandles(_show_handles); + // always show outlines for clips and masks + newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL); + newpm->showPathDirection(_show_path_direction); + newpm->setLiveOutline(_live_outline); + newpm->setLiveObjects(_live_objects); + _mmap.insert(std::make_pair(r, newpm)); + } +} + +void MultiPathManipulator::selectSubpaths() +{ + if (_selection.empty()) { + _selection.selectAll(); + } else { + invokeForAll(&PathManipulator::selectSubpaths); + } +} + +void MultiPathManipulator::shiftSelection(int dir) +{ + invokeForAll(&PathManipulator::shiftSelection, dir); +} + +void MultiPathManipulator::invertSelectionInSubpaths() +{ + invokeForAll(&PathManipulator::invertSelectionInSubpaths); +} + +void MultiPathManipulator::setNodeType(NodeType type) +{ + if (_selection.empty()) return; + for (ControlPointSelection::iterator i = _selection.begin(); i != _selection.end(); ++i) { + Node *node = dynamic_cast<Node*>(*i); + if (node) node->setType(type); + } + _done(_("Change node type")); +} + +void MultiPathManipulator::setSegmentType(SegmentType type) +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::setSegmentType, type); + if (type == SEGMENT_STRAIGHT) { + _done(_("Straighten segments")); + } else { + _done(_("Make segments curves")); + } +} + +void MultiPathManipulator::insertNodes() +{ + invokeForAll(&PathManipulator::insertNodes); + _done(_("Add nodes")); +} + +void MultiPathManipulator::joinNodes() +{ + invokeForAll(&PathManipulator::hideDragPoint); + // Node join has two parts. In the first one we join two subpaths by fusing endpoints + // into one. In the second we fuse nodes in each subpath. + IterPairList joins; + NodeList::iterator preserve_pos; + Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point); + if (mouseover_node) { + preserve_pos = NodeList::get_iterator(mouseover_node); + } + find_join_iterators(_selection, joins); + + for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) { + bool same_path = prepare_join(*i); + NodeList &sp_first = NodeList::get(i->first); + NodeList &sp_second = NodeList::get(i->second); + i->first->setType(NODE_CUSP, false); + + Geom::Point joined_pos, pos_handle_front, pos_handle_back; + pos_handle_front = *i->second->front(); + pos_handle_back = *i->first->back(); + + // When we encounter the mouseover node, we unset the iterator - it will be invalidated + if (i->first == preserve_pos) { + joined_pos = *i->first; + preserve_pos = NodeList::iterator(); + } else if (i->second == preserve_pos) { + joined_pos = *i->second; + preserve_pos = NodeList::iterator(); + } else { + joined_pos = Geom::middle_point(*i->first, *i->second); + } + + // if the handles aren't degenerate, don't move them + i->first->move(joined_pos); + Node *joined_node = i->first.ptr(); + if (!i->second->front()->isDegenerate()) { + joined_node->front()->setPosition(pos_handle_front); + } + if (!i->first->back()->isDegenerate()) { + joined_node->back()->setPosition(pos_handle_back); + } + sp_second.erase(i->second); + + if (same_path) { + sp_first.setClosed(true); + } else { + sp_first.splice(sp_first.end(), sp_second); + sp_second.kill(); + } + _selection.insert(i->first.ptr()); + } + + if (joins.empty()) { + // Second part replaces contiguous selections of nodes with single nodes + invokeForAll(&PathManipulator::weldNodes, preserve_pos); + } + + _doneWithCleanup(_("Join nodes")); +} + +void MultiPathManipulator::breakNodes() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::breakNodes); + _done(_("Break nodes")); +} + +void MultiPathManipulator::deleteNodes(bool keep_shape) +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::deleteNodes, keep_shape); + _doneWithCleanup(_("Delete nodes")); +} + +/** Join selected endpoints to create segments. */ +void MultiPathManipulator::joinSegments() +{ + IterPairList joins; + find_join_iterators(_selection, joins); + + for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) { + bool same_path = prepare_join(*i); + NodeList &sp_first = NodeList::get(i->first); + NodeList &sp_second = NodeList::get(i->second); + i->first->setType(NODE_CUSP, false); + i->second->setType(NODE_CUSP, false); + if (same_path) { + sp_first.setClosed(true); + } else { + sp_first.splice(sp_first.end(), sp_second); + sp_second.kill(); + } + } + + if (joins.empty()) { + invokeForAll(&PathManipulator::weldSegments); + } + _doneWithCleanup("Join segments"); +} + +void MultiPathManipulator::deleteSegments() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::deleteSegments); + _doneWithCleanup("Delete segments"); +} + +void MultiPathManipulator::alignNodes(Geom::Dim2 d) +{ + _selection.align(d); + if (d == Geom::X) { + _done("Align nodes to a horizontal line"); + } else { + _done("Align nodes to a vertical line"); + } +} + +void MultiPathManipulator::distributeNodes(Geom::Dim2 d) +{ + _selection.distribute(d); + if (d == Geom::X) { + _done("Distrubute nodes horizontally"); + } else { + _done("Distribute nodes vertically"); + } +} + +void MultiPathManipulator::reverseSubpaths() +{ + if (_selection.empty()) { + invokeForAll(&PathManipulator::reverseSubpaths, false); + _done("Reverse subpaths"); + } else { + invokeForAll(&PathManipulator::reverseSubpaths, true); + _done("Reverse selected subpaths"); + } +} + +void MultiPathManipulator::move(Geom::Point const &delta) +{ + _selection.transform(Geom::Translate(delta)); + _done("Move nodes"); +} + +void MultiPathManipulator::showOutline(bool show) +{ + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + // always show outlines for clipping paths and masks + i->second->showOutline(show || i->first.role != SHAPE_ROLE_NORMAL); + } + _show_outline = show; +} + +void MultiPathManipulator::showHandles(bool show) +{ + invokeForAll(&PathManipulator::showHandles, show); + _show_handles = show; +} + +void MultiPathManipulator::showPathDirection(bool show) +{ + invokeForAll(&PathManipulator::showPathDirection, show); + _show_path_direction = show; +} + +/** @brief Set live outline update status + * When set to true, outline will be updated continuously when dragging + * or transforming nodes. Otherwise it will only update when changes are committed + * to XML. */ +void MultiPathManipulator::setLiveOutline(bool set) +{ + invokeForAll(&PathManipulator::setLiveOutline, set); + _live_outline = set; +} + +/** @brief Set live object update status + * When set to true, objects will be updated continuously when dragging + * or transforming nodes. Otherwise they will only update when changes are committed + * to XML. */ +void MultiPathManipulator::setLiveObjects(bool set) +{ + invokeForAll(&PathManipulator::setLiveObjects, set); + _live_objects = set; +} + +void MultiPathManipulator::updateOutlineColors() +{ + //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + // i->second->setOutlineColor(_getOutlineColor(i->first.role)); + //} +} + +bool MultiPathManipulator::event(GdkEvent *event) +{ + _tracker.event(event); + guint key = 0; + if (event->type == GDK_KEY_PRESS) { + key = shortcut_key(event->key); + } + + // Single handle adjustments go here. + if (_selection.size() == 1 && event->type == GDK_KEY_PRESS) { + do { + Node *n = dynamic_cast<Node *>(*_selection.begin()); + if (!n) break; + + PathManipulator &pm = n->nodeList().subpathList().pm(); + + int which = 0; + if (_tracker.rightAlt() || _tracker.rightControl()) { + which = 1; + } + if (_tracker.leftAlt() || _tracker.leftControl()) { + if (which != 0) break; // ambiguous + which = -1; + } + if (which == 0) break; // no handle chosen + bool one_pixel = _tracker.leftAlt() || _tracker.rightAlt(); + bool handled = true; + + switch (key) { + // single handle functions + // rotation + case GDK_bracketleft: + case GDK_braceleft: + pm.rotateHandle(n, which, 1, one_pixel); + break; + case GDK_bracketright: + case GDK_braceright: + pm.rotateHandle(n, which, -1, one_pixel); + break; + // adjust length + case GDK_period: + case GDK_greater: + pm.scaleHandle(n, which, 1, one_pixel); + break; + case GDK_comma: + case GDK_less: + pm.scaleHandle(n, which, -1, one_pixel); + break; + default: + handled = false; + break; + } + + if (handled) return true; + } while(0); + } + + + switch (event->type) { + case GDK_KEY_PRESS: + switch (key) { + case GDK_Insert: + case GDK_KP_Insert: + // Insert - insert nodes in the middle of selected segments + insertNodes(); + return true; + case GDK_i: + case GDK_I: + if (held_only_shift(event->key)) { + // Shift+I - insert nodes (alternate keybinding for Mac keyboards + // that don't have the Insert key) + insertNodes(); + return true; + } + break; + case GDK_j: + case GDK_J: + if (held_only_shift(event->key)) { + // Shift+J - join nodes + joinNodes(); + return true; + } + if (held_only_alt(event->key)) { + // Alt+J - join segments + joinSegments(); + return true; + } + break; + case GDK_b: + case GDK_B: + if (held_only_shift(event->key)) { + // Shift+B - break nodes + breakNodes(); + return true; + } + break; + case GDK_Delete: + case GDK_KP_Delete: + case GDK_BackSpace: + if (held_shift(event->key)) break; + if (held_alt(event->key)) { + // Alt+Delete - delete segments + deleteSegments(); + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool del_preserves_shape = prefs->getBool("/tools/nodes/delete_preserves_shape", true); + // pass keep_shape = true when: + // a) del preserves shape, and control is not pressed + // b) ctrl+del preserves shape (del_preserves_shape is false), and control is pressed + // Hence xor + deleteNodes(del_preserves_shape ^ held_control(event->key)); + } + return true; + case GDK_c: + case GDK_C: + if (held_only_shift(event->key)) { + // Shift+C - make nodes cusp + setNodeType(NODE_CUSP); + return true; + } + break; + case GDK_s: + case GDK_S: + if (held_only_shift(event->key)) { + // Shift+S - make nodes smooth + setNodeType(NODE_SMOOTH); + return true; + } + break; + case GDK_a: + case GDK_A: + if (held_only_shift(event->key)) { + // Shift+A - make nodes auto-smooth + setNodeType(NODE_AUTO); + return true; + } + break; + case GDK_y: + case GDK_Y: + if (held_only_shift(event->key)) { + // Shift+Y - make nodes symmetric + setNodeType(NODE_SYMMETRIC); + return true; + } + break; + case GDK_r: + case GDK_R: + if (held_only_shift(event->key)) { + // Shift+R - reverse subpaths + reverseSubpaths(); + return true; + } + break; + default: + break; + } + break; + case GDK_MOTION_NOTIFY: + combine_motion_events(_desktop->canvas, event->motion, 0); + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + if (i->second->event(event)) return true; + } + break; + default: break; + } + + return false; +} + +/** Commit changes to XML and add undo stack entry based on the action that was done. Invoked + * by sub-manipulators, for example TransformHandleSet and ControlPointSelection. */ +void MultiPathManipulator::_commit(CommitEvent cps) +{ + gchar const *reason = NULL; + gchar const *key = NULL; + switch(cps) { + case COMMIT_MOUSE_MOVE: + reason = _("Move nodes"); + break; + case COMMIT_KEYBOARD_MOVE_X: + reason = _("Move nodes horizontally"); + key = "node:move:x"; + break; + case COMMIT_KEYBOARD_MOVE_Y: + reason = _("Move nodes vertically"); + key = "node:move:y"; + break; + case COMMIT_MOUSE_ROTATE: + reason = _("Rotate nodes"); + break; + case COMMIT_KEYBOARD_ROTATE: + reason = _("Rotate nodes"); + key = "node:rotate"; + break; + case COMMIT_MOUSE_SCALE_UNIFORM: + reason = _("Scale nodes uniformly"); + break; + case COMMIT_MOUSE_SCALE: + reason = _("Scale nodes"); + break; + case COMMIT_KEYBOARD_SCALE_UNIFORM: + reason = _("Scale nodes uniformly"); + key = "node:scale:uniform"; + break; + case COMMIT_KEYBOARD_SCALE_X: + reason = _("Scale nodes horizontally"); + key = "node:scale:x"; + break; + case COMMIT_KEYBOARD_SCALE_Y: + reason = _("Scale nodes vertically"); + key = "node:scale:y"; + break; + case COMMIT_FLIP_X: + reason = _("Flip nodes horizontally"); + break; + case COMMIT_FLIP_Y: + reason = _("Flip nodes vertically"); + break; + default: return; + } + + _selection.signal_update.emit(); + invokeForAll(&PathManipulator::writeXML); + if (key) { + sp_document_maybe_done(sp_desktop_document(_desktop), key, SP_VERB_CONTEXT_NODE, reason); + } else { + sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason); + } + signal_coords_changed.emit(); +} + +/** Commits changes to XML and adds undo stack entry. */ +void MultiPathManipulator::_done(gchar const *reason) { + invokeForAll(&PathManipulator::update); + invokeForAll(&PathManipulator::writeXML); + sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason); + signal_coords_changed.emit(); +} + +/** Commits changes to XML, adds undo stack entry and removes empty manipulators. */ +void MultiPathManipulator::_doneWithCleanup(gchar const *reason) { + _changed.block(); + _done(reason); + cleanup(); + _changed.unblock(); +} + +/** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */ +guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch(role) { + case SHAPE_ROLE_CLIPPING_PATH: + return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff); + case SHAPE_ROLE_MASK: + return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff); + case SHAPE_ROLE_LPE_PARAM: + return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff); + case SHAPE_ROLE_NORMAL: + default: + return prefs->getColor("/tools/nodes/outline_color", 0xff0000ff); + } +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/multi-path-manipulator.h b/src/ui/tool/multi-path-manipulator.h new file mode 100644 index 000000000..181ae6d1d --- /dev/null +++ b/src/ui/tool/multi-path-manipulator.h @@ -0,0 +1,138 @@ +/** @file + * Multi path manipulator - a tool component that edits multiple paths at once + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_MULTI_PATH_MANIPULATOR_H +#define SEEN_UI_TOOL_MULTI_PATH_MANIPULATOR_H + +#include <sigc++/connection.h> +#include "display/display-forward.h" +#include "forward.h" +#include "ui/tool/commit-events.h" +#include "ui/tool/manipulator.h" +#include "ui/tool/modifier-tracker.h" +#include "ui/tool/node.h" +#include "ui/tool/node-types.h" +#include "ui/tool/shape-record.h" + +struct SPCanvasGroup; + +namespace Inkscape { +namespace UI { + +class PathManipulator; +class MultiPathManipulator; +struct PathSharedData; + +/** + * Manipulator that manages multiple path manipulators active at the same time. + */ +class MultiPathManipulator : public PointManipulator { +public: + MultiPathManipulator(PathSharedData &data, sigc::connection &chg); + virtual ~MultiPathManipulator(); + virtual bool event(GdkEvent *event); + + bool empty() { return _mmap.empty(); } + unsigned size() { return _mmap.empty(); } + void setItems(std::set<ShapeRecord> const &); + void clear() { _mmap.clear(); } + void cleanup(); + + void selectSubpaths(); + void shiftSelection(int dir); + void invertSelectionInSubpaths(); + + void setNodeType(NodeType t); + void setSegmentType(SegmentType t); + + void insertNodes(); + void joinNodes(); + void breakNodes(); + void deleteNodes(bool keep_shape = true); + void joinSegments(); + void deleteSegments(); + void alignNodes(Geom::Dim2 d); + void distributeNodes(Geom::Dim2 d); + void reverseSubpaths(); + void move(Geom::Point const &delta); + + void showOutline(bool show); + void showHandles(bool show); + void showPathDirection(bool show); + void setLiveOutline(bool set); + void setLiveObjects(bool set); + void updateOutlineColors(); + + sigc::signal<void> signal_coords_changed; /// Emitted whenever the coordinates + /// shown in the status bar need updating +private: + typedef std::pair<ShapeRecord, boost::shared_ptr<PathManipulator> > MapPair; + typedef std::map<ShapeRecord, boost::shared_ptr<PathManipulator> > MapType; + + template <typename R> + void invokeForAll(R (PathManipulator::*method)()) { + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(); + } + } + template <typename R, typename A> + void invokeForAll(R (PathManipulator::*method)(A), A a) { + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a); + } + } + template <typename R, typename A> + void invokeForAll(R (PathManipulator::*method)(A const &), A const &a) { + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a); + } + } + template <typename R, typename A, typename B> + void invokeForAll(R (PathManipulator::*method)(A,B), A a, B b) { + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a, b); + } + } + + void _commit(CommitEvent cps); + void _done(gchar const *); + void _doneWithCleanup(gchar const *); + guint32 _getOutlineColor(ShapeRole role); + + MapType _mmap; +public: + PathSharedData const &_path_data; +private: + sigc::connection &_changed; + ModifierTracker _tracker; + bool _show_handles; + bool _show_outline; + bool _show_path_direction; + bool _live_outline; + bool _live_objects; + + friend class PathManipulator; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/node-tool.cpp b/src/ui/tool/node-tool.cpp new file mode 100644 index 000000000..443e7f258 --- /dev/null +++ b/src/ui/tool/node-tool.cpp @@ -0,0 +1,651 @@ +/** @file + * @brief New node tool - implementation + */ +/* Authors: + * Krzysztof Kosiński <tweenk@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <glib.h> +#include <glib/gi18n.h> +#include "desktop.h" +#include "desktop-handles.h" +#include "display/canvas-bpath.h" +#include "display/curve.h" +#include "display/sp-canvas.h" +#include "document.h" +#include "live_effects/lpeobject.h" +#include "message-context.h" +#include "selection.h" +#include "shape-editor.h" // temporary! +#include "sp-clippath.h" +#include "sp-item-group.h" +#include "sp-mask.h" +#include "sp-object-group.h" +#include "sp-path.h" +#include "ui/tool/node-tool.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/curve-drag-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/manipulator.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tool/selector.h" +#include "ui/tool/shape-record.h" + +#include "pixmaps/cursor-node.xpm" +#include "pixmaps/cursor-node-d.xpm" + +/** @struct InkNodeTool + * + * Node tool event context. + * + * @par Architectural overview of the tool + * @par + * Here's a breakdown of what each object does. + * - Handle: shows a handle and keeps the node type constraint (smooth / symmetric) by updating + * the other handle's position when dragged. Its move() method cannot violate the constraints. + * - Node: keeps node type constraints for auto nodes and smooth nodes at ends of linear segments. + * Its move() method cannot violate constraints. Handles linear grow and dispatches spatial grow + * to MultiPathManipulator. Keeps a reference to its NodeList. + * - NodeList: exposes an iterator-based interface to nodes. It is possible to obtain an iterator + * to a node from the node. Keeps a reference to its SubpathList. + * - SubpathList: list of NodeLists that represents an editable pathvector. Keeps a reference + * to its PathManipulator. + * - PathManipulator: performs most of the single-path actions like reverse subpaths, + * delete segment, shift selection, etc. Keeps a reference to MultiPathManipulator. + * - MultiPathManipulator: performs additional operations for actions that are not per-path, + * for example node joins and segment joins. Tracks the control transforms for PMs that edit + * clipping paths and masks. It is more or less equivalent to ShapeEditor and in the future + * it might handle all shapes. Handles XML commit of actions that affect all paths or + * the node selection and removes PathManipulators that have no nodes left after e.g. node + * deletes. + * - ControlPointSelection: keeps track of node selection and a set of nodes that can potentially + * be selected. There can be more than one selection. Performs actions that require no + * knowledge about the path, only about the nodes, like dragging and transforms. It is not + * specific to nodes and can accomodate any control point derived from SelectableControlPoint. + * Transforms nodes in response to transform handle events. + * - TransformHandleSet: displays nodeset transform handles and emits transform events. The aim + * is to eventually use a common class for object and control point transforms. + * - SelectableControlPoint: base for any type of selectable point. It can belong to only one + * selection. + * + * @par Plans for the future + * @par + * - MultiPathManipulator should become a generic shape editor that manages all active manipulator, + * more or less like the old ShapeEditor. + * - Knotholder should be rewritten into one manipulator class per shape, using the control point + * classes. Interesting features like dragging rectangle sides could be added along the way. + * - Better handling of clip and mask editing, particularly in response to undo. + * - High level refactoring of the event context hierarchy. All aspects of tools, like toolbox + * controls, icons, event handling should be collected in one class, though each aspect + * of a tool might be in an separate class for better modularity. The long term goal is to allow + * tools to be defined in extensions or shared library plugins. + */ + +namespace { +SPCanvasGroup *create_control_group(SPDesktop *d); +void ink_node_tool_class_init(InkNodeToolClass *klass); +void ink_node_tool_init(InkNodeTool *node_context); +void ink_node_tool_dispose(GObject *object); + +void ink_node_tool_setup(SPEventContext *ec); +gint ink_node_tool_root_handler(SPEventContext *event_context, GdkEvent *event); +gint ink_node_tool_item_handler(SPEventContext *event_context, SPItem *item, GdkEvent *event); +void ink_node_tool_set(SPEventContext *ec, Inkscape::Preferences::Entry *value); + +void ink_node_tool_update_tip(InkNodeTool *nt, GdkEvent *event); +void ink_node_tool_selection_changed(InkNodeTool *nt, Inkscape::Selection *sel); +void ink_node_tool_select_area(InkNodeTool *nt, Geom::Rect const &, GdkEventButton *); +void ink_node_tool_select_point(InkNodeTool *nt, Geom::Point const &, GdkEventButton *); +void ink_node_tool_mouseover_changed(InkNodeTool *nt, Inkscape::UI::ControlPoint *p); +} // anonymous namespace + +GType ink_node_tool_get_type() +{ + static GType type = 0; + if (!type) { + GTypeInfo info = { + sizeof(InkNodeToolClass), + NULL, NULL, + (GClassInitFunc) ink_node_tool_class_init, + NULL, NULL, + sizeof(InkNodeTool), + 4, + (GInstanceInitFunc) ink_node_tool_init, + NULL, /* value_table */ + }; + type = g_type_register_static(SP_TYPE_EVENT_CONTEXT, "InkNodeTool", &info, (GTypeFlags)0); + } + return type; +} + +namespace { + +SPCanvasGroup *create_control_group(SPDesktop *d) +{ + return reinterpret_cast<SPCanvasGroup*>(sp_canvas_item_new( + sp_desktop_controls(d), SP_TYPE_CANVAS_GROUP, NULL)); +} + +void destroy_group(SPCanvasGroup *g) +{ + gtk_object_destroy(GTK_OBJECT(g)); +} + +void ink_node_tool_class_init(InkNodeToolClass *klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + SPEventContextClass *event_context_class = (SPEventContextClass *) klass; + + object_class->dispose = ink_node_tool_dispose; + + event_context_class->setup = ink_node_tool_setup; + event_context_class->set = ink_node_tool_set; + event_context_class->root_handler = ink_node_tool_root_handler; + event_context_class->item_handler = ink_node_tool_item_handler; +} + +void ink_node_tool_init(InkNodeTool *nt) +{ + SPEventContext *event_context = SP_EVENT_CONTEXT(nt); + + event_context->cursor_shape = cursor_node_xpm; + event_context->hot_x = 1; + event_context->hot_y = 1; + + new (&nt->_selection_changed_connection) sigc::connection(); + new (&nt->_selection_modified_connection) sigc::connection(); + new (&nt->_mouseover_changed_connection) sigc::connection(); + //new (&nt->_mgroup) Inkscape::UI::ManipulatorGroup(nt->desktop); + new (&nt->_selected_nodes) CSelPtr(); + new (&nt->_multipath) MultiPathPtr(); + new (&nt->_selector) SelectorPtr(); + new (&nt->_path_data) PathSharedDataPtr(); +} + +void ink_node_tool_dispose(GObject *object) +{ + InkNodeTool *nt = INK_NODE_TOOL(object); + + nt->enableGrDrag(false); + + nt->_selection_changed_connection.disconnect(); + nt->_selection_modified_connection.disconnect(); + nt->_mouseover_changed_connection.disconnect(); + nt->_multipath.~MultiPathPtr(); + nt->_selected_nodes.~CSelPtr(); + nt->_selector.~SelectorPtr(); + + Inkscape::UI::PathSharedData &data = *nt->_path_data; + destroy_group(data.node_data.node_group); + destroy_group(data.node_data.handle_group); + destroy_group(data.node_data.handle_line_group); + destroy_group(data.outline_group); + destroy_group(data.dragpoint_group); + destroy_group(nt->_transform_handle_group); + + nt->_path_data.~PathSharedDataPtr(); + nt->_selection_changed_connection.~connection(); + nt->_selection_modified_connection.~connection(); + nt->_mouseover_changed_connection.~connection(); + + if (nt->_node_message_context) { + delete nt->_node_message_context; + } + if (nt->shape_editor) { + nt->shape_editor->unset_item(SH_KNOTHOLDER); + delete nt->shape_editor; + } + + G_OBJECT_CLASS(g_type_class_peek(g_type_parent(INK_TYPE_NODE_TOOL)))->dispose(object); +} + +void ink_node_tool_setup(SPEventContext *ec) +{ + InkNodeTool *nt = INK_NODE_TOOL(ec); + + SPEventContextClass *parent = (SPEventContextClass *) g_type_class_peek(g_type_parent(INK_TYPE_NODE_TOOL)); + if (parent->setup) parent->setup(ec); + + nt->_node_message_context = new Inkscape::MessageContext((ec->desktop)->messageStack()); + + nt->_path_data.reset(new Inkscape::UI::PathSharedData()); + Inkscape::UI::PathSharedData &data = *nt->_path_data; + data.node_data.desktop = nt->desktop; + + // selector has to be created here, so that its hidden control point is on the bottom + nt->_selector.reset(new Inkscape::UI::Selector(nt->desktop)); + + // Prepare canvas groups for controls. This guarantees correct z-order, so that + // for example a dragpoint won't obscure a node + data.outline_group = create_control_group(nt->desktop); + data.node_data.handle_line_group = create_control_group(nt->desktop); + data.dragpoint_group = create_control_group(nt->desktop); + nt->_transform_handle_group = create_control_group(nt->desktop); + data.node_data.node_group = create_control_group(nt->desktop); + data.node_data.handle_group = create_control_group(nt->desktop); + + Inkscape::Selection *selection = sp_desktop_selection (ec->desktop); + nt->_selection_changed_connection.disconnect(); + nt->_selection_changed_connection = + selection->connectChanged( + sigc::bind<0>( + sigc::ptr_fun(&ink_node_tool_selection_changed), + nt)); + /*nt->_selection_modified_connection.disconnect(); + nt->_selection_modified_connection = + selection->connectModified( + sigc::hide(sigc::bind<0>( + sigc::ptr_fun(&ink_node_tool_selection_modified), + nt)));*/ + nt->_mouseover_changed_connection.disconnect(); + nt->_mouseover_changed_connection = + Inkscape::UI::ControlPoint::signal_mouseover_change.connect( + sigc::bind<0>( + sigc::ptr_fun(&ink_node_tool_mouseover_changed), + nt)); + + nt->_selected_nodes.reset( + new Inkscape::UI::ControlPointSelection(nt->desktop, nt->_transform_handle_group)); + data.node_data.selection = nt->_selected_nodes.get(); + nt->_multipath.reset(new Inkscape::UI::MultiPathManipulator(data, + nt->_selection_changed_connection)); + + nt->_selector->signal_point.connect( + sigc::bind<0>( + sigc::ptr_fun(&ink_node_tool_select_point), + nt)); + nt->_selector->signal_area.connect( + sigc::bind<0>( + sigc::ptr_fun(&ink_node_tool_select_area), + nt)); + + nt->_multipath->signal_coords_changed.connect( + sigc::bind( + sigc::mem_fun(*nt->desktop, &SPDesktop::emitToolSubselectionChanged), + (void*) 0)); + nt->_selected_nodes->signal_point_changed.connect( + sigc::hide( sigc::hide( + sigc::bind( + sigc::bind( + sigc::ptr_fun(ink_node_tool_update_tip), + (GdkEvent*)0), + nt)))); + + nt->cursor_drag = false; + nt->show_transform_handles = true; + nt->single_node_transform_handles = false; + nt->flash_tempitem = NULL; + nt->flashed_item = NULL; + nt->_last_over = NULL; + // TODO long term, fold ShapeEditor into MultiPathManipulator and rename MPM + // to something better + nt->shape_editor = new ShapeEditor(nt->desktop); + + // read prefs before adding items to selection to prevent momentarily showing the outline + sp_event_context_read(nt, "show_handles"); + sp_event_context_read(nt, "show_outline"); + sp_event_context_read(nt, "live_outline"); + sp_event_context_read(nt, "live_objects"); + sp_event_context_read(nt, "show_path_direction"); + sp_event_context_read(nt, "show_transform_handles"); + sp_event_context_read(nt, "single_node_transform_handles"); + sp_event_context_read(nt, "edit_clipping_paths"); + sp_event_context_read(nt, "edit_masks"); + + ink_node_tool_selection_changed(nt, selection); + ink_node_tool_update_tip(nt, NULL); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/nodes/selcue")) { + ec->enableSelectionCue(); + } + if (prefs->getBool("/tools/nodes/gradientdrag")) { + ec->enableGrDrag(); + } + + nt->desktop->emitToolSubselectionChanged(NULL); // sets the coord entry fields to inactive +} + +void ink_node_tool_set(SPEventContext *ec, Inkscape::Preferences::Entry *value) +{ + InkNodeTool *nt = INK_NODE_TOOL(ec); + Glib::ustring entry_name = value->getEntryName(); + + if (entry_name == "show_handles") { + nt->_multipath->showHandles(value->getBool(true)); + } else if (entry_name == "show_outline") { + nt->show_outline = value->getBool(); + nt->_multipath->showOutline(nt->show_outline); + } else if (entry_name == "live_outline") { + nt->live_outline = value->getBool(); + nt->_multipath->setLiveOutline(nt->live_outline); + } else if (entry_name == "live_objects") { + nt->live_objects = value->getBool(); + nt->_multipath->setLiveObjects(nt->live_objects); + } else if (entry_name == "show_path_direction") { + nt->show_path_direction = value->getBool(); + nt->_multipath->showPathDirection(nt->show_path_direction); + } else if (entry_name == "show_transform_handles") { + nt->show_transform_handles = value->getBool(true); + nt->_selected_nodes->showTransformHandles( + nt->show_transform_handles, nt->single_node_transform_handles); + } else if (entry_name == "single_node_transform_handles") { + nt->single_node_transform_handles = value->getBool(); + nt->_selected_nodes->showTransformHandles( + nt->show_transform_handles, nt->single_node_transform_handles); + } else if (entry_name == "edit_clipping_paths") { + nt->edit_clipping_paths = value->getBool(); + ink_node_tool_selection_changed(nt, nt->desktop->selection); + } else if (entry_name == "edit_masks") { + nt->edit_masks = value->getBool(); + ink_node_tool_selection_changed(nt, nt->desktop->selection); + } else { + SPEventContextClass *parent_class = + (SPEventContextClass *) g_type_class_peek(g_type_parent(INK_TYPE_NODE_TOOL)); + if (parent_class->set) + parent_class->set(ec, value); + } +} + +/** Recursively collect ShapeRecords */ +void gather_items(InkNodeTool *nt, SPItem *base, SPObject *obj, Inkscape::UI::ShapeRole role, + std::set<Inkscape::UI::ShapeRecord> &s) +{ + using namespace Inkscape::UI; + if (!obj) return; + + if (SP_IS_PATH(obj) && obj->repr->attribute("inkscape:original-d") != NULL) { + ShapeRecord r; + r.item = static_cast<SPItem*>(obj); + r.edit_transform = Geom::identity(); // TODO wrong? + r.role = role; + s.insert(r); + } else if (role != SHAPE_ROLE_NORMAL && (SP_IS_GROUP(obj) || SP_IS_OBJECTGROUP(obj))) { + for (SPObject *c = obj->children; c; c = c->next) { + gather_items(nt, base, c, role, s); + } + } else if (SP_IS_ITEM(obj)) { + SPItem *item = static_cast<SPItem*>(obj); + ShapeRecord r; + r.item = item; + // TODO add support for objectBoundingBox + r.edit_transform = base ? sp_item_i2doc_affine(base) : Geom::identity(); + r.role = role; + if (s.insert(r).second) { + // this item was encountered the first time + if (nt->edit_clipping_paths && item->clip_ref) { + gather_items(nt, item, item->clip_ref->getObject(), SHAPE_ROLE_CLIPPING_PATH, s); + } + if (nt->edit_masks && item->mask_ref) { + gather_items(nt, item, item->mask_ref->getObject(), SHAPE_ROLE_MASK, s); + } + } + } +} + +void ink_node_tool_selection_changed(InkNodeTool *nt, Inkscape::Selection *sel) +{ + using namespace Inkscape::UI; + + std::set<ShapeRecord> shapes; + + GSList const *ilist = sel->itemList(); + + for (GSList *i = const_cast<GSList*>(ilist); i; i = i->next) { + SPObject *obj = static_cast<SPObject*>(i->data); + if (SP_IS_ITEM(obj)) { + gather_items(nt, NULL, static_cast<SPItem*>(obj), SHAPE_ROLE_NORMAL, shapes); + } + } + + // ugly hack: set the first editable non-path item for knotholder + // maybe use multiple ShapeEditors for now, to allow editing many shapes at once? + bool something_set = false; + for (std::set<ShapeRecord>::iterator i = shapes.begin(); i != shapes.end(); ++i) { + ShapeRecord const &r = *i; + if (SP_IS_SHAPE(r.item) || + (SP_IS_PATH(r.item) && r.item->repr->attribute("inkscape:original-d") != NULL)) + { + nt->shape_editor->set_item(r.item, SH_KNOTHOLDER); + something_set = true; + break; + } + } + if (!something_set) { + nt->shape_editor->unset_item(SH_KNOTHOLDER); + } + + nt->_multipath->setItems(shapes); + ink_node_tool_update_tip(nt, NULL); + nt->desktop->updateNow(); +} + +gint ink_node_tool_root_handler(SPEventContext *event_context, GdkEvent *event) +{ + /* things to handle here: + * 1. selection of items + * 2. passing events to manipulators + * 3. some keybindings + */ + using namespace Inkscape::UI; // pull in event helpers + + SPDesktop *desktop = event_context->desktop; + Inkscape::Selection *selection = desktop->selection; + InkNodeTool *nt = static_cast<InkNodeTool*>(event_context); + static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (nt->_multipath->event(event)) return true; + if (nt->_selector->event(event)) return true; + if (nt->_selected_nodes->event(event)) return true; + + switch (event->type) + { + case GDK_MOTION_NOTIFY: { + combine_motion_events(desktop->canvas, event->motion, 0); + SPItem *over_item = sp_event_context_find_item (desktop, event_point(event->button), + FALSE, TRUE); + if (over_item != nt->_last_over) { + nt->_last_over = over_item; + ink_node_tool_update_tip(nt, event); + } + + // create pathflash outline + if (prefs->getBool("/tools/nodes/pathflash_enabled")) { + if (over_item == nt->flashed_item) break; + if (!prefs->getBool("/tools/nodes/pathflash_selected") && selection->includes(over_item)) break; + if (nt->flash_tempitem) { + desktop->remove_temporary_canvasitem(nt->flash_tempitem); + nt->flash_tempitem = NULL; + nt->flashed_item = NULL; + } + if (!SP_IS_PATH(over_item)) break; // for now, handle only paths + + nt->flashed_item = over_item; + SPCurve *c = sp_path_get_curve_for_edit(SP_PATH(over_item)); + c->transform(sp_item_i2d_affine(over_item)); + SPCanvasItem *flash = sp_canvas_bpath_new(sp_desktop_tempgroup(desktop), c); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(flash), + prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff), 1.0, + SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(flash), 0, SP_WIND_RULE_NONZERO); + nt->flash_tempitem = desktop->add_temporary_canvasitem(flash, + prefs->getInt("/tools/nodes/pathflash_timeout", 500)); + c->unref(); + } + } break; // do not return true, because we need to pass this event to the parent context + // otherwise some features cease to work + + case GDK_KEY_PRESS: + switch (get_group0_keyval(&event->key)) + { + case GDK_Escape: // deselect everything + if (nt->_selected_nodes->empty()) { + selection->clear(); + } else { + nt->_selected_nodes->clear(); + } + ink_node_tool_update_tip(nt, event); + return TRUE; + case GDK_a: + if (held_control(event->key) && held_alt(event->key)) { + nt->_selected_nodes->selectAll(); + // Ctrl+A is handled in selection-chemistry.cpp via verb + ink_node_tool_update_tip(nt, event); + return TRUE; + } + break; + default: + break; + } + ink_node_tool_update_tip(nt, event); + break; + case GDK_KEY_RELEASE: + ink_node_tool_update_tip(nt, event); + break; + default: break; + } + + SPEventContextClass *parent_class = (SPEventContextClass *) g_type_class_peek(g_type_parent(INK_TYPE_NODE_TOOL)); + if (parent_class->root_handler) + return parent_class->root_handler(event_context, event); + return FALSE; +} + +void ink_node_tool_update_tip(InkNodeTool *nt, GdkEvent *event) +{ + using namespace Inkscape::UI; + if (event && (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)) { + unsigned new_state = state_after_event(event); + if (new_state == event->key.state) return; + if (state_held_shift(new_state)) { + if (nt->_last_over) { + nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, + C_("Node tool tip", "<b>Shift:</b> drag to add nodes to the selection, " + "click to toggle object selection")); + } else { + nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, + C_("Node tool tip", "<b>Shift:</b> drag to add nodes to the selection")); + } + return; + } + } + unsigned sz = nt->_selected_nodes->size(); + unsigned total = nt->_selected_nodes->allPoints().size(); + if (sz != 0) { + if (nt->_last_over) { + char *dyntip = g_strdup_printf(C_("Node tool tip", + "<b>%u of %u nodes</b> selected. " + "Drag to select nodes, click to edit only this object (more: Shift)"), sz, total); + nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, dyntip); + g_free(dyntip); + } else { + char *dyntip = g_strdup_printf(C_("Node tool tip", + "<b>%u of %u nodes</b> selected. " + "Drag to select nodes, click clear the selection"), sz, total); + nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, dyntip); + g_free(dyntip); + } + } else if (!nt->_multipath->empty()) { + if (nt->_last_over) { + nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select nodes, click to edit only this object")); + } else { + nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select nodes, click to clear the selection")); + } + } else { + if (nt->_last_over) { + nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select objects to edit, click to edit this object (more: Shift)")); + } else { + nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select objects to edit")); + } + } +} + +gint ink_node_tool_item_handler(SPEventContext *event_context, SPItem *item, GdkEvent *event) +{ + SPEventContextClass *parent_class = + (SPEventContextClass *) g_type_class_peek(g_type_parent(INK_TYPE_NODE_TOOL)); + if (parent_class->item_handler) + return parent_class->item_handler(event_context, item, event); + return FALSE; +} + +void ink_node_tool_select_area(InkNodeTool *nt, Geom::Rect const &sel, GdkEventButton *event) +{ + using namespace Inkscape::UI; + if (nt->_multipath->empty()) { + // if multipath is empty, select rubberbanded items rather than nodes + Inkscape::Selection *selection = nt->desktop->selection; + GSList *items = sp_document_items_in_box( + sp_desktop_document(nt->desktop), nt->desktop->dkey, sel); + selection->setList(items); + g_slist_free(items); + } else { + if (!held_shift(*event)) nt->_selected_nodes->clear(); + nt->_selected_nodes->selectArea(sel); + } +} +void ink_node_tool_select_point(InkNodeTool *nt, Geom::Point const &/*sel*/, GdkEventButton *event) +{ + using namespace Inkscape::UI; // pull in event helpers + if (!event) return; + if (event->button != 1) return; + + Inkscape::Selection *selection = nt->desktop->selection; + + SPItem *item_clicked = sp_event_context_find_item (nt->desktop, event_point(*event), + (event->state & GDK_MOD1_MASK) && !(event->state & GDK_CONTROL_MASK), TRUE); + + if (item_clicked == NULL) { // nothing under cursor + // if no Shift, deselect + if (!(event->state & GDK_SHIFT_MASK)) { + selection->clear(); + } + } else { + if (held_shift(*event)) { + selection->toggle(item_clicked); + } else { + selection->set(item_clicked); + } + nt->desktop->updateNow(); + } +} + +void ink_node_tool_mouseover_changed(InkNodeTool *nt, Inkscape::UI::ControlPoint *p) +{ + using Inkscape::UI::CurveDragPoint; + CurveDragPoint *cdp = dynamic_cast<CurveDragPoint*>(p); + if (cdp && !nt->cursor_drag) { + nt->cursor_shape = cursor_node_d_xpm; + nt->hot_x = 1; + nt->hot_y = 1; + sp_event_context_update_cursor(nt); + nt->cursor_drag = true; + } else if (!cdp && nt->cursor_drag) { + nt->cursor_shape = cursor_node_xpm; + nt->hot_x = 1; + nt->hot_y = 1; + sp_event_context_update_cursor(nt); + nt->cursor_drag = false; + } +} + +} // anonymous namespace + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/node-tool.h b/src/ui/tool/node-tool.h new file mode 100644 index 000000000..baac642ac --- /dev/null +++ b/src/ui/tool/node-tool.h @@ -0,0 +1,88 @@ +/** @file + * @brief New node tool with support for multiple path editing + */ +/* Authors: + * Krzysztof Kosiński <tweenk@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_NODE_TOOL_H +#define SEEN_UI_TOOL_NODE_TOOL_H + +#include <memory> +#include <glib.h> +#include <sigc++/sigc++.h> +#include "event-context.h" +#include "forward.h" +#include "display/display-forward.h" +#include "ui/tool/node-types.h" + +#define INK_TYPE_NODE_TOOL (ink_node_tool_get_type ()) +#define INK_NODE_TOOL(obj) (GTK_CHECK_CAST ((obj), INK_TYPE_NODE_TOOL, InkNodeTool)) +#define INK_NODE_TOOL_CLASS(klass) (GTK_CHECK_CLASS_CAST ((klass), INK_TYPE_NODE_TOOL, InkNodeToolClass)) +#define INK_IS_NODE_TOOL(obj) (GTK_CHECK_TYPE ((obj), INK_TYPE_NODE_TOOL)) +#define INK_IS_NODE_TOOL_CLASS(klass) (GTK_CHECK_CLASS_TYPE ((klass), INK_TYPE_NODE_TOOL)) + +class InkNodeTool; +class InkNodeToolClass; + +namespace Inkscape { +namespace UI { +class MultiPathManipulator; +class ControlPointSelection; +class Selector; +struct PathSharedData; +} +} + +typedef std::auto_ptr<Inkscape::UI::MultiPathManipulator> MultiPathPtr; +typedef std::auto_ptr<Inkscape::UI::ControlPointSelection> CSelPtr; +typedef std::auto_ptr<Inkscape::UI::Selector> SelectorPtr; +typedef std::auto_ptr<Inkscape::UI::PathSharedData> PathSharedDataPtr; + +struct InkNodeTool : public SPEventContext +{ + sigc::connection _selection_changed_connection; + sigc::connection _mouseover_changed_connection; + sigc::connection _selection_modified_connection; + Inkscape::MessageContext *_node_message_context; + SPItem *flashed_item; + Inkscape::Display::TemporaryItem *flash_tempitem; + CSelPtr _selected_nodes; + MultiPathPtr _multipath; + SelectorPtr _selector; + PathSharedDataPtr _path_data; + SPCanvasGroup *_transform_handle_group; + SPItem *_last_over; + + unsigned cursor_drag : 1; + unsigned show_outline : 1; + unsigned live_outline : 1; + unsigned live_objects : 1; + unsigned show_path_direction : 1; + unsigned show_transform_handles : 1; + unsigned single_node_transform_handles : 1; + unsigned edit_clipping_paths : 1; + unsigned edit_masks : 1; +}; + +struct InkNodeToolClass { + SPEventContextClass parent_class; +}; + +GType ink_node_tool_get_type (void); + +#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:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/node-types.h b/src/ui/tool/node-types.h new file mode 100644 index 000000000..80eaf4fa7 --- /dev/null +++ b/src/ui/tool/node-types.h @@ -0,0 +1,48 @@ +/** @file + * Node types and other small enums. + * This file exists to reduce the number of includes pulled in by toolbox.cpp. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_NODE_TYPES_H +#define SEEN_UI_TOOL_NODE_TYPES_H + +namespace Inkscape { +namespace UI { + +/** Types of nodes supported in the node tool. */ +enum NodeType { + NODE_CUSP, ///< Cusp node - no handle constraints + NODE_SMOOTH, ///< Smooth node - handles must be colinear + NODE_AUTO, ///< Auto node - handles adjusted automatically based on neighboring nodes + NODE_SYMMETRIC, ///< Symmetric node - handles must be colinear and of equal length + NODE_LAST_REAL_TYPE, ///< Last real type of node - used for ctrl+click on a node + NODE_PICK_BEST = 100 ///< Select type based on handle positions +}; + +/** Types of segments supported in the node tool. */ +enum SegmentType { + SEGMENT_STRAIGHT, ///< Straight linear segment + SEGMENT_CUBIC_BEZIER ///< Bezier curve with two control points +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/node.cpp b/src/ui/tool/node.cpp new file mode 100644 index 000000000..e9fa79fb3 --- /dev/null +++ b/src/ui/tool/node.cpp @@ -0,0 +1,1407 @@ +/** @file + * Editable node - implementation + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <iostream> +#include <stdexcept> +#include <boost/utility.hpp> +#include <glib.h> +#include <glib/gi18n.h> +#include <2geom/bezier-utils.h> +#include <2geom/transforms.h> + +#include "display/sp-ctrlline.h" +#include "display/sp-canvas.h" +#include "display/sp-canvas-util.h" +#include "desktop.h" +#include "desktop-handles.h" +#include "preferences.h" +#include "snap.h" +#include "snap-preferences.h" +#include "sp-metrics.h" +#include "sp-namedview.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/node.h" +#include "ui/tool/path-manipulator.h" + +namespace Inkscape { +namespace UI { + +static SelectableControlPoint::ColorSet node_colors = { + { + {0xbfbfbf00, 0x000000ff}, // normal fill, stroke + {0xff000000, 0x000000ff}, // mouseover fill, stroke + {0xff000000, 0x000000ff} // clicked fill, stroke + }, + {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected + {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected + {0xff000000, 0x000000ff} // clicked fill, stroke when selected +}; + +static ControlPoint::ColorSet handle_colors = { + {0xffffffff, 0x000000ff}, // normal fill, stroke + {0xff000000, 0x000000ff}, // mouseover fill, stroke + {0xff000000, 0x000000ff} // clicked fill, stroke +}; + +std::ostream &operator<<(std::ostream &out, NodeType type) +{ + switch(type) { + case NODE_CUSP: out << 'c'; break; + case NODE_SMOOTH: out << 's'; break; + case NODE_AUTO: out << 'a'; break; + case NODE_SYMMETRIC: out << 'z'; break; + default: out << 'b'; break; + } + return out; +} + +/** Computes an unit vector of the direction from first to second control point */ +static Geom::Point direction(Geom::Point const &first, Geom::Point const &second) { + return Geom::unit_vector(second - first); +} + +/** + * @class Handle + * @brief Control point of a cubic Bezier curve in a path. + * + * Handle keeps the node type invariant only for the opposite handle of the same node. + * Keeping the invariant on node moves is left to the %Node class. + */ + +Geom::Point Handle::_saved_other_pos(0, 0); +double Handle::_saved_length = 0.0; +bool Handle::_drag_out = false; + +Handle::Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent) + : ControlPoint(data.desktop, initial_pos, Gtk::ANCHOR_CENTER, SP_CTRL_SHAPE_CIRCLE, 7.0, + &handle_colors, data.handle_group) + , _parent(parent) + , _degenerate(true) +{ + _cset = &handle_colors; + _handle_line = sp_canvas_item_new(data.handle_line_group, SP_TYPE_CTRLLINE, NULL); + setVisible(false); +} +Handle::~Handle() +{ + //sp_canvas_item_hide(_handle_line); + gtk_object_destroy(GTK_OBJECT(_handle_line)); +} + +void Handle::setVisible(bool v) +{ + ControlPoint::setVisible(v); + if (v) sp_canvas_item_show(_handle_line); + else sp_canvas_item_hide(_handle_line); +} + +void Handle::move(Geom::Point const &new_pos) +{ + Handle *other = this->other(); + Node *node_towards = _parent->nodeToward(this); // node in direction of this handle + Node *node_away = _parent->nodeAwayFrom(this); // node in the opposite direction + Handle *towards = node_towards ? node_towards->handleAwayFrom(_parent) : NULL; + Handle *towards_second = node_towards ? node_towards->handleToward(_parent) : NULL; + + if (Geom::are_near(new_pos, _parent->position())) { + // The handle becomes degenerate. If the segment between it and the node + // in its direction becomes linear and there are smooth nodes + // at its ends, make their handles colinear with the segment + if (towards && towards->isDegenerate()) { + if (node_towards->type() == NODE_SMOOTH) { + towards_second->setDirection(*_parent, *node_towards); + } + if (_parent->type() == NODE_SMOOTH) { + other->setDirection(*node_towards, *_parent); + } + } + setPosition(new_pos); + return; + } + + if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) { + // restrict movement to the line joining the nodes + Geom::Point direction = _parent->position() - node_away->position(); + Geom::Point delta = new_pos - _parent->position(); + // project the relative position on the direction line + Geom::Point new_delta = (Geom::dot(delta, direction) + / Geom::L2sq(direction)) * direction; + setRelativePos(new_delta); + return; + } + + switch (_parent->type()) { + case NODE_AUTO: + _parent->setType(NODE_SMOOTH, false); + // fall through - auto nodes degrade into smooth nodes + case NODE_SMOOTH: { + /* for smooth nodes, we need to rotate the other handle so that it's colinear + * with the dragged one while conserving length. */ + other->setDirection(new_pos, *_parent); + } break; + case NODE_SYMMETRIC: + // for symmetric nodes, place the other handle on the opposite side + other->setRelativePos(-(new_pos - _parent->position())); + break; + default: break; + } + + setPosition(new_pos); +} + +void Handle::setPosition(Geom::Point const &p) +{ + ControlPoint::setPosition(p); + sp_ctrlline_set_coords(SP_CTRLLINE(_handle_line), _parent->position(), position()); + + // update degeneration info and visibility + if (Geom::are_near(position(), _parent->position())) + _degenerate = true; + else _degenerate = false; + if (_parent->_handles_shown && _parent->visible() && !_degenerate) { + setVisible(true); + } else { + setVisible(false); + } + // If both handles become degenerate, convert to parent cusp node + if (_parent->isDegenerate()) { + _parent->setType(NODE_CUSP, false); + } +} + +void Handle::setLength(double len) +{ + if (isDegenerate()) return; + Geom::Point dir = Geom::unit_vector(relativePos()); + setRelativePos(dir * len); +} + +void Handle::retract() +{ + setPosition(_parent->position()); +} + +void Handle::setDirection(Geom::Point const &from, Geom::Point const &to) +{ + setDirection(to - from); +} + +void Handle::setDirection(Geom::Point const &dir) +{ + Geom::Point unitdir = Geom::unit_vector(dir); + setRelativePos(unitdir * length()); +} + +char const *Handle::handle_type_to_localized_string(NodeType type) +{ + switch(type) { + case NODE_CUSP: return _("Cusp node handle"); + case NODE_SMOOTH: return _("Smooth node handle"); + case NODE_SYMMETRIC: return _("Symmetric node handle"); + case NODE_AUTO: return _("Auto-smooth node handle"); + default: return ""; + } +} + +bool Handle::grabbed(GdkEventMotion *) +{ + _saved_other_pos = other()->position(); + _saved_length = _drag_out ? 0 : length(); + _pm()._handleGrabbed(); + return false; +} + +void Handle::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + Geom::Point parent_pos = _parent->position(); + Geom::Point origin = _last_drag_origin(); + SnapManager &sm = _desktop->namedview->snap_manager; + bool snap = sm.someSnapperMightSnap(); + + // with Alt, preserve length + if (held_alt(*event)) { + new_pos = parent_pos + Geom::unit_vector(new_pos - parent_pos) * _saved_length; + snap = false; + } + // with Ctrl, constrain to M_PI/rotationsnapsperpi increments from vertical + // and the original position. + if (held_control(*event)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = 2 * prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + + // note: if snapping to the original position is only desired in the original + // direction of the handle, change to Ray instead of Line + Geom::Line original_line(parent_pos, origin); + Geom::Point snap_pos = parent_pos + Geom::constrain_angle( + Geom::Point(0,0), new_pos - parent_pos, snaps, Geom::Point(1,0)); + Geom::Point orig_pos = original_line.pointAt(original_line.nearestPoint(new_pos)); + + if (Geom::distance(snap_pos, new_pos) < Geom::distance(orig_pos, new_pos)) { + new_pos = snap_pos; + } else { + new_pos = orig_pos; + } + snap = false; + } + + std::vector<Inkscape::SnapCandidatePoint> unselected; + if (snap) { + typedef ControlPointSelection::Set Set; + Set &nodes = _parent->_selection.allPoints(); + for (Set::iterator i = nodes.begin(); i != nodes.end(); ++i) { + Node *n = static_cast<Node*>(*i); + Inkscape::SnapCandidatePoint p(n->position(), n->_snapSourceType(), n->_snapTargetType()); + unselected.push_back(p); + } + sm.setupIgnoreSelection(_desktop, true, &unselected); + + Node *node_away = (this == &_parent->_front ? _parent->_prev() : _parent->_next()); + if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) { + Inkscape::Snapper::ConstraintLine cl(_parent->position(), + _parent->position() - node_away->position()); + Inkscape::SnappedPoint p; + p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), cl); + if (p.getSnapped()) { + p.getPoint(new_pos); + } + } else { + sm.freeSnapReturnByRef(new_pos, SNAPSOURCE_NODE_HANDLE); + } + } + + // with Shift, if the node is cusp, rotate the other handle as well + if (_parent->type() == NODE_CUSP && !_drag_out) { + if (held_shift(*event)) { + Geom::Point other_relpos = _saved_other_pos - parent_pos; + other_relpos *= Geom::Rotate(Geom::angle_between(origin - parent_pos, new_pos - parent_pos)); + other()->setRelativePos(other_relpos); + } else { + // restore the position + other()->setPosition(_saved_other_pos); + } + } + move(new_pos); // needed for correct update, even though it's redundant + _pm().update(); +} + +void Handle::ungrabbed(GdkEventButton *event) +{ + // hide the handle if it's less than dragtolerance away from the node + // TODO is this actually desired? + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + Geom::Point dist = _desktop->d2w(_parent->position()) - _desktop->d2w(position()); + if (dist.length() <= drag_tolerance) { + move(_parent->position()); + } + + // HACK: If the handle was dragged out, call parent's ungrabbed handler, + // so that transform handles reappear + if (_drag_out) { + _parent->ungrabbed(event); + } + _drag_out = false; + + _pm()._handleUngrabbed(); +} + +bool Handle::clicked(GdkEventButton *event) +{ + _pm()._handleClicked(this, event); + return true; +} + +Handle *Handle::other() +{ + if (this == &_parent->_front) return &_parent->_back; + return &_parent->_front; +} + +static double snap_increment_degrees() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + return 180.0 / snaps; +} + +Glib::ustring Handle::_getTip(unsigned state) +{ + char const *more; + bool can_shift_rotate = _parent->type() == NODE_CUSP && !other()->isDegenerate(); + if (can_shift_rotate) { + more = C_("Path handle tip", "more: Shift, Ctrl, Alt"); + } else { + more = C_("Path handle tip", "more: Ctrl, Alt"); + } + if (state_held_alt(state)) { + if (state_held_control(state)) { + if (state_held_shift(state) && can_shift_rotate) { + return format_tip(C_("Path handle tip", + "<b>Shift+Ctrl+Alt</b>: preserve length and snap rotation angle to %g° " + "increments while rotating both handles"), + snap_increment_degrees()); + } else { + return format_tip(C_("Path handle tip", + "<b>Ctrl+Alt</b>: preserve length and snap rotation angle to %g° increments"), + snap_increment_degrees()); + } + } else { + if (state_held_shift(state) && can_shift_rotate) { + return C_("Path handle tip", + "<b>Shift+Alt:</b> preserve handle length and rotate both handles"); + } else { + return C_("Path handle tip", + "<b>Alt:</b> preserve handle length while dragging"); + } + } + } else { + if (state_held_control(state)) { + if (state_held_shift(state) && can_shift_rotate) { + return format_tip(C_("Path handle tip", + "<b>Shift+Ctrl:</b> snap rotation angle to %g° increments and rotate both handles"), + snap_increment_degrees()); + } else { + return format_tip(C_("Path handle tip", + "<b>Ctrl:</b> snap rotation angle to %g° increments, click to retract"), + snap_increment_degrees()); + } + } else if (state_held_shift(state) && can_shift_rotate) { + return C_("Path hande tip", + "<b>Shift</b>: rotate both handles by the same angle"); + } + } + + switch (_parent->type()) { + case NODE_AUTO: + return format_tip(C_("Path handle tip", + "<b>Auto node handle:</b> drag to convert to smooth node (%s)"), more); + default: + return format_tip(C_("Path handle tip", + "<b>%s:</b> drag to shape the segment (%s)"), + handle_type_to_localized_string(_parent->type()), more); + } +} + +Glib::ustring Handle::_getDragTip(GdkEventMotion */*event*/) +{ + Geom::Point dist = position() - _last_drag_origin(); + // report angle in mathematical convention + double angle = Geom::angle_between(Geom::Point(-1,0), position() - _parent->position()); + angle += M_PI; // angle is (-M_PI...M_PI] - offset by +pi and scale to 0...360 + angle *= 360.0 / (2 * M_PI); + GString *x = SP_PX_TO_METRIC_STRING(dist[Geom::X], _desktop->namedview->getDefaultMetric()); + GString *y = SP_PX_TO_METRIC_STRING(dist[Geom::Y], _desktop->namedview->getDefaultMetric()); + GString *len = SP_PX_TO_METRIC_STRING(length(), _desktop->namedview->getDefaultMetric()); + Glib::ustring ret = format_tip(C_("Path handle tip", + "Move handle by %s, %s; angle %.2f°, length %s"), x->str, y->str, angle, len->str); + g_string_free(x, TRUE); + g_string_free(y, TRUE); + g_string_free(len, TRUE); + return ret; +} + +/** + * @class Node + * @brief Curve endpoint in an editable path. + * + * The method move() keeps node type invariants during translations. + */ + +Node::Node(NodeSharedData const &data, Geom::Point const &initial_pos) + : SelectableControlPoint(data.desktop, initial_pos, Gtk::ANCHOR_CENTER, + SP_CTRL_SHAPE_DIAMOND, 9.0, *data.selection, &node_colors, data.node_group) + , _front(data, initial_pos, this) + , _back(data, initial_pos, this) + , _type(NODE_CUSP) + , _handles_shown(false) +{ + // NOTE we do not set type here, because the handles are still degenerate +} + +// NOTE: not using iterators won't make this much quicker because iterators can be 100% inlined. +Node *Node::_next() +{ + NodeList::iterator n = NodeList::get_iterator(this).next(); + if (n) return n.ptr(); + return NULL; +} +Node *Node::_prev() +{ + NodeList::iterator p = NodeList::get_iterator(this).prev(); + if (p) return p.ptr(); + return NULL; +} + +void Node::move(Geom::Point const &new_pos) +{ + // move handles when the node moves. + Geom::Point old_pos = position(); + Geom::Point delta = new_pos - position(); + setPosition(new_pos); + _front.setPosition(_front.position() + delta); + _back.setPosition(_back.position() + delta); + + // if the node has a smooth handle after a line segment, it should be kept colinear + // with the segment + _fixNeighbors(old_pos, new_pos); +} + +void Node::transform(Geom::Matrix const &m) +{ + Geom::Point old_pos = position(); + setPosition(position() * m); + _front.setPosition(_front.position() * m); + _back.setPosition(_back.position() * m); + + /* Affine transforms keep handle invariants for smooth and symmetric nodes, + * but smooth nodes at ends of linear segments and auto nodes need special treatment */ + _fixNeighbors(old_pos, position()); +} + +Geom::Rect Node::bounds() +{ + Geom::Rect b(position(), position()); + b.expandTo(_front.position()); + b.expandTo(_back.position()); + return b; +} + +void Node::_fixNeighbors(Geom::Point const &old_pos, Geom::Point const &new_pos) +{ + /* This method restores handle invariants for neighboring nodes, + * and invariants that are based on positions of those nodes for this one. */ + + /* Fix auto handles */ + if (_type == NODE_AUTO) _updateAutoHandles(); + if (old_pos != new_pos) { + if (_next() && _next()->_type == NODE_AUTO) _next()->_updateAutoHandles(); + if (_prev() && _prev()->_type == NODE_AUTO) _prev()->_updateAutoHandles(); + } + + /* Fix smooth handles at the ends of linear segments. + * Rotate the appropriate handle to be colinear with the segment. + * If there is a smooth node at the other end of the segment, rotate it too. */ + Handle *handle, *other_handle; + Node *other; + if (_is_line_segment(this, _next())) { + handle = &_back; + other = _next(); + other_handle = &_next()->_front; + } else if (_is_line_segment(_prev(), this)) { + handle = &_front; + other = _prev(); + other_handle = &_prev()->_back; + } else return; + + if (_type == NODE_SMOOTH && !handle->isDegenerate()) { + handle->setDirection(other->position(), new_pos); + } + // also update the handle on the other end of the segment + if (other->_type == NODE_SMOOTH && !other_handle->isDegenerate()) { + other_handle->setDirection(new_pos, other->position()); + } +} + +void Node::_updateAutoHandles() +{ + // Recompute the position of automatic handles. + // For endnodes, retract both handles. (It's only possible to create an end auto node + // through the XML editor.) + if (isEndNode()) { + _front.retract(); + _back.retract(); + return; + } + + // Auto nodes automaticaly adjust their handles to give an appearance of smoothness, + // no matter what their surroundings are. + Geom::Point vec_next = _next()->position() - position(); + Geom::Point vec_prev = _prev()->position() - position(); + double len_next = vec_next.length(), len_prev = vec_prev.length(); + if (len_next > 0 && len_prev > 0) { + // "dir" is an unit vector perpendicular to the bisector of the angle created + // by the previous node, this auto node and the next node. + Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev); + // Handle lengths are equal to 1/3 of the distance from the adjacent node. + _back.setRelativePos(-dir * (len_prev / 3)); + _front.setRelativePos(dir * (len_next / 3)); + } else { + // If any of the adjacent nodes coincides, retract both handles. + _front.retract(); + _back.retract(); + } +} + +void Node::showHandles(bool v) +{ + _handles_shown = v; + if (!_front.isDegenerate()) _front.setVisible(v); + if (!_back.isDegenerate()) _back.setVisible(v); +} + +/** Sets the node type and optionally restores the invariants associated with the given type. + * @param type The type to set + * @param update_handles Whether to restore invariants associated with the given type. + * Passing false is useful e.g. wen initially creating the path, + * and when making cusp nodes during some node algorithms. + * Pass true when used in response to an UI node type button. + */ +void Node::setType(NodeType type, bool update_handles) +{ + if (type == NODE_PICK_BEST) { + pickBestType(); + updateState(); // The size of the control might have changed + return; + } + + // if update_handles is true, adjust handle positions to match the node type + // handle degenerate handles appropriately + if (update_handles) { + switch (type) { + case NODE_CUSP: + // if the existing type is also NODE_CUSP, retract handles + if (_type == NODE_CUSP) { + _front.retract(); + _back.retract(); + } + break; + case NODE_AUTO: + // auto handles make no sense for endnodes + if (isEndNode()) return; + _updateAutoHandles(); + break; + case NODE_SMOOTH: { + // rotate handles to be colinear + // for degenerate nodes set positions like auto handles + bool prev_line = _is_line_segment(_prev(), this); + bool next_line = _is_line_segment(this, _next()); + if (_type == NODE_SMOOTH) { + // for a node that is already smooth and has a degenerate handle, + // drag out the second handle to 1/3 the length of the linear segment + if (_front.isDegenerate()) { + double dist = Geom::distance(_next()->position(), position()); + _front.setRelativePos(Geom::unit_vector(-_back.relativePos()) * dist / 3); + } + if (_back.isDegenerate()) { + double dist = Geom::distance(_prev()->position(), position()); + _back.setRelativePos(Geom::unit_vector(-_front.relativePos()) * dist / 3); + } + } else if (isDegenerate()) { + _updateAutoHandles(); + } else if (_front.isDegenerate()) { + // if the front handle is degenerate and this...next is a line segment, + // make back colinear; otherwise pull out the other handle + // to 1/3 of distance to prev + if (next_line) { + _back.setDirection(*_next(), *this); + } else if (_prev()) { + Geom::Point dir = direction(_back, *this); + _front.setRelativePos(Geom::distance(_prev()->position(), position()) / 3 * dir); + } + } else if (_back.isDegenerate()) { + if (prev_line) { + _front.setDirection(*_prev(), *this); + } else if (_next()) { + Geom::Point dir = direction(_front, *this); + _back.setRelativePos(Geom::distance(_next()->position(), position()) / 3 * dir); + } + } else { + // both handles are extended. make colinear while keeping length + // first make back colinear with the vector front ---> back, + // then make front colinear with back ---> node + // (not back ---> front because back's position was changed in the first call) + _back.setDirection(_front, _back); + _front.setDirection(_back, *this); + } + } break; + case NODE_SYMMETRIC: + if (isEndNode()) return; // symmetric handles make no sense for endnodes + if (isDegenerate()) { + // similar to auto handles but set the same length for both + Geom::Point vec_next = _next()->position() - position(); + Geom::Point vec_prev = _prev()->position() - position(); + double len_next = vec_next.length(), len_prev = vec_prev.length(); + double len = (len_next + len_prev) / 6; // take 1/3 of average + if (len == 0) return; + + Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev); + _back.setRelativePos(-dir * len); + _front.setRelativePos(dir * len); + } else { + // Both handles are extended. Compute average length, use direction from + // back handle to front handle. This also works correctly for degenerates + double len = (_front.length() + _back.length()) / 2; + Geom::Point dir = direction(_back, _front); + _front.setRelativePos(dir * len); + _back.setRelativePos(-dir * len); + } + break; + default: break; + } + } + _type = type; + _setShape(_node_type_to_shape(type)); + updateState(); +} + +/** Pick the best type for this node, based on the position of its handles. + * This is what assigns types to nodes created using the pen tool. */ +void Node::pickBestType() +{ + _type = NODE_CUSP; + bool front_degen = _front.isDegenerate(); + bool back_degen = _back.isDegenerate(); + bool both_degen = front_degen && back_degen; + bool neither_degen = !front_degen && !back_degen; + do { + // if both handles are degenerate, do nothing + if (both_degen) break; + // if neither are degenerate, check their respective positions + if (neither_degen) { + Geom::Point front_delta = _front.position() - position(); + Geom::Point back_delta = _back.position() - position(); + // for now do not automatically make nodes symmetric, it can be annoying + /*if (Geom::are_near(front_delta, -back_delta)) { + _type = NODE_SYMMETRIC; + break; + }*/ + if (Geom::are_near(Geom::unit_vector(front_delta), + Geom::unit_vector(-back_delta))) + { + _type = NODE_SMOOTH; + break; + } + } + // check whether the handle aligns with the previous line segment. + // we know that if front is degenerate, back isn't, because + // both_degen was false + if (front_degen && _next() && _next()->_back.isDegenerate()) { + Geom::Point segment_delta = Geom::unit_vector(_next()->position() - position()); + Geom::Point handle_delta = Geom::unit_vector(_back.position() - position()); + if (Geom::are_near(segment_delta, -handle_delta)) { + _type = NODE_SMOOTH; + break; + } + } else if (back_degen && _prev() && _prev()->_front.isDegenerate()) { + Geom::Point segment_delta = Geom::unit_vector(_prev()->position() - position()); + Geom::Point handle_delta = Geom::unit_vector(_front.position() - position()); + if (Geom::are_near(segment_delta, -handle_delta)) { + _type = NODE_SMOOTH; + break; + } + } + } while (false); + _setShape(_node_type_to_shape(_type)); + updateState(); +} + +bool Node::isEndNode() +{ + return !_prev() || !_next(); +} + +/** Move the node to the bottom of its canvas group. Useful for node break, to ensure that + * the selected nodes are above the unselected ones. */ +void Node::sink() +{ + sp_canvas_item_move_to_z(_canvas_item, 0); +} + +NodeType Node::parse_nodetype(char x) +{ + switch (x) { + case 'a': return NODE_AUTO; + case 'c': return NODE_CUSP; + case 's': return NODE_SMOOTH; + case 'z': return NODE_SYMMETRIC; + default: return NODE_PICK_BEST; + } +} + +/** Customized event handler to catch scroll events needed for selection grow/shrink. */ +bool Node::_eventHandler(GdkEvent *event) +{ + static NodeList::iterator origin; + static int dir; + + switch (event->type) + { + case GDK_SCROLL: + if (event->scroll.direction == GDK_SCROLL_UP) { + dir = 1; + } else if (event->scroll.direction == GDK_SCROLL_DOWN) { + dir = -1; + } else break; + if (held_control(event->scroll)) { + _selection.spatialGrow(this, dir); + } else { + _linearGrow(dir); + } + return true; + default: + break; + } + return ControlPoint::_eventHandler(event); +} + +// TODO Move this to 2Geom! +static double bezier_length (Geom::Point a0, Geom::Point a1, Geom::Point a2, Geom::Point a3) +{ + double lower = Geom::distance(a0, a3); + double upper = Geom::distance(a0, a1) + Geom::distance(a1, a2) + Geom::distance(a2, a3); + + if (upper - lower < Geom::EPSILON) return (lower + upper)/2; + + Geom::Point // Casteljau subdivision + b0 = a0, + c0 = a3, + b1 = 0.5*(a0 + a1), + t0 = 0.5*(a1 + a2), + c1 = 0.5*(a2 + a3), + b2 = 0.5*(b1 + t0), + c2 = 0.5*(t0 + c1), + b3 = 0.5*(b2 + c2); // == c3 + return bezier_length(b0, b1, b2, b3) + bezier_length(b3, c2, c1, c0); +} + +/** Select or deselect a node in this node's subpath based on its path distance from this node. + * @param dir If negative, shrink selection by one node; if positive, grow by one node */ +void Node::_linearGrow(int dir) +{ + // Interestingly, we do not need any help from PathManipulator when doing linear grow. + // First handle the trivial case of growing over an unselected node. + if (!selected() && dir > 0) { + _selection.insert(this); + return; + } + + NodeList::iterator this_iter = NodeList::get_iterator(this); + NodeList::iterator fwd = this_iter, rev = this_iter; + double distance_back = 0, distance_front = 0; + + // Linear grow is simple. We find the first unselected nodes in each direction + // and compare the linear distances to them. + if (dir > 0) { + if (!selected()) { + _selection.insert(this); + return; + } + + // find first unselected nodes on both sides + while (fwd && fwd->selected()) { + NodeList::iterator n = fwd.next(); + distance_front += bezier_length(*fwd, fwd->_front, n->_back, *n); + fwd = n; + if (fwd == this_iter) + // there is no unselected node in this cyclic subpath + return; + } + // do the same for the second direction. Do not check for equality with + // this node, because there is at least one unselected node in the subpath, + // so we are guaranteed to stop. + while (rev && rev->selected()) { + NodeList::iterator p = rev.prev(); + distance_back += bezier_length(*rev, rev->_back, p->_front, *p); + rev = p; + } + + NodeList::iterator t; // node to select + if (fwd && rev) { + if (distance_front <= distance_back) t = fwd; + else t = rev; + } else { + if (fwd) t = fwd; + if (rev) t = rev; + } + if (t) _selection.insert(t.ptr()); + + // Linear shrink is more complicated. We need to find the farthest selected node. + // This means we have to check the entire subpath. We go in the direction in which + // the distance we traveled is lower. We do this until we run out of nodes (ends of path) + // or the two iterators meet. On the way, we store the last selected node and its distance + // in each direction (if any). At the end, we choose the one that is farther and deselect it. + } else { + // both iterators that store last selected nodes are initially empty + NodeList::iterator last_fwd, last_rev; + double last_distance_back = 0, last_distance_front = 0; + + while (rev || fwd) { + if (fwd && (!rev || distance_front <= distance_back)) { + if (fwd->selected()) { + last_fwd = fwd; + last_distance_front = distance_front; + } + NodeList::iterator n = fwd.next(); + if (n) distance_front += bezier_length(*fwd, fwd->_front, n->_back, *n); + fwd = n; + } else if (rev && (!fwd || distance_front > distance_back)) { + if (rev->selected()) { + last_rev = rev; + last_distance_back = distance_back; + } + NodeList::iterator p = rev.prev(); + if (p) distance_back += bezier_length(*rev, rev->_back, p->_front, *p); + rev = p; + } + // Check whether we walked the entire cyclic subpath. + // This is initially true because both iterators start from this node, + // so this check cannot go in the while condition. + // When this happens, we need to check the last node, pointed to by the iterators. + if (fwd && fwd == rev) { + if (!fwd->selected()) break; + NodeList::iterator fwdp = fwd.prev(), revn = rev.next(); + double df = distance_front + bezier_length(*fwdp, fwdp->_front, fwd->_back, *fwd); + double db = distance_back + bezier_length(*revn, revn->_back, rev->_front, *rev); + if (df > db) { + last_fwd = fwd; + last_distance_front = df; + } else { + last_rev = rev; + last_distance_back = db; + } + break; + } + } + + NodeList::iterator t; + if (last_fwd && last_rev) { + if (last_distance_front >= last_distance_back) t = last_fwd; + else t = last_rev; + } else { + if (last_fwd) t = last_fwd; + if (last_rev) t = last_rev; + } + if (t) _selection.erase(t.ptr()); + } +} + +void Node::_setState(State state) +{ + // change node size to match type and selection state + switch (_type) { + case NODE_AUTO: + case NODE_CUSP: + if (selected()) _setSize(11); + else _setSize(9); + break; + default: + if(selected()) _setSize(9); + else _setSize(7); + break; + } + SelectableControlPoint::_setState(state); +} + +bool Node::grabbed(GdkEventMotion *event) +{ + if (SelectableControlPoint::grabbed(event)) + return true; + + // Dragging out handles with Shift + drag on a node. + if (!held_shift(*event)) return false; + + Handle *h; + Geom::Point evp = event_point(*event); + Geom::Point rel_evp = evp - _last_click_event_point(); + + // This should work even if dragtolerance is zero and evp coincides with node position. + double angle_next = HUGE_VAL; + double angle_prev = HUGE_VAL; + bool has_degenerate = false; + // determine which handle to drag out based on degeneration and the direction of drag + if (_front.isDegenerate() && _next()) { + Geom::Point next_relpos = _desktop->d2w(_next()->position()) + - _desktop->d2w(position()); + angle_next = fabs(Geom::angle_between(rel_evp, next_relpos)); + has_degenerate = true; + } + if (_back.isDegenerate() && _prev()) { + Geom::Point prev_relpos = _desktop->d2w(_prev()->position()) + - _desktop->d2w(position()); + angle_prev = fabs(Geom::angle_between(rel_evp, prev_relpos)); + has_degenerate = true; + } + if (!has_degenerate) return false; + h = angle_next < angle_prev ? &_front : &_back; + + h->setPosition(_desktop->w2d(evp)); + h->setVisible(true); + h->transferGrab(this, event); + Handle::_drag_out = true; + return true; +} + +void Node::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + // For a note on how snapping is implemented in Inkscape, see snap.h. + SnapManager &sm = _desktop->namedview->snap_manager; + bool snap = sm.someSnapperMightSnap(); + std::vector<Inkscape::SnapCandidatePoint> unselected; + if (snap) { + /* setup + * TODO We are doing this every time a snap happens. It should once be done only once + * per drag - maybe in the grabbed handler? + * TODO Unselected nodes vector must be valid during the snap run, because it is not + * copied. Fix this in snap.h and snap.cpp, then the above. + * TODO Snapping to unselected segments of selected paths doesn't work yet. */ + + // Build the list of unselected nodes. + typedef ControlPointSelection::Set Set; + Set &nodes = _selection.allPoints(); + for (Set::iterator i = nodes.begin(); i != nodes.end(); ++i) { + if (!(*i)->selected()) { + Node *n = static_cast<Node*>(*i); + Inkscape::SnapCandidatePoint p(n->position(), n->_snapSourceType(), n->_snapTargetType()); + unselected.push_back(p); + } + } + sm.setupIgnoreSelection(_desktop, false, &unselected); + } + + if (held_control(*event)) { + Geom::Point origin = _last_drag_origin(); + Inkscape::SnappedPoint fp, bp; + if (held_alt(*event)) { + // with Ctrl+Alt, constrain to handle lines + // project the new position onto a handle line that is closer + boost::optional<Geom::Point> front_point, back_point; + boost::optional<Inkscape::Snapper::ConstraintLine> line_front, line_back; + if (_front.isDegenerate()) { + if (_is_line_segment(this, _next())) + front_point = _next()->position() - origin; + } else { + front_point = _front.relativePos(); + } + if (_back.isDegenerate()) { + if (_is_line_segment(_prev(), this)) + back_point = _prev()->position() - origin; + } else { + back_point = _back.relativePos(); + } + if (front_point) + line_front = Inkscape::Snapper::ConstraintLine(origin, *front_point); + if (back_point) + line_back = Inkscape::Snapper::ConstraintLine(origin, *back_point); + + // TODO: combine the snap and non-snap branches by modifying snap.h / snap.cpp + if (snap) { + if (line_front) { + fp = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, + _snapSourceType()), *line_front); + } + if (line_back) { + bp = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, + _snapSourceType()), *line_back); + } + } + if (fp.getSnapped() || bp.getSnapped()) { + if (fp.isOtherSnapBetter(bp, false)) { + fp = bp; + } + fp.getPoint(new_pos); + _desktop->snapindicator->set_new_snaptarget(fp); + } else { + boost::optional<Geom::Point> pos; + if (line_front) { + pos = line_front->projection(new_pos); + } + if (line_back) { + Geom::Point pos2 = line_back->projection(new_pos); + if (!pos || (pos && Geom::distance(new_pos, *pos) > Geom::distance(new_pos, pos2))) + pos = pos2; + } + if (pos) { + new_pos = *pos; + } else { + new_pos = origin; + } + } + } else { + // with Ctrl, constrain to axes + // TODO combine the two branches + if (snap) { + Inkscape::Snapper::ConstraintLine line_x(origin, Geom::Point(1, 0)); + Inkscape::Snapper::ConstraintLine line_y(origin, Geom::Point(0, 1)); + fp = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), line_x); + bp = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), line_y); + } + if (fp.getSnapped() || bp.getSnapped()) { + if (fp.isOtherSnapBetter(bp, false)) { + fp = bp; + } + fp.getPoint(new_pos); + if (fp.getTarget() != SNAPTARGET_CONSTRAINT) { + _desktop->snapindicator->set_new_snaptarget(fp); + } + } else { + Geom::Point origin = _last_drag_origin(); + Geom::Point delta = new_pos - origin; + Geom::Dim2 d = (fabs(delta[Geom::X]) < fabs(delta[Geom::Y])) ? Geom::X : Geom::Y; + new_pos[d] = origin[d]; + } + } + } else if (snap) { + Inkscape::SnappedPoint p = sm.freeSnap(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType())); + if (p.getSnapped()) { + p.getPoint(new_pos); + _desktop->snapindicator->set_new_snaptarget(p); + } + } + + SelectableControlPoint::dragged(new_pos, event); +} + +bool Node::clicked(GdkEventButton *event) +{ + if(_pm()._nodeClicked(this, event)) + return true; + return SelectableControlPoint::clicked(event); +} + +Inkscape::SnapSourceType Node::_snapSourceType() +{ + if (_type == NODE_SMOOTH || _type == NODE_AUTO) + return SNAPSOURCE_NODE_SMOOTH; + return SNAPSOURCE_NODE_CUSP; +} +Inkscape::SnapTargetType Node::_snapTargetType() +{ + if (_type == NODE_SMOOTH || _type == NODE_AUTO) + return SNAPTARGET_NODE_SMOOTH; + return SNAPTARGET_NODE_CUSP; +} + +/** @brief Gets the handle that faces the given adjacent node. + * Will abort with error if the given node is not adjacent. */ +Handle *Node::handleToward(Node *to) +{ + if (_next() == to) { + return front(); + } + if (_prev() == to) { + return back(); + } + g_error("Node::handleToward(): second node is not adjacent!"); +} + +/** @brief Gets the node in the direction of the given handle. + * Will abort with error if the handle doesn't belong to this node. */ +Node *Node::nodeToward(Handle *dir) +{ + if (front() == dir) { + return _next(); + } + if (back() == dir) { + return _prev(); + } + g_error("Node::nodeToward(): handle is not a child of this node!"); +} + +/** @brief Gets the handle that goes in the direction opposite to the given adjacent node. + * Will abort with error if the given node is not adjacent. */ +Handle *Node::handleAwayFrom(Node *to) +{ + if (_next() == to) { + return back(); + } + if (_prev() == to) { + return front(); + } + g_error("Node::handleAwayFrom(): second node is not adjacent!"); +} + +/** @brief Gets the node in the direction opposite to the given handle. + * Will abort with error if the handle doesn't belong to this node. */ +Node *Node::nodeAwayFrom(Handle *h) +{ + if (front() == h) { + return _prev(); + } + if (back() == h) { + return _next(); + } + g_error("Node::nodeAwayFrom(): handle is not a child of this node!"); +} + +Glib::ustring Node::_getTip(unsigned state) +{ + if (state_held_shift(state)) { + bool can_drag_out = (_next() && _front.isDegenerate()) || (_prev() && _back.isDegenerate()); + if (can_drag_out) { + /*if (state_held_control(state)) { + return format_tip(C_("Path node tip", + "<b>Shift+Ctrl:</b> drag out a handle and snap its angle " + "to %f° increments"), snap_increment_degrees()); + }*/ + return C_("Path node tip", + "<b>Shift:</b> drag out a handle, click to toggle selection"); + } + return C_("Path node tip", "<b>Shift:</b> click to toggle selection"); + } + + if (state_held_control(state)) { + if (state_held_alt(state)) { + return C_("Path node tip", "<b>Ctrl+Alt:</b> move along handle lines, click to delete node"); + } + return C_("Path node tip", + "<b>Ctrl:</b> move along axes, click to change node type"); + } + + if (state_held_alt(state)) { + return C_("Path node tip", "<b>Alt:</b> sculpt nodes"); + } + + // No modifiers: assemble tip from node type + char const *nodetype = node_type_to_localized_string(_type); + if (_selection.transformHandlesEnabled() && selected()) { + if (_selection.size() == 1) { + return format_tip(C_("Path node tip", + "<b>%s:</b> drag to shape the path (more: Shift, Ctrl, Alt)"), nodetype); + } + return format_tip(C_("Path node tip", + "<b>%s:</b> drag to shape the path, click to toggle scale/rotation handles (more: Shift, Ctrl, Alt)"), nodetype); + } + return format_tip(C_("Path node tip", + "<b>%s:</b> drag to shape the path, click to select only this node (more: Shift, Ctrl, Alt)"), nodetype); +} + +Glib::ustring Node::_getDragTip(GdkEventMotion */*event*/) +{ + Geom::Point dist = position() - _last_drag_origin(); + GString *x = SP_PX_TO_METRIC_STRING(dist[Geom::X], _desktop->namedview->getDefaultMetric()); + GString *y = SP_PX_TO_METRIC_STRING(dist[Geom::Y], _desktop->namedview->getDefaultMetric()); + Glib::ustring ret = format_tip(C_("Path node tip", "Move node by %s, %s"), + x->str, y->str); + g_string_free(x, TRUE); + g_string_free(y, TRUE); + return ret; +} + +char const *Node::node_type_to_localized_string(NodeType type) +{ + switch (type) { + case NODE_CUSP: return _("Cusp node"); + case NODE_SMOOTH: return _("Smooth node"); + case NODE_SYMMETRIC: return _("Symmetric node"); + case NODE_AUTO: return _("Auto-smooth node"); + default: return ""; + } +} + +/** Determine whether two nodes are joined by a linear segment. */ +bool Node::_is_line_segment(Node *first, Node *second) +{ + if (!first || !second) return false; + if (first->_next() == second) + return first->_front.isDegenerate() && second->_back.isDegenerate(); + if (second->_next() == first) + return second->_front.isDegenerate() && first->_back.isDegenerate(); + return false; +} + +SPCtrlShapeType Node::_node_type_to_shape(NodeType type) +{ + switch(type) { + case NODE_CUSP: return SP_CTRL_SHAPE_DIAMOND; + case NODE_SMOOTH: return SP_CTRL_SHAPE_SQUARE; + case NODE_AUTO: return SP_CTRL_SHAPE_CIRCLE; + case NODE_SYMMETRIC: return SP_CTRL_SHAPE_SQUARE; + default: return SP_CTRL_SHAPE_DIAMOND; + } +} + + +/** + * @class NodeList + * @brief An editable list of nodes representing a subpath. + * + * It can optionally be cyclic to represent a closed path. + * The list has iterators that act like plain node iterators, but can also be used + * to obtain shared pointers to nodes. + */ + +NodeList::NodeList(SubpathList &splist) + : _list(splist) + , _closed(false) +{ + this->ln_list = this; + this->ln_next = this; + this->ln_prev = this; +} + +NodeList::~NodeList() +{ + clear(); +} + +bool NodeList::empty() +{ + return ln_next == this; +} + +NodeList::size_type NodeList::size() +{ + size_type sz = 0; + for (ListNode *ln = ln_next; ln != this; ln = ln->ln_next) ++sz; + return sz; +} + +bool NodeList::closed() +{ + return _closed; +} + +/** A subpath is degenerate if it has no segments - either one node in an open path + * or no nodes in a closed path */ +bool NodeList::degenerate() +{ + return closed() ? empty() : ++begin() == end(); +} + +NodeList::iterator NodeList::before(double t, double *fracpart) +{ + double intpart; + *fracpart = std::modf(t, &intpart); + int index = intpart; + + iterator ret = begin(); + std::advance(ret, index); + return ret; +} + +// insert a node before i +NodeList::iterator NodeList::insert(iterator i, Node *x) +{ + ListNode *ins = i._node; + x->ln_next = ins; + x->ln_prev = ins->ln_prev; + ins->ln_prev->ln_next = x; + ins->ln_prev = x; + x->ln_list = this; + return iterator(x); +} + +void NodeList::splice(iterator pos, NodeList &list) +{ + splice(pos, list, list.begin(), list.end()); +} + +void NodeList::splice(iterator pos, NodeList &list, iterator i) +{ + NodeList::iterator j = i; + ++j; + splice(pos, list, i, j); +} + +void NodeList::splice(iterator pos, NodeList &/*list*/, iterator first, iterator last) +{ + ListNode *ins_beg = first._node, *ins_end = last._node, *at = pos._node; + for (ListNode *ln = ins_beg; ln != ins_end; ln = ln->ln_next) { + ln->ln_list = this; + } + ins_beg->ln_prev->ln_next = ins_end; + ins_end->ln_prev->ln_next = at; + at->ln_prev->ln_next = ins_beg; + + ListNode *atprev = at->ln_prev; + at->ln_prev = ins_end->ln_prev; + ins_end->ln_prev = ins_beg->ln_prev; + ins_beg->ln_prev = atprev; +} + +void NodeList::shift(int n) +{ + // 1. make the list perfectly cyclic + ln_next->ln_prev = ln_prev; + ln_prev->ln_next = ln_next; + // 2. find new begin + ListNode *new_begin = ln_next; + if (n > 0) { + for (; n > 0; --n) new_begin = new_begin->ln_next; + } else { + for (; n < 0; ++n) new_begin = new_begin->ln_prev; + } + // 3. relink begin to list + ln_next = new_begin; + ln_prev = new_begin->ln_prev; + new_begin->ln_prev->ln_next = this; + new_begin->ln_prev = this; +} + +void NodeList::reverse() +{ + for (ListNode *ln = ln_next; ln != this; ln = ln->ln_prev) { + std::swap(ln->ln_next, ln->ln_prev); + Node *node = static_cast<Node*>(ln); + Geom::Point save_pos = node->front()->position(); + node->front()->setPosition(node->back()->position()); + node->back()->setPosition(save_pos); + } + std::swap(ln_next, ln_prev); +} + +void NodeList::clear() +{ + for (iterator i = begin(); i != end();) erase (i++); +} + +NodeList::iterator NodeList::erase(iterator i) +{ + // some gymnastics are required to ensure that the node is valid when deleted; + // otherwise the code that updates handle visibility will break + Node *rm = static_cast<Node*>(i._node); + ListNode *rmnext = rm->ln_next, *rmprev = rm->ln_prev; + ++i; + delete rm; + rmprev->ln_next = rmnext; + rmnext->ln_prev = rmprev; + return i; +} + +// TODO this method is very ugly! +// converting SubpathList to an intrusive list might allow us to get rid of it +void NodeList::kill() +{ + for (SubpathList::iterator i = _list.begin(); i != _list.end(); ++i) { + if (i->get() == this) { + _list.erase(i); + return; + } + } +} + +NodeList &NodeList::get(Node *n) { + return n->nodeList(); +} +NodeList &NodeList::get(iterator const &i) { + return *(i._node->ln_list); +} + + +/** + * @class SubpathList + * @brief Editable path composed of one or more subpaths + */ + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/node.h b/src/ui/tool/node.h new file mode 100644 index 000000000..af4cd7e3a --- /dev/null +++ b/src/ui/tool/node.h @@ -0,0 +1,411 @@ +/** @file + * Editable node and associated data structures. + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_NODE_H +#define SEEN_UI_TOOL_NODE_H + +#include <glib.h> +#include <iterator> +#include <iosfwd> +#include <stdexcept> +#include <tr1/functional> +#include <boost/utility.hpp> +#include <boost/shared_ptr.hpp> +#include <boost/optional.hpp> +#include <boost/operators.hpp> +#include "snapped-point.h" +#include "ui/tool/selectable-control-point.h" +#include "ui/tool/node-types.h" + + +namespace Inkscape { +namespace UI { +template <typename> class NodeIterator; +} +} + +namespace std { +namespace tr1 { +template <typename N> struct hash< Inkscape::UI::NodeIterator<N> >; +} +} + +namespace Inkscape { +namespace UI { + +class PathManipulator; +class MultiPathManipulator; + +class Node; +class Handle; +class NodeList; +class SubpathList; +template <typename> class NodeIterator; + +std::ostream &operator<<(std::ostream &, NodeType); + +/* +template <typename T> +struct ListMember { + T *next; + T *prev; +}; +struct SubpathMember : public ListMember<NodeListMember> { + Subpath *list; +}; +struct SubpathListMember : public ListMember<SubpathListMember> { + SubpathList *list; +}; +*/ + +struct ListNode { + ListNode *ln_next; + ListNode *ln_prev; + NodeList *ln_list; +}; + +struct NodeSharedData { + SPDesktop *desktop; + ControlPointSelection *selection; + SPCanvasGroup *node_group; + SPCanvasGroup *handle_group; + SPCanvasGroup *handle_line_group; +}; + +class Handle : public ControlPoint { +public: + virtual ~Handle(); + inline Geom::Point relativePos(); + inline double length(); + bool isDegenerate() { return _degenerate; } + + virtual void setVisible(bool); + virtual void move(Geom::Point const &p); + + virtual void setPosition(Geom::Point const &p); + inline void setRelativePos(Geom::Point const &p); + void setLength(double len); + void retract(); + void setDirection(Geom::Point const &from, Geom::Point const &to); + void setDirection(Geom::Point const &dir); + Node *parent() { return _parent; } + Handle *other(); + + static char const *handle_type_to_localized_string(NodeType type); +protected: + Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent); + + virtual void dragged(Geom::Point &, GdkEventMotion *); + virtual bool grabbed(GdkEventMotion *); + virtual void ungrabbed(GdkEventButton *); + virtual bool clicked(GdkEventButton *); + + virtual Glib::ustring _getTip(unsigned state); + virtual Glib::ustring _getDragTip(GdkEventMotion *event); + virtual bool _hasDragTips() { return true; } +private: + inline PathManipulator &_pm(); + Node *_parent; // the handle's lifetime does not extend beyond that of the parent node, + // so a naked pointer is OK and allows setting it during Node's construction + SPCanvasItem *_handle_line; + bool _degenerate; // this is used often internally so it makes sense to cache this + + static Geom::Point _saved_other_pos; + static double _saved_length; + static bool _drag_out; + friend class Node; +}; + +class Node : ListNode, public SelectableControlPoint { +public: + Node(NodeSharedData const &data, Geom::Point const &pos); + virtual void move(Geom::Point const &p); + virtual void transform(Geom::Matrix const &m); + virtual Geom::Rect bounds(); + + NodeType type() { return _type; } + void setType(NodeType type, bool update_handles = true); + void showHandles(bool v); + void pickBestType(); // automatically determine the type from handle positions + bool isDegenerate() { return _front.isDegenerate() && _back.isDegenerate(); } + bool isEndNode(); + Handle *front() { return &_front; } + Handle *back() { return &_back; } + Handle *handleToward(Node *to); + Node *nodeToward(Handle *h); + Handle *handleAwayFrom(Node *to); + Node *nodeAwayFrom(Handle *h); + NodeList &nodeList() { return *(static_cast<ListNode*>(this)->ln_list); } + void sink(); + + static NodeType parse_nodetype(char x); + static char const *node_type_to_localized_string(NodeType type); + // temporarily public + virtual bool _eventHandler(GdkEvent *event); +protected: + virtual void dragged(Geom::Point &, GdkEventMotion *); + virtual bool grabbed(GdkEventMotion *); + virtual bool clicked(GdkEventButton *); + + virtual void _setState(State state); + virtual Glib::ustring _getTip(unsigned state); + virtual Glib::ustring _getDragTip(GdkEventMotion *event); + virtual bool _hasDragTips() { return true; } +private: + Node(Node const &); + void _fixNeighbors(Geom::Point const &old_pos, Geom::Point const &new_pos); + void _updateAutoHandles(); + void _linearGrow(int dir); + Node *_next(); + Node *_prev(); + Inkscape::SnapSourceType _snapSourceType(); + Inkscape::SnapTargetType _snapTargetType(); + inline PathManipulator &_pm(); + static SPCtrlShapeType _node_type_to_shape(NodeType type); + static bool _is_line_segment(Node *first, Node *second); + + // Handles are always present, but are not visible if they coincide with the node + // (are degenerate). A segment that has both handles degenerate is always treated + // as a line segment + Handle _front; ///< Node handle in the backward direction of the path + Handle _back; ///< Node handle in the forward direction of the path + NodeType _type; ///< Type of node - cusp, smooth... + bool _handles_shown; + friend class Handle; + friend class NodeList; + friend class NodeIterator<Node>; + friend class NodeIterator<Node const>; +}; + +/// Iterator for editable nodes +/** Use this class for all operations that require some knowledge about the node's + * neighbors. It is a bidirectional iterator. + * + * Because paths can be cyclic, node iterators have two different ways to + * increment and decrement them. When using ++/--, the end iterator will eventually + * be returned. Whent using advance()/retreat(), the end iterator will only be returned + * when the path is open. If it's closed, calling advance() will cycle indefinitely. + * This is particularly useful for cases where the adjacency of nodes is more important + * than their sequence order. + * + * When @a i is a node iterator, then: + * - <code>++i</code> moves the iterator to the next node in sequence order; + * - <code>--i</code> moves the iterator to the previous node in sequence order; + * - <code>i.next()</code> returns the next node with wrap-around; + * - <code>i.prev()</code> returns the previous node with wrap-around; + * - <code>i.advance()</code> moves the iterator to the next node with wrap-around; + * - <code>i.retreat()</code> moves the iterator to the previous node with wrap-around. + * + * next() and prev() do not change their iterator. They can return the end iterator + * if the path is open. + * + * Unlike most other iterators, you can check whether you've reached the end of the list + * without having access to the iterator's container. + * Simply use <code>if (i) { ...</code> + * */ +template <typename N> +class NodeIterator + : public boost::bidirectional_iterator_helper<NodeIterator<N>, N, std::ptrdiff_t, + N *, N &> +{ +public: + typedef NodeIterator self; + NodeIterator() + : _node(0) + {} + // default copy, default assign + + self &operator++() { + _node = _node->ln_next; + return *this; + } + self &operator--() { + _node = _node->ln_prev; + return *this; + } + bool operator==(self const &other) const { return _node == other._node; } + N &operator*() const { return *static_cast<N*>(_node); } + inline operator bool() const; // define after NodeList + /// Get a pointer to the underlying node. Equivalent to <code>&*i</code>. + N *get_pointer() const { return static_cast<N*>(_node); } + /// @see get_pointer() + N *ptr() const { return static_cast<N*>(_node); } + + self next() const { + self r(*this); + r.advance(); + return r; + } + self prev() const { + self r(*this); + r.retreat(); + return r; + } + self &advance(); + self &retreat(); +private: + NodeIterator(ListNode const *n) + : _node(const_cast<ListNode*>(n)) + {} + ListNode *_node; + friend class NodeList; +}; + +class NodeList : ListNode, boost::noncopyable, public boost::enable_shared_from_this<NodeList> { +public: + typedef std::size_t size_type; + typedef Node &reference; + typedef Node const &const_reference; + typedef Node *pointer; + typedef Node const *const_pointer; + typedef Node value_type; + typedef NodeIterator<value_type> iterator; + typedef NodeIterator<value_type const> const_iterator; + + // TODO Lame. Make this private and make SubpathList a factory + NodeList(SubpathList &_list); + ~NodeList(); + + // iterators + iterator begin() { return iterator(ln_next); } + iterator end() { return iterator(this); } + const_iterator begin() const { return const_iterator(ln_next); } + const_iterator end() const { return const_iterator(this); } + + // size + bool empty(); + size_type size(); + + // extra node-specific methods + bool closed(); + bool degenerate(); + void setClosed(bool c) { _closed = c; } + iterator before(double t, double *fracpart = NULL); + const_iterator before(double t, double *fracpart = NULL) const { + return const_iterator(before(t, fracpart)._node); + } + + // list operations + iterator insert(iterator pos, Node *x); + template <class InputIterator> + void insert(iterator pos, InputIterator first, InputIterator last) { + for (; first != last; ++first) insert(pos, *first); + } + void splice(iterator pos, NodeList &list); + void splice(iterator pos, NodeList &list, iterator i); + void splice(iterator pos, NodeList &list, iterator first, iterator last); + void reverse(); + void shift(int n); + void push_front(Node *x) { insert(begin(), x); } + void pop_front() { erase(begin()); } + void push_back(Node *x) { insert(end(), x); } + void pop_back() { erase(--end()); } + void clear(); + iterator erase(iterator pos); + iterator erase(iterator first, iterator last) { + NodeList::iterator ret = first; + while (first != last) ret = erase(first++); + return ret; + } + + // member access - undefined results when the list is empty + Node &front() { return *static_cast<Node*>(ln_next); } + Node &back() { return *static_cast<Node*>(ln_prev); } + + // HACK remove this subpath from its path. This will be removed later. + void kill(); + SubpathList &subpathList() { return _list; } + + static iterator get_iterator(Node *n) { return iterator(n); } + static const_iterator get_iterator(Node const *n) { return const_iterator(n); } + static NodeList &get(Node *n); + static NodeList &get(iterator const &i); +private: + // no copy or assign + NodeList(NodeList const &); + void operator=(NodeList const &); + + SubpathList &_list; + bool _closed; + + friend class Node; + friend class Handle; // required to access handle and handle line groups + friend class NodeIterator<Node>; + friend class NodeIterator<Node const>; +}; + +/** List of node lists. Represents an editable path. */ +class SubpathList : public std::list< boost::shared_ptr<NodeList> > { +public: + typedef std::list< boost::shared_ptr<NodeList> > list_type; + + SubpathList(PathManipulator &pm) : _path_manipulator(pm) {} + PathManipulator &pm() { return _path_manipulator; } + +private: + list_type _nodelists; + PathManipulator &_path_manipulator; + friend class NodeList; + friend class Node; + friend class Handle; +}; + + + +// define inline Handle funcs after definition of Node +inline Geom::Point Handle::relativePos() { + return position() - _parent->position(); +} +inline void Handle::setRelativePos(Geom::Point const &p) { + setPosition(_parent->position() + p); +} +inline double Handle::length() { + return relativePos().length(); +} +inline PathManipulator &Handle::_pm() { + return _parent->_pm(); +} +inline PathManipulator &Node::_pm() { + return nodeList().subpathList().pm(); +} + +// definitions for node iterator +template <typename N> +NodeIterator<N>::operator bool() const { + return _node && static_cast<ListNode*>(_node->ln_list) != _node; +} +template <typename N> +NodeIterator<N> &NodeIterator<N>::advance() { + ++(*this); + if (G_UNLIKELY(!*this) && _node->ln_list->closed()) ++(*this); + return *this; +} +template <typename N> +NodeIterator<N> &NodeIterator<N>::retreat() { + --(*this); + if (G_UNLIKELY(!*this) && _node->ln_list->closed()) --(*this); + return *this; +} + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp new file mode 100644 index 000000000..66f72f379 --- /dev/null +++ b/src/ui/tool/path-manipulator.cpp @@ -0,0 +1,1462 @@ +/** @file + * Path manipulator - implementation + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <string> +#include <sstream> +#include <deque> +#include <stdexcept> +#include <boost/shared_ptr.hpp> +#include <2geom/bezier-curve.h> +#include <2geom/bezier-utils.h> +#include <2geom/svg-path.h> +#include <glibmm.h> +#include <glibmm/i18n.h> +#include "ui/tool/path-manipulator.h" +#include "desktop.h" +#include "desktop-handles.h" +#include "display/sp-canvas.h" +#include "display/sp-canvas-util.h" +#include "display/curve.h" +#include "display/canvas-bpath.h" +#include "document.h" +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "live_effects/parameter/path.h" +#include "sp-path.h" +#include "helper/geom.h" +#include "preferences.h" +#include "style.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/curve-drag-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "xml/node.h" +#include "xml/node-observer.h" + +namespace Inkscape { +namespace UI { + +namespace { +/// Types of path changes that we must react to. +enum PathChange { + PATH_CHANGE_D, + PATH_CHANGE_TRANSFORM +}; + +} // anonymous namespace + +/** + * Notifies the path manipulator when something changes the path being edited + * (e.g. undo / redo) + */ +class PathManipulatorObserver : public Inkscape::XML::NodeObserver { +public: + PathManipulatorObserver(PathManipulator *p, Inkscape::XML::Node *node) + : _pm(p) + , _node(node) + , _blocked(false) + { + Inkscape::GC::anchor(_node); + _node->addObserver(*this); + } + + ~PathManipulatorObserver() { + _node->removeObserver(*this); + Inkscape::GC::release(_node); + } + + virtual void notifyAttributeChanged(Inkscape::XML::Node &/*node*/, GQuark attr, + Util::ptr_shared<char>, Util::ptr_shared<char>) + { + // do nothing if blocked + if (_blocked) return; + + GQuark path_d = g_quark_from_static_string("d"); + GQuark path_transform = g_quark_from_static_string("transform"); + GQuark lpe_quark = _pm->_lpe_key.empty() ? 0 : g_quark_from_string(_pm->_lpe_key.data()); + + // only react to "d" (path data) and "transform" attribute changes + if (attr == lpe_quark || attr == path_d) { + _pm->_externalChange(PATH_CHANGE_D); + } else if (attr == path_transform) { + _pm->_externalChange(PATH_CHANGE_TRANSFORM); + } + } + + void block() { _blocked = true; } + void unblock() { _blocked = false; } +private: + PathManipulator *_pm; + Inkscape::XML::Node *_node; + bool _blocked; +}; + +void build_segment(Geom::PathBuilder &, Node *, Node *); + +PathManipulator::PathManipulator(MultiPathManipulator &mpm, SPPath *path, + Geom::Matrix const &et, guint32 outline_color, Glib::ustring lpe_key) + : PointManipulator(mpm._path_data.node_data.desktop, *mpm._path_data.node_data.selection) + , _subpaths(*this) + , _multi_path_manipulator(mpm) + , _path(path) + , _spcurve(new SPCurve()) + , _dragpoint(new CurveDragPoint(*this)) + , _observer(new PathManipulatorObserver(this, SP_OBJECT(path)->repr)) + , _edit_transform(et) + , _num_selected(0) + , _show_handles(true) + , _show_outline(false) + , _show_path_direction(false) + , _live_outline(true) + , _live_objects(true) + , _lpe_key(lpe_key) +{ + if (_lpe_key.empty()) { + _i2d_transform = sp_item_i2d_affine(SP_ITEM(path)); + } else { + _i2d_transform = Geom::identity(); + } + _d2i_transform = _i2d_transform.inverse(); + _dragpoint->setVisible(false); + + _getGeometry(); + + _outline = sp_canvas_bpath_new(_multi_path_manipulator._path_data.outline_group, NULL); + sp_canvas_item_hide(_outline); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(_outline), outline_color, 1.0, + SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(_outline), 0, SP_WIND_RULE_NONZERO); + + _selection.signal_update.connect( + sigc::mem_fun(*this, &PathManipulator::update)); + _selection.signal_point_changed.connect( + sigc::mem_fun(*this, &PathManipulator::_selectionChanged)); + _desktop->signal_zoom_changed.connect( + sigc::hide( sigc::mem_fun(*this, &PathManipulator::_updateOutlineOnZoomChange))); + + _createControlPointsFromGeometry(); +} + +PathManipulator::~PathManipulator() +{ + delete _dragpoint; + delete _observer; + gtk_object_destroy(_outline); + _spcurve->unref(); + clear(); +} + +/** Handle motion events to update the position of the curve drag point. */ +bool PathManipulator::event(GdkEvent *event) +{ + if (empty()) return false; + + switch (event->type) + { + case GDK_MOTION_NOTIFY: + _updateDragPoint(event_point(event->motion)); + break; + default: break; + } + return false; +} + +/** Check whether the manipulator has any nodes. */ +bool PathManipulator::empty() { + return !_path || _subpaths.empty(); +} + +/** Update the display and the outline of the path. */ +void PathManipulator::update() +{ + _createGeometryFromControlPoints(); +} + +/** Store the changes to the path in XML. */ +void PathManipulator::writeXML() +{ + if (!_live_outline) + _updateOutline(); + if (!_live_objects) + _setGeometry(); + + if (!_path) return; + _observer->block(); + if (!empty()) { + SP_OBJECT(_path)->updateRepr(); + _getXMLNode()->setAttribute(_nodetypesKey().data(), _createTypeString().data()); + } else { + // this manipulator will have to be destroyed right after this call + _getXMLNode()->removeObserver(*_observer); + sp_object_ref(_path); + _path->deleteObject(true, true); + sp_object_unref(_path); + _path = 0; + } + _observer->unblock(); +} + +/** Remove all nodes from the path. */ +void PathManipulator::clear() +{ + // no longer necessary since nodes remove themselves from selection on destruction + //_removeNodesFromSelection(); + _subpaths.clear(); +} + +/** Select all nodes in subpaths that have something selected. */ +void PathManipulator::selectSubpaths() +{ + for (std::list<SubpathPtr>::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + NodeList::iterator sp_start = (*i)->begin(), sp_end = (*i)->end(); + for (NodeList::iterator j = sp_start; j != sp_end; ++j) { + if (j->selected()) { + // if at least one of the nodes from this subpath is selected, + // select all nodes from this subpath + for (NodeList::iterator ins = sp_start; ins != sp_end; ++ins) + _selection.insert(ins.ptr()); + continue; + } + } + } +} + +/** Move the selection forward or backward by one node in each subpath, based on the sign + * of the parameter. */ +void PathManipulator::shiftSelection(int dir) +{ + if (dir == 0) return; + if (_num_selected == 0) { + // select the first node of the path. + SubpathList::iterator s = _subpaths.begin(); + if (s == _subpaths.end()) return; + NodeList::iterator n = (*s)->begin(); + if (n != (*s)->end()) + _selection.insert(n.ptr()); + return; + } + // We cannot do any tricks here, like iterating in different directions based on + // the sign and only setting the selection of nodes behind us, because it would break + // for closed paths. + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + std::deque<bool> sels; // I hope this is specialized for bools! + unsigned num = 0; + + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + sels.push_back(j->selected()); + _selection.erase(j.ptr()); + ++num; + } + if (num == 0) continue; // should never happen! zero-node subpaths are not allowed + + num = 0; + // In closed subpath, shift the selection cyclically. In an open one, + // let the selection 'slide into nothing' at ends. + if (dir > 0) { + if ((*i)->closed()) { + bool last = sels.back(); + sels.pop_back(); + sels.push_front(last); + } else { + sels.push_front(false); + } + } else { + if ((*i)->closed()) { + bool first = sels.front(); + sels.pop_front(); + sels.push_back(first); + } else { + sels.push_back(false); + num = 1; + } + } + + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + if (sels[num]) _selection.insert(j.ptr()); + ++num; + } + } +} + +/** Invert selection in the selected subpaths. */ +void PathManipulator::invertSelectionInSubpaths() +{ + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + if (j->selected()) { + // found selected node - invert selection in this subpath + for (NodeList::iterator k = (*i)->begin(); k != (*i)->end(); ++k) { + if (k->selected()) _selection.erase(k.ptr()); + else _selection.insert(k.ptr()); + } + // next subpath + break; + } + } + } +} + +/** Insert a new node in the middle of each selected segment. */ +void PathManipulator::insertNodes() +{ + if (_num_selected < 2) return; + + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + NodeList::iterator k = j.next(); + if (k && j->selected() && k->selected()) { + j = subdivideSegment(j, 0.5); + _selection.insert(j.ptr()); + } + } + } +} + +/** Replace contiguous selections of nodes in each subpath with one node. */ +void PathManipulator::weldNodes(NodeList::iterator preserve_pos) +{ + if (_num_selected < 2) return; + hideDragPoint(); + + bool pos_valid = preserve_pos; + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + SubpathPtr sp = *i; + unsigned num_selected = 0, num_unselected = 0; + for (NodeList::iterator j = sp->begin(); j != sp->end(); ++j) { + if (j->selected()) ++num_selected; + else ++num_unselected; + } + if (num_selected < 2) continue; + if (num_unselected == 0) { + // if all nodes in a subpath are selected, the operation doesn't make much sense + continue; + } + + // Start from unselected node in closed paths, so that we don't start in the middle + // of a selection + NodeList::iterator sel_beg = sp->begin(), sel_end; + if (sp->closed()) { + while (sel_beg->selected()) ++sel_beg; + } + + // Work loop + while (num_selected > 0) { + // Find selected node + while (sel_beg && !sel_beg->selected()) sel_beg = sel_beg.next(); + if (!sel_beg) throw std::logic_error("Join nodes: end of open path reached, " + "but there are still nodes to process!"); + + // note: this is initialized to zero, because the loop below counts sel_beg as well + // the loop conditions are simpler that way + unsigned num_points = 0; + bool use_pos = false; + Geom::Point back_pos, front_pos; + back_pos = *sel_beg->back(); + + for (sel_end = sel_beg; sel_end && sel_end->selected(); sel_end = sel_end.next()) { + ++num_points; + front_pos = *sel_end->front(); + if (pos_valid && sel_end == preserve_pos) use_pos = true; + } + if (num_points > 1) { + Geom::Point joined_pos; + if (use_pos) { + joined_pos = preserve_pos->position(); + pos_valid = false; + } else { + joined_pos = Geom::middle_point(back_pos, front_pos); + } + sel_beg->setType(NODE_CUSP, false); + sel_beg->move(joined_pos); + // do not move handles if they aren't degenerate + if (!sel_beg->back()->isDegenerate()) { + sel_beg->back()->setPosition(back_pos); + } + if (!sel_end.prev()->front()->isDegenerate()) { + sel_beg->front()->setPosition(front_pos); + } + sel_beg = sel_beg.next(); + while (sel_beg != sel_end) { + NodeList::iterator next = sel_beg.next(); + sp->erase(sel_beg); + sel_beg = next; + --num_selected; + } + } + --num_selected; // for the joined node or single selected node + } + } +} + +/** Remove nodes in the middle of selected segments. */ +void PathManipulator::weldSegments() +{ + if (_num_selected < 2) return; + hideDragPoint(); + + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + SubpathPtr sp = *i; + unsigned num_selected = 0, num_unselected = 0; + for (NodeList::iterator j = sp->begin(); j != sp->end(); ++j) { + if (j->selected()) ++num_selected; + else ++num_unselected; + } + if (num_selected < 3) continue; + if (num_unselected == 0 && sp->closed()) { + // if all nodes in a closed subpath are selected, the operation doesn't make much sense + continue; + } + + // Start from unselected node in closed paths, so that we don't start in the middle + // of a selection + NodeList::iterator sel_beg = sp->begin(), sel_end; + if (sp->closed()) { + while (sel_beg->selected()) ++sel_beg; + } + + // Work loop + while (num_selected > 0) { + // Find selected node + while (sel_beg && !sel_beg->selected()) sel_beg = sel_beg.next(); + if (!sel_beg) throw std::logic_error("Join nodes: end of open path reached, " + "but there are still nodes to process!"); + + // note: this is initialized to zero, because the loop below counts sel_beg as well + // the loop conditions are simpler that way + unsigned num_points = 0; + + // find the end of selected segment + for (sel_end = sel_beg; sel_end && sel_end->selected(); sel_end = sel_end.next()) { + ++num_points; + } + if (num_points > 2) { + // remove nodes in the middle + sel_beg = sel_beg.next(); + while (sel_beg != sel_end.prev()) { + NodeList::iterator next = sel_beg.next(); + sp->erase(sel_beg); + sel_beg = next; + } + sel_beg = sel_end; + } + num_selected -= num_points; + } + } +} + +/** Break the subpath at selected nodes. It also works for single node closed paths. */ +void PathManipulator::breakNodes() +{ + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + SubpathPtr sp = *i; + NodeList::iterator cur = sp->begin(), end = sp->end(); + if (!sp->closed()) { + // Each open path must have at least two nodes so no checks are required. + // For 2-node open paths, cur == end + ++cur; + --end; + } + for (; cur != end; ++cur) { + if (!cur->selected()) continue; + SubpathPtr ins; + bool becomes_open = false; + + if (sp->closed()) { + // Move the node to break at to the beginning of path + if (cur != sp->begin()) + sp->splice(sp->begin(), *sp, cur, sp->end()); + sp->setClosed(false); + ins = sp; + becomes_open = true; + } else { + SubpathPtr new_sp(new NodeList(_subpaths)); + new_sp->splice(new_sp->end(), *sp, sp->begin(), cur); + _subpaths.insert(i, new_sp); + ins = new_sp; + } + + Node *n = new Node(_multi_path_manipulator._path_data.node_data, cur->position()); + ins->insert(ins->end(), n); + cur->setType(NODE_CUSP, false); + n->back()->setRelativePos(cur->back()->relativePos()); + cur->back()->retract(); + n->sink(); + + if (becomes_open) { + cur = sp->begin(); // this will be increased to ++sp->begin() + end = --sp->end(); + } + } + } +} + +/** Delete selected nodes in the path, optionally substituting deleted segments with bezier curves + * in a way that attempts to preserve the original shape of the curve. */ +void PathManipulator::deleteNodes(bool keep_shape) +{ + if (_num_selected == 0) return; + hideDragPoint(); + + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) { + SubpathPtr sp = *i; + + // If there are less than 2 unselected nodes in an open subpath or no unselected nodes + // in a closed one, delete entire subpath. + unsigned num_unselected = 0, num_selected = 0; + for (NodeList::iterator j = sp->begin(); j != sp->end(); ++j) { + if (j->selected()) ++num_selected; + else ++num_unselected; + } + if (num_selected == 0) { + ++i; + continue; + } + if (sp->closed() ? (num_unselected < 1) : (num_unselected < 2)) { + _subpaths.erase(i++); + continue; + } + + // In closed paths, start from an unselected node - otherwise we might start in the middle + // of a selected stretch and the resulting bezier fit would be suboptimal + NodeList::iterator sel_beg = sp->begin(), sel_end; + if (sp->closed()) { + while (sel_beg->selected()) ++sel_beg; + } + sel_end = sel_beg; + + while (num_selected > 0) { + while (sel_beg && !sel_beg->selected()) { + sel_beg = sel_beg.next(); + } + sel_end = sel_beg; + + while (sel_end && sel_end->selected()) { + sel_end = sel_end.next(); + } + + num_selected -= _deleteStretch(sel_beg, sel_end, keep_shape); + sel_beg = sel_end; + } + ++i; + } +} + +/** @brief Delete nodes between the two iterators. + * The given range can cross the beginning of the subpath in closed subpaths. + * @param start Beginning of the range to delete + * @param end End of the range + * @param keep_shape Whether to fit the handles at surrounding nodes to approximate + * the shape before deletion + * @return Number of deleted nodes */ +unsigned PathManipulator::_deleteStretch(NodeList::iterator start, NodeList::iterator end, bool keep_shape) +{ + unsigned const samples_per_segment = 10; + double const t_step = 1.0 / samples_per_segment; + + unsigned del_len = 0; + for (NodeList::iterator i = start; i != end; i = i.next()) { + ++del_len; + } + if (del_len == 0) return 0; + + // set surrounding node types to cusp if: + // 1. keep_shape is on, or + // 2. we are deleting at the end or beginning of an open path + if ((keep_shape || !end) && start.prev()) start.prev()->setType(NODE_CUSP, false); + if ((keep_shape || !start.prev()) && end) end->setType(NODE_CUSP, false); + + if (keep_shape && start.prev() && end) { + unsigned num_samples = (del_len + 1) * samples_per_segment + 1; + Geom::Point *bezier_data = new Geom::Point[num_samples]; + Geom::Point result[4]; + unsigned seg = 0; + + for (NodeList::iterator cur = start.prev(); cur != end; cur = cur.next()) { + Geom::CubicBezier bc(*cur, *cur->front(), *cur.next(), *cur.next()->back()); + for (unsigned s = 0; s < samples_per_segment; ++s) { + bezier_data[seg * samples_per_segment + s] = bc.pointAt(t_step * s); + } + ++seg; + } + // Fill last point + bezier_data[num_samples - 1] = end->position(); + // Compute replacement bezier curve + // TODO the fitting algorithm sucks - rewrite it to be awesome + bezier_fit_cubic(result, bezier_data, num_samples, 0.5); + delete[] bezier_data; + + start.prev()->front()->setPosition(result[1]); + end->back()->setPosition(result[2]); + } + + // We can't use nl->erase(start, end), because it would break when the stretch + // crosses the beginning of a closed subpath + NodeList &nl = start->nodeList(); + while (start != end) { + NodeList::iterator next = start.next(); + nl.erase(start); + start = next; + } + + return del_len; +} + +/** Removes selected segments */ +void PathManipulator::deleteSegments() +{ + if (_num_selected == 0) return; + hideDragPoint(); + + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) { + SubpathPtr sp = *i; + bool has_unselected = false; + unsigned num_selected = 0; + for (NodeList::iterator j = sp->begin(); j != sp->end(); ++j) { + if (j->selected()) { + ++num_selected; + } else { + has_unselected = true; + } + } + if (!has_unselected) { + _subpaths.erase(i++); + continue; + } + + NodeList::iterator sel_beg = sp->begin(); + if (sp->closed()) { + while (sel_beg && sel_beg->selected()) ++sel_beg; + } + while (num_selected > 0) { + if (!sel_beg->selected()) { + sel_beg = sel_beg.next(); + continue; + } + NodeList::iterator sel_end = sel_beg; + unsigned num_points = 0; + while (sel_end && sel_end->selected()) { + sel_end = sel_end.next(); + ++num_points; + } + if (num_points >= 2) { + // Retract end handles + sel_end.prev()->setType(NODE_CUSP, false); + sel_end.prev()->back()->retract(); + sel_beg->setType(NODE_CUSP, false); + sel_beg->front()->retract(); + if (sp->closed()) { + // In closed paths, relocate the beginning of the path to the last selected + // node and then unclose it. Remove the nodes from the first selected node + // to the new end of path. + if (sel_end.prev() != sp->begin()) + sp->splice(sp->begin(), *sp, sel_end.prev(), sp->end()); + sp->setClosed(false); + sp->erase(sel_beg.next(), sp->end()); + } else { + // for open paths: + // 1. At end or beginning, delete including the node on the end or beginning + // 2. In the middle, delete only inner nodes + if (sel_beg == sp->begin()) { + sp->erase(sp->begin(), sel_end.prev()); + } else if (sel_end == sp->end()) { + sp->erase(sel_beg.next(), sp->end()); + } else { + SubpathPtr new_sp(new NodeList(_subpaths)); + new_sp->splice(new_sp->end(), *sp, sp->begin(), sel_beg.next()); + _subpaths.insert(i, new_sp); + if (sel_end.prev()) + sp->erase(sp->begin(), sel_end.prev()); + } + } + } + sel_beg = sel_end; + num_selected -= num_points; + } + ++i; + } +} + +/** Reverse subpaths of the path. + * @param selected_only If true, only paths that have at least one selected node + * will be reversed. Otherwise all subpaths will be reversed. */ +void PathManipulator::reverseSubpaths(bool selected_only) +{ + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + if (selected_only) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + if (j->selected()) { + (*i)->reverse(); + break; // continue with the next subpath + } + } + } else { + (*i)->reverse(); + } + } +} + +/** Make selected segments curves / lines. */ +void PathManipulator::setSegmentType(SegmentType type) +{ + if (_num_selected == 0) return; + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + NodeList::iterator k = j.next(); + if (!(k && j->selected() && k->selected())) continue; + switch (type) { + case SEGMENT_STRAIGHT: + if (j->front()->isDegenerate() && k->back()->isDegenerate()) + break; + j->front()->move(*j); + k->back()->move(*k); + break; + case SEGMENT_CUBIC_BEZIER: + if (!j->front()->isDegenerate() || !k->back()->isDegenerate()) + break; + // move both handles to 1/3 of the line + j->front()->move(j->position() + (k->position() - j->position()) / 3); + k->back()->move(k->position() + (j->position() - k->position()) / 3); + break; + } + } + } +} + +void PathManipulator::scaleHandle(Node *n, int which, int dir, bool pixel) +{ + if (n->type() == NODE_SYMMETRIC || n->type() == NODE_AUTO) { + n->setType(NODE_SMOOTH); + } + Handle *h = _chooseHandle(n, which); + double length_change; + + if (pixel) { + length_change = 1.0 / _desktop->current_zoom() * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000); + length_change *= dir; + } + + Geom::Point relpos; + if (h->isDegenerate()) { + if (dir < 0) return; + Node *nh = n->nodeToward(h); + if (!nh) return; + relpos = Geom::unit_vector(nh->position() - n->position()) * length_change; + } else { + relpos = h->relativePos(); + double rellen = relpos.length(); + relpos *= ((rellen + length_change) / rellen); + } + h->setRelativePos(relpos); + update(); + + gchar const *key = which < 0 ? "handle:scale:left" : "handle:scale:right"; + _commit(_("Scale handle"), key); +} + +void PathManipulator::rotateHandle(Node *n, int which, int dir, bool pixel) +{ + if (n->type() != NODE_CUSP) { + n->setType(NODE_CUSP); + } + Handle *h = _chooseHandle(n, which); + if (h->isDegenerate()) return; + + double angle; + if (pixel) { + // Rotate by "one pixel" + angle = atan2(1.0 / _desktop->current_zoom(), h->length()) * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + angle = M_PI * dir / snaps; + } + + h->setRelativePos(h->relativePos() * Geom::Rotate(angle)); + update(); + gchar const *key = which < 0 ? "handle:rotate:left" : "handle:rotate:right"; + _commit(_("Rotate handle"), key); +} + +Handle *PathManipulator::_chooseHandle(Node *n, int which) +{ + NodeList::iterator i = NodeList::get_iterator(n); + Node *prev = i.prev().ptr(); + Node *next = i.next().ptr(); + + // on an endnode, the remaining handle automatically wins + if (!next) return n->back(); + if (!prev) return n->front(); + + // compare X coord ofline segments + Geom::Point npos = next->position(); + Geom::Point ppos = prev->position(); + if (which < 0) { + // pick left handle. + // we just swap the handles and pick the right handle below. + std::swap(npos, ppos); + } + + if (npos[Geom::X] >= ppos[Geom::X]) { + return n->front(); + } else { + return n->back(); + } +} + +/** Set the visibility of handles. */ +void PathManipulator::showHandles(bool show) +{ + if (show == _show_handles) return; + if (show) { + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + if (!j->selected()) continue; + j->showHandles(true); + if (j.prev()) j.prev()->showHandles(true); + if (j.next()) j.next()->showHandles(true); + } + } + } else { + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + j->showHandles(false); + } + } + } + _show_handles = show; +} + +/** Set the visibility of outline. */ +void PathManipulator::showOutline(bool show) +{ + if (show == _show_outline) return; + _show_outline = show; + _updateOutline(); +} + +void PathManipulator::showPathDirection(bool show) +{ + if (show == _show_path_direction) return; + _show_path_direction = show; + _updateOutline(); +} + +void PathManipulator::setLiveOutline(bool set) +{ + _live_outline = set; +} + +void PathManipulator::setLiveObjects(bool set) +{ + _live_objects = set; +} + +void PathManipulator::setControlsTransform(Geom::Matrix const &tnew) +{ + Geom::Matrix delta = _i2d_transform.inverse() * _edit_transform.inverse() * tnew * _i2d_transform; + _edit_transform = tnew; + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + j->transform(delta); + } + } + _createGeometryFromControlPoints(); +} + +/** Hide the curve drag point until the next motion event. + * This should be called at the beginning of every method that can delete nodes. + * Otherwise the invalidated iterator in the dragpoint can cause crashes. */ +void PathManipulator::hideDragPoint() +{ + _dragpoint->setVisible(false); + _dragpoint->setIterator(NodeList::iterator()); +} + +/** Insert a node in the segment beginning with the supplied iterator, + * at the given time value */ +NodeList::iterator PathManipulator::subdivideSegment(NodeList::iterator first, double t) +{ + if (!first) throw std::invalid_argument("Subdivide after invalid iterator"); + NodeList &list = NodeList::get(first); + NodeList::iterator second = first.next(); + if (!second) throw std::invalid_argument("Subdivide after last node in open path"); + + // We need to insert the segment after 'first'. We can't simply use 'second' + // as the point of insertion, because when 'first' is the last node of closed path, + // the new node will be inserted as the first node instead. + NodeList::iterator insert_at = first; + ++insert_at; + + NodeList::iterator inserted; + if (first->front()->isDegenerate() && second->back()->isDegenerate()) { + // for a line segment, insert a cusp node + Node *n = new Node(_multi_path_manipulator._path_data.node_data, + Geom::lerp(t, first->position(), second->position())); + n->setType(NODE_CUSP, false); + inserted = list.insert(insert_at, n); + } else { + // build bezier curve and subdivide + Geom::CubicBezier temp(first->position(), first->front()->position(), + second->back()->position(), second->position()); + std::pair<Geom::CubicBezier, Geom::CubicBezier> div = temp.subdivide(t); + std::vector<Geom::Point> seg1 = div.first.points(), seg2 = div.second.points(); + + // set new handle positions + Node *n = new Node(_multi_path_manipulator._path_data.node_data, seg2[0]); + n->back()->setPosition(seg1[2]); + n->front()->setPosition(seg2[1]); + n->setType(NODE_SMOOTH, false); + inserted = list.insert(insert_at, n); + + first->front()->move(seg1[1]); + second->back()->move(seg2[2]); + } + return inserted; +} + +/** Find the node that is closest/farthest from the origin + * @param origin Point of reference + * @param search_selected Consider selected nodes + * @param search_unselected Consider unselected nodes + * @param closest If true, return closest node, if false, return farthest + * @return The matching node, or an empty iterator if none found + */ +NodeList::iterator PathManipulator::extremeNode(NodeList::iterator origin, bool search_selected, + bool search_unselected, bool closest) +{ + NodeList::iterator match; + double extr_dist = closest ? HUGE_VAL : -HUGE_VAL; + if (_num_selected == 0 && !search_unselected) return match; + + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + if(j->selected()) { + if (!search_selected) continue; + } else { + if (!search_unselected) continue; + } + double dist = Geom::distance(*j, *origin); + bool cond = closest ? (dist < extr_dist) : (dist > extr_dist); + if (cond) { + match = j; + extr_dist = dist; + } + } + } + return match; +} + +/** Called by the XML observer when something else than us modifies the path. */ +void PathManipulator::_externalChange(unsigned type) +{ + switch (type) { + case PATH_CHANGE_D: { + _getGeometry(); + + // ugly: stored offsets of selected nodes in a vector + // vector<bool> should be specialized so that it takes only 1 bit per value + std::vector<bool> selpos; + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + selpos.push_back(j->selected()); + } + } + unsigned size = selpos.size(), curpos = 0; + + _createControlPointsFromGeometry(); + + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + if (curpos >= size) goto end_restore; + if (selpos[curpos]) _selection.insert(j.ptr()); + ++curpos; + } + } + end_restore: + + _updateOutline(); + } break; + case PATH_CHANGE_TRANSFORM: { + Geom::Matrix i2d_change = _d2i_transform; + _i2d_transform = sp_item_i2d_affine(SP_ITEM(_path)); + _d2i_transform = _i2d_transform.inverse(); + i2d_change *= _i2d_transform; + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + j->transform(i2d_change); + } + } + _updateOutline(); + } break; + default: break; + } +} + +/** Create nodes and handles based on the XML of the edited path. */ +void PathManipulator::_createControlPointsFromGeometry() +{ + clear(); + + // sanitize pathvector and store it in SPCurve, + // so that _updateDragPoint doesn't crash on paths with naked movetos + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(_spcurve->get_pathvector()); + for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) { + // NOTE: this utilizes the fact that Geom::PathVector is an std::vector. + // When we erase an element, the next one slides into position, + // so we do not increment the iterator even though it is theoretically invalidated. + if (i->empty()) { + pathv.erase(i); + } else { + ++i; + } + } + _spcurve->set_pathvector(pathv); + + pathv *= (_edit_transform * _i2d_transform); + + // in this loop, we know that there are no zero-segment subpaths + for (Geom::PathVector::const_iterator pit = pathv.begin(); pit != pathv.end(); ++pit) { + // prepare new subpath + SubpathPtr subpath(new NodeList(_subpaths)); + _subpaths.push_back(subpath); + + Node *previous_node = new Node(_multi_path_manipulator._path_data.node_data, pit->initialPoint()); + subpath->push_back(previous_node); + Geom::Curve const &cseg = pit->back_closed(); + bool fuse_ends = pit->closed() + && Geom::are_near(cseg.initialPoint(), cseg.finalPoint()); + + for (Geom::Path::const_iterator cit = pit->begin(); cit != pit->end_open(); ++cit) { + Geom::Point pos = cit->finalPoint(); + Node *current_node; + // if the closing segment is degenerate and the path is closed, we need to move + // the handle of the first node instead of creating a new one + if (fuse_ends && cit == --(pit->end_open())) { + current_node = subpath->begin().get_pointer(); + } else { + /* regardless of segment type, create a new node at the end + * of this segment (unless this is the last segment of a closed path + * with a degenerate closing segment */ + current_node = new Node(_multi_path_manipulator._path_data.node_data, pos); + subpath->push_back(current_node); + } + // if this is a bezier segment, move handles appropriately + if (Geom::CubicBezier const *cubic_bezier = + dynamic_cast<Geom::CubicBezier const*>(&*cit)) + { + std::vector<Geom::Point> points = cubic_bezier->points(); + + previous_node->front()->setPosition(points[1]); + current_node ->back() ->setPosition(points[2]); + } + previous_node = current_node; + } + // If the path is closed, make the list cyclic + if (pit->closed()) subpath->setClosed(true); + } + + // we need to set the nodetypes after all the handles are in place, + // so that pickBestType works correctly + // TODO maybe migrate to inkscape:node-types? + // TODO move this into SPPath - do not manipulate directly + gchar const *nts_raw = _path ? _path->repr->attribute(_nodetypesKey().data()) : 0; + std::string nodetype_string = nts_raw ? nts_raw : ""; + /* Calculate the needed length of the nodetype string. + * For closed paths, the entry is duplicated for the starting node, + * so we can just use the count of segments including the closing one + * to include the extra end node. */ + std::string::size_type nodetype_len = 0; + for (Geom::PathVector::const_iterator i = pathv.begin(); i != pathv.end(); ++i) { + if (i->empty()) continue; + nodetype_len += i->size_closed(); + } + /* pad the string to required length with a bogus value. + * 'b' and any other letter not recognized by the parser causes the best fit to be set + * as the node type */ + if (nodetype_len > nodetype_string.size()) { + nodetype_string.append(nodetype_len - nodetype_string.size(), 'b'); + } + std::string::iterator tsi = nodetype_string.begin(); + for (std::list<SubpathPtr>::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + j->setType(Node::parse_nodetype(*tsi++), false); + } + if ((*i)->closed()) { + // STUPIDITY ALERT: it seems we need to use the duplicate type symbol instead of + // the first one to remain backward compatible. + (*i)->begin()->setType(Node::parse_nodetype(*tsi++), false); + } + } +} + +/** Construct the geometric representation of nodes and handles, update the outline + * and display */ +void PathManipulator::_createGeometryFromControlPoints() +{ + Geom::PathBuilder builder; + for (std::list<SubpathPtr>::iterator spi = _subpaths.begin(); spi != _subpaths.end(); ) { + SubpathPtr subpath = *spi; + if (subpath->empty()) { + _subpaths.erase(spi++); + continue; + } + NodeList::iterator prev = subpath->begin(); + builder.moveTo(prev->position()); + + for (NodeList::iterator i = ++subpath->begin(); i != subpath->end(); ++i) { + build_segment(builder, prev.ptr(), i.ptr()); + prev = i; + } + if (subpath->closed()) { + // Here we link the last and first node if the path is closed. + // If the last segment is Bezier, we add it. + if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()) { + build_segment(builder, prev.ptr(), subpath->begin().ptr()); + } + // if that segment is linear, we just call closePath(). + builder.closePath(); + } + ++spi; + } + builder.finish(); + _spcurve->set_pathvector(builder.peek() * (_edit_transform * _i2d_transform).inverse()); + if (_live_outline) + _updateOutline(); + if (_live_objects) + _setGeometry(); +} + +/** Build one segment of the geometric representation. + * @relates PathManipulator */ +void build_segment(Geom::PathBuilder &builder, Node *prev_node, Node *cur_node) +{ + if (cur_node->back()->isDegenerate() && prev_node->front()->isDegenerate()) + { + // NOTE: It seems like the renderer cannot correctly handle vline / hline segments, + // and trying to display a path using them results in funny artifacts. + builder.lineTo(cur_node->position()); + } else { + // this is a bezier segment + builder.curveTo( + prev_node->front()->position(), + cur_node->back()->position(), + cur_node->position()); + } +} + +/** Construct a node type string to store in the sodipodi:nodetypes attribute. */ +std::string PathManipulator::_createTypeString() +{ + // precondition: no single-node subpaths + std::stringstream tstr; + for (std::list<SubpathPtr>::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + tstr << j->type(); + } + // nodestring format peculiarity: first node is counted twice for closed paths + if ((*i)->closed()) tstr << (*i)->begin()->type(); + } + return tstr.str(); +} + +/** Update the path outline. */ +void PathManipulator::_updateOutline() +{ + if (!_show_outline) { + sp_canvas_item_hide(_outline); + return; + } + + Geom::PathVector pv = _spcurve->get_pathvector(); + pv *= (_edit_transform * _i2d_transform); + // This SPCurve thing has to be killed with extreme prejudice + SPCurve *_hc = new SPCurve(); + if (_show_path_direction) { + // To show the direction, we append additional subpaths which consist of a single + // linear segment that starts at the time value of 0.5 and extends for 10 pixels + // at an angle 150 degrees from the unit tangent. This creates the appearance + // of little 'harpoons' that show the direction of the subpaths. + Geom::PathVector arrows; + for (Geom::PathVector::iterator i = pv.begin(); i != pv.end(); ++i) { + Geom::Path &path = *i; + for (Geom::Path::const_iterator j = path.begin(); j != path.end_default(); ++j) { + Geom::Point at = j->pointAt(0.5); + Geom::Point ut = j->unitTangentAt(0.5); + // rotate the point + ut *= Geom::Rotate(150.0 / 180.0 * M_PI); + Geom::Point arrow_end = _desktop->w2d( + _desktop->d2w(at) + Geom::unit_vector(_desktop->d2w(ut)) * 10.0); + + Geom::Path arrow(at); + arrow.appendNew<Geom::LineSegment>(arrow_end); + arrows.push_back(arrow); + } + } + pv.insert(pv.end(), arrows.begin(), arrows.end()); + } + _hc->set_pathvector(pv); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(_outline), _hc); + sp_canvas_item_show(_outline); + _hc->unref(); +} + +/** Retrieve the geometry of the edited object from the object tree */ +void PathManipulator::_getGeometry() +{ + using namespace Inkscape::LivePathEffect; + if (!_lpe_key.empty()) { + Effect *lpe = LIVEPATHEFFECT(_path)->get_lpe(); + if (lpe) { + PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data())); + _spcurve->unref(); + _spcurve = new SPCurve(pathparam->get_pathvector()); + } + } else { + _spcurve->unref(); + _spcurve = sp_path_get_curve_for_edit(_path); + } +} + +/** Set the geometry of the edited object in the object tree, but do not commit to XML */ +void PathManipulator::_setGeometry() +{ + using namespace Inkscape::LivePathEffect; + if (empty()) return; + + if (!_lpe_key.empty()) { + // copied from nodepath.cpp + // NOTE: if we are editing an LPE param, _path is not actually an SPPath, it is + // a LivePathEffectObject. (mad laughter) + Effect *lpe = LIVEPATHEFFECT(_path)->get_lpe(); + if (lpe) { + PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data())); + pathparam->set_new_value(_spcurve->get_pathvector(), false); + LIVEPATHEFFECT(_path)->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } else { + if (_path->repr->attribute("inkscape:original-d")) + sp_path_set_original_curve(_path, _spcurve, false, false); + else + sp_shape_set_curve(SP_SHAPE(_path), _spcurve, false); + } +} + +/** Figure out in what attribute to store the nodetype string. */ +Glib::ustring PathManipulator::_nodetypesKey() +{ + if (_lpe_key.empty()) return "sodipodi:nodetypes"; + return _lpe_key + "-nodetypes"; +} + +/** Return the XML node we are editing. + * This method is wrong but necessary at the moment. */ +Inkscape::XML::Node *PathManipulator::_getXMLNode() +{ + if (_lpe_key.empty()) return _path->repr; + return LIVEPATHEFFECT(_path)->repr; +} + +bool PathManipulator::_nodeClicked(Node *n, GdkEventButton *event) +{ + if (event->button != 1) return false; + if (held_alt(*event) && held_control(*event)) { + // Ctrl+Alt+click: delete nodes + hideDragPoint(); + NodeList::iterator iter = NodeList::get_iterator(n); + NodeList &nl = iter->nodeList(); + + if (nl.size() <= 1 || (nl.size() <= 2 && !nl.closed())) { + // Removing last node of closed path - delete it + nl.kill(); + } else { + // In other cases, delete the node under cursor + _deleteStretch(iter, iter.next(), true); + } + + if (!empty()) { + update(); + } + // We need to call MPM's method because it could have been our last node + _multi_path_manipulator._doneWithCleanup(_("Delete node")); + + return true; + } else if (held_control(*event)) { + // Ctrl+click: cycle between node types + if (n->isEndNode()) { + if (n->type() == NODE_CUSP) { + n->setType(NODE_SMOOTH); + } else { + n->setType(NODE_CUSP); + } + } else { + n->setType(static_cast<NodeType>((n->type() + 1) % NODE_LAST_REAL_TYPE)); + } + update(); + _commit(_("Cycle node type")); + return true; + } + return false; +} + +void PathManipulator::_handleGrabbed() +{ + _selection.hideTransformHandles(); +} + +void PathManipulator::_handleUngrabbed() +{ + _selection.restoreTransformHandles(); + _commit(_("Drag handle")); +} + +bool PathManipulator::_handleClicked(Handle *h, GdkEventButton *event) +{ + // retracting by Ctrl+click + if (event->button == 1 && held_control(*event)) { + h->move(h->parent()->position()); + update(); + _commit(_("Retract handle")); + return true; + } + return false; +} + +void PathManipulator::_selectionChanged(SelectableControlPoint *p, bool selected) +{ + if (selected) ++_num_selected; + else --_num_selected; + + // don't do anything if we do not show handles + if (!_show_handles) return; + + // only do something if a node changed selection state + Node *node = dynamic_cast<Node*>(p); + if (!node) return; + + // update handle display + NodeList::iterator iters[5]; + iters[2] = NodeList::get_iterator(node); + iters[1] = iters[2].prev(); + iters[3] = iters[2].next(); + if (selected) { + // selection - show handles on this node and adjacent ones + node->showHandles(true); + if (iters[1]) iters[1]->showHandles(true); + if (iters[3]) iters[3]->showHandles(true); + } else { + /* Deselection is more complex. + * The change might affect 3 nodes - this one and two adjacent. + * If the node and both its neighbors are deselected, hide handles. + * Otherwise, leave as is. */ + if (iters[1]) iters[0] = iters[1].prev(); + if (iters[3]) iters[4] = iters[3].next(); + bool nodesel[5]; + for (int i = 0; i < 5; ++i) { + nodesel[i] = iters[i] && iters[i]->selected(); + } + for (int i = 1; i < 4; ++i) { + if (iters[i] && !nodesel[i-1] && !nodesel[i] && !nodesel[i+1]) { + iters[i]->showHandles(false); + } + } + } +} + +/** Removes all nodes belonging to this manipulator from the control pont selection */ +void PathManipulator::_removeNodesFromSelection() +{ + // remove this manipulator's nodes from selection + for (std::list<SubpathPtr>::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) { + _selection.erase(j.get_pointer()); + } + } +} + +/** Update the XML representation and put the specified annotation on the undo stack */ +void PathManipulator::_commit(Glib::ustring const &annotation) +{ + writeXML(); + sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, annotation.data()); +} + +void PathManipulator::_commit(Glib::ustring const &annotation, gchar const *key) +{ + writeXML(); + sp_document_maybe_done(sp_desktop_document(_desktop), key, SP_VERB_CONTEXT_NODE, + annotation.data()); +} + +/** Update the position of the curve drag point such that it is over the nearest + * point of the path. */ +void PathManipulator::_updateDragPoint(Geom::Point const &evp) +{ + Geom::Matrix to_desktop = _edit_transform * _i2d_transform; + Geom::PathVector pv = _spcurve->get_pathvector(); + boost::optional<Geom::PathVectorPosition> pvp + = Geom::nearestPoint(pv, _desktop->w2d(evp) * to_desktop.inverse()); + if (!pvp) return; + Geom::Point nearest_point = _desktop->d2w(pv.at(pvp->path_nr).pointAt(pvp->t) * to_desktop); + + double fracpart; + std::list<SubpathPtr>::iterator spi = _subpaths.begin(); + for (unsigned i = 0; i < pvp->path_nr; ++i, ++spi) {} + NodeList::iterator first = (*spi)->before(pvp->t, &fracpart); + + double stroke_tolerance = _getStrokeTolerance(); + if (Geom::distance(evp, nearest_point) < stroke_tolerance) { + _dragpoint->setVisible(true); + _dragpoint->setPosition(_desktop->w2d(nearest_point)); + _dragpoint->setSize(2 * stroke_tolerance); + _dragpoint->setTimeValue(fracpart); + _dragpoint->setIterator(first); + } else { + _dragpoint->setVisible(false); + } +} + +/// This is called on zoom change to update the direction arrows +void PathManipulator::_updateOutlineOnZoomChange() +{ + if (_show_path_direction) _updateOutline(); +} + +/** Compute the radius from the edge of the path where clicks chould initiate a curve drag + * or segment selection, in window coordinates. */ +double PathManipulator::_getStrokeTolerance() +{ + /* Stroke event tolerance is equal to half the stroke's width plus the global + * drag tolerance setting. */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double ret = prefs->getIntLimited("/options/dragtolerance/value", 2, 0, 100); + if (_path && SP_OBJECT_STYLE(_path) && !SP_OBJECT_STYLE(_path)->stroke.isNone()) { + ret += SP_OBJECT_STYLE(_path)->stroke_width.computed * 0.5 + * (_edit_transform * _i2d_transform).descrim() // scale to desktop coords + * _desktop->current_zoom(); // == _d2w.descrim() - scale to window coords + } + return ret; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/path-manipulator.h b/src/ui/tool/path-manipulator.h new file mode 100644 index 000000000..a8f1c957e --- /dev/null +++ b/src/ui/tool/path-manipulator.h @@ -0,0 +1,165 @@ +/** @file + * Path manipulator - a component that edits a single path on-canvas + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_PATH_MANIPULATOR_H +#define SEEN_UI_TOOL_PATH_MANIPULATOR_H + +#include <string> +#include <memory> +#include <2geom/pathvector.h> +#include <2geom/matrix.h> +#include <boost/shared_ptr.hpp> +#include <boost/weak_ptr.hpp> +#include "display/display-forward.h" +#include "forward.h" +#include "ui/tool/node.h" +#include "ui/tool/manipulator.h" + +struct SPCanvasItem; + +namespace Inkscape { +namespace XML { class Node; } + +namespace UI { + +class PathManipulator; +class ControlPointSelection; +class PathManipulatorObserver; +class CurveDragPoint; +class PathCanvasGroups; +class MultiPathManipulator; +class Node; +class Handle; + +struct PathSharedData { + NodeSharedData node_data; + SPCanvasGroup *outline_group; + SPCanvasGroup *dragpoint_group; +}; + +/** + * Manipulator that edits a single path using nodes with handles. + * Currently only cubic bezier and linear segments are supported, but this might change + * some time in the future. + */ +class PathManipulator : public PointManipulator { +public: + typedef SPPath *ItemType; + + PathManipulator(MultiPathManipulator &mpm, SPPath *path, Geom::Matrix const &edit_trans, + guint32 outline_color, Glib::ustring lpe_key); + ~PathManipulator(); + virtual bool event(GdkEvent *); + + bool empty(); + void writeXML(); + void update(); // update display, but don't commit + void clear(); // remove all nodes from manipulator + SPPath *item() { return _path; } + + void selectSubpaths(); + void shiftSelection(int dir); + void invertSelectionInSubpaths(); + + void insertNodes(); + void weldNodes(NodeList::iterator preserve_pos = NodeList::iterator()); + void weldSegments(); + void breakNodes(); + void deleteNodes(bool keep_shape = true); + void deleteSegments(); + void reverseSubpaths(bool selected_only); + void setSegmentType(SegmentType); + + void scaleHandle(Node *n, int which, int dir, bool pixel); + void rotateHandle(Node *n, int which, int dir, bool pixel); + + void showOutline(bool show); + void showHandles(bool show); + void showPathDirection(bool show); + void setLiveOutline(bool set); + void setLiveObjects(bool set); + void setControlsTransform(Geom::Matrix const &); + void hideDragPoint(); + MultiPathManipulator &mpm() { return _multi_path_manipulator; } + + NodeList::iterator subdivideSegment(NodeList::iterator after, double t); + NodeList::iterator extremeNode(NodeList::iterator origin, bool search_selected, + bool search_unselected, bool closest); + + static bool is_item_type(void *item); +private: + typedef NodeList Subpath; + typedef boost::shared_ptr<NodeList> SubpathPtr; + + void _createControlPointsFromGeometry(); + void _createGeometryFromControlPoints(); + unsigned _deleteStretch(NodeList::iterator first, NodeList::iterator last, bool keep_shape); + std::string _createTypeString(); + void _updateOutline(); + //void _setOutline(Geom::PathVector const &); + void _getGeometry(); + void _setGeometry(); + Glib::ustring _nodetypesKey(); + Inkscape::XML::Node *_getXMLNode(); + + void _selectionChanged(SelectableControlPoint *p, bool selected); + bool _nodeClicked(Node *, GdkEventButton *); + void _handleGrabbed(); + bool _handleClicked(Handle *, GdkEventButton *); + void _handleUngrabbed(); + + void _externalChange(unsigned type); + void _removeNodesFromSelection(); + void _commit(Glib::ustring const &annotation); + void _commit(Glib::ustring const &annotation, gchar const *key); + void _updateDragPoint(Geom::Point const &); + void _updateOutlineOnZoomChange(); + double _getStrokeTolerance(); + Handle *_chooseHandle(Node *n, int which); + + SubpathList _subpaths; + MultiPathManipulator &_multi_path_manipulator; + SPPath *_path; + SPCurve *_spcurve; // in item coordinates + SPCanvasItem *_outline; + CurveDragPoint *_dragpoint; // an invisible control point hoverng over curve + PathManipulatorObserver *_observer; + Geom::Matrix _d2i_transform; ///< desktop-to-item transform + Geom::Matrix _i2d_transform; ///< item-to-desktop transform, inverse of _d2i_transform + Geom::Matrix _edit_transform; ///< additional transform to apply to editing controls + unsigned _num_selected; ///< number of selected nodes + bool _show_handles; + bool _show_outline; + bool _show_path_direction; + bool _live_outline; + bool _live_objects; + Glib::ustring _lpe_key; + + friend class PathManipulatorObserver; + friend class CurveDragPoint; + friend class Node; + friend class Handle; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/selectable-control-point.cpp b/src/ui/tool/selectable-control-point.cpp new file mode 100644 index 000000000..76028dd82 --- /dev/null +++ b/src/ui/tool/selectable-control-point.cpp @@ -0,0 +1,139 @@ +/** @file + * Desktop-bound selectable control object - implementation + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/selectable-control-point.h" + +namespace Inkscape { +namespace UI { + +static SelectableControlPoint::ColorSet default_scp_color_set = { + { + {0xffffff00, 0x01000000}, // normal fill, stroke + {0xff0000ff, 0x01000000}, // mouseover fill, stroke + {0x0000ffff, 0x01000000} // clicked fill, stroke + }, + {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected + {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected + {0xff000000, 0x000000ff} // clicked fill, stroke when selected +}; + +SelectableControlPoint::SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, + Gtk::AnchorType anchor, SPCtrlShapeType shape, unsigned int size, + ControlPointSelection &sel, ColorSet *cset, SPCanvasGroup *group) + : ControlPoint (d, initial_pos, anchor, shape, size, + cset ? reinterpret_cast<ControlPoint::ColorSet*>(cset) + : reinterpret_cast<ControlPoint::ColorSet*>(&default_scp_color_set), group) + , _selection (sel) +{ + _selection.allPoints().insert(this); +} +SelectableControlPoint::SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, + Gtk::AnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pixbuf, + ControlPointSelection &sel, ColorSet *cset, SPCanvasGroup *group) + : ControlPoint (d, initial_pos, anchor, pixbuf, + cset ? reinterpret_cast<ControlPoint::ColorSet*>(cset) + : reinterpret_cast<ControlPoint::ColorSet*>(&default_scp_color_set), group) + , _selection (sel) +{ + _selection.allPoints().insert(this); +} + +SelectableControlPoint::~SelectableControlPoint() +{ + _selection.erase(this); + _selection.allPoints().erase(this); +} + +bool SelectableControlPoint::grabbed(GdkEventMotion *) +{ + // if a point is dragged while not selected, it should select itself + if (!selected()) { + _takeSelection(); + } + _selection._pointGrabbed(this); + return false; +} + +void SelectableControlPoint::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + _selection._pointDragged(new_pos, event); +} + +void SelectableControlPoint::ungrabbed(GdkEventButton *) +{ + _selection._pointUngrabbed(); +} + +bool SelectableControlPoint::clicked(GdkEventButton *event) +{ + if (_selection._pointClicked(this, event)) + return true; + + if (event->button != 1) return false; + if (held_shift(*event)) { + if (selected()) { + _selection.erase(this); + } else { + _selection.insert(this); + } + } else { + _takeSelection(); + } + return true; +} + +void SelectableControlPoint::_takeSelection() +{ + _selection.clear(); + _selection.insert(this); +} + +bool SelectableControlPoint::selected() const +{ + SelectableControlPoint *p = const_cast<SelectableControlPoint*>(this); + return _selection.find(p) != _selection.end(); +} + +void SelectableControlPoint::_setState(State state) +{ + if (!selected()) { + ControlPoint::_setState(state); + return; + } + + ColorSet *cset = reinterpret_cast<ColorSet*>(_cset); + ColorEntry current = {0, 0}; + switch (state) { + case STATE_NORMAL: + current = cset->selected_normal; break; + case STATE_MOUSEOVER: + current = cset->selected_mouseover; break; + case STATE_CLICKED: + current = cset->selected_clicked; break; + } + _setColors(current); + _state = state; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/selectable-control-point.h b/src/ui/tool/selectable-control-point.h new file mode 100644 index 000000000..2fde16ea9 --- /dev/null +++ b/src/ui/tool/selectable-control-point.h @@ -0,0 +1,72 @@ +/** @file + * Desktop-bound selectable control object + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_SELECTABLE_CONTROL_POINT_H +#define SEEN_UI_TOOL_SELECTABLE_CONTROL_POINT_H + +#include <boost/enable_shared_from_this.hpp> +#include "ui/tool/control-point.h" + +namespace Inkscape { +namespace UI { + +class ControlPointSelection; + +class SelectableControlPoint : public ControlPoint { +public: + struct ColorSet { + ControlPoint::ColorSet cpset; + ColorEntry selected_normal; + ColorEntry selected_mouseover; + ColorEntry selected_clicked; + }; + + ~SelectableControlPoint(); + bool selected() const; + void updateState() const { const_cast<SelectableControlPoint*>(this)->_setState(_state); } + virtual Geom::Rect bounds() { + return Geom::Rect(position(), position()); + } +protected: + SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, + Gtk::AnchorType anchor, SPCtrlShapeType shape, + unsigned int size, ControlPointSelection &sel, ColorSet *cset = 0, + SPCanvasGroup *group = 0); + SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, + Gtk::AnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pixbuf, + ControlPointSelection &sel, ColorSet *cset = 0, SPCanvasGroup *group = 0); + + virtual void _setState(State state); + + virtual void dragged(Geom::Point &, GdkEventMotion *); + virtual bool grabbed(GdkEventMotion *); + virtual void ungrabbed(GdkEventButton *); + virtual bool clicked(GdkEventButton *); + + ControlPointSelection &_selection; +private: + void _takeSelection(); +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/selector.cpp b/src/ui/tool/selector.cpp new file mode 100644 index 000000000..d766d5be3 --- /dev/null +++ b/src/ui/tool/selector.cpp @@ -0,0 +1,133 @@ +/** @file + * Selector component (click and rubberband) + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include "desktop.h" +#include "desktop-handles.h" +#include "display/sodipodi-ctrlrect.h" +#include "event-context.h" +#include "preferences.h" +#include "ui/tool/control-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/selector.h" + +namespace Inkscape { +namespace UI { + +/** A hidden control point used for rubberbanding and selection. + * It uses a clever hack: the canvas item is hidden and only receives events when they + * are passed to it using Selector's event() function. When left mouse button + * is pressed, it grabs events and handles drags and clicks in the usual way. */ +class SelectorPoint : public ControlPoint { +public: + SelectorPoint(SPDesktop *d, SPCanvasGroup *group, Selector *s) + : ControlPoint(d, Geom::Point(0,0), Gtk::ANCHOR_CENTER, SP_CTRL_SHAPE_SQUARE, + 1, &invisible_cset, group) + , _selector(s) + , _cancel(false) + { + setVisible(false); + _rubber = static_cast<CtrlRect*>(sp_canvas_item_new(sp_desktop_controls(_desktop), + SP_TYPE_CTRLRECT, NULL)); + sp_canvas_item_hide(_rubber); + } + ~SelectorPoint() { + gtk_object_destroy(_rubber); + } + SPDesktop *desktop() { return _desktop; } + bool event(GdkEvent *e) { + return _eventHandler(e); + } + +protected: + virtual bool _eventHandler(GdkEvent *event) { + if (event->type == GDK_KEY_PRESS && shortcut_key(event->key) == GDK_Escape && + sp_canvas_item_is_visible(_rubber)) + { + _cancel = true; + sp_canvas_item_hide(_rubber); + return true; + } + return ControlPoint::_eventHandler(event); + } + +private: + virtual bool grabbed(GdkEventMotion *) { + _cancel = false; + _start = position(); + sp_canvas_item_show(_rubber); + return false; + } + virtual void dragged(Geom::Point &new_pos, GdkEventMotion *) { + if (_cancel) return; + Geom::Rect sel(_start, new_pos); + _rubber->setRectangle(sel); + } + virtual void ungrabbed(GdkEventButton *event) { + if (_cancel) return; + sp_canvas_item_hide(_rubber); + Geom::Rect sel(_start, position()); + _selector->signal_area.emit(sel, event); + } + virtual bool clicked(GdkEventButton *event) { + if (event->button != 1) return false; + _selector->signal_point.emit(position(), event); + return true; + } + CtrlRect *_rubber; + Selector *_selector; + Geom::Point _start; + bool _cancel; +}; + + +Selector::Selector(SPDesktop *d) + : Manipulator(d) + , _dragger(new SelectorPoint(d, sp_desktop_controls(d), this)) +{ + _dragger->setVisible(false); +} + +Selector::~Selector() +{ + delete _dragger; +} + +bool Selector::event(GdkEvent *event) +{ + // The hidden control point will capture all events after it obtains the grab, + // but it relies on this function to initiate it. If we pass only first button + // press events here, it won't interfere with any other event handling. + switch (event->type) { + case GDK_BUTTON_PRESS: + // Do not pass button presses other than left button to the control point. + // This way middle click and right click can be handled in SPEventContext. + if (event->button.button == 1) { + _dragger->setPosition(_desktop->w2d(event_point(event->motion))); + return _dragger->event(event); + } + break; + default: break; + } + return false; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/selector.h b/src/ui/tool/selector.h new file mode 100644 index 000000000..f7c00ea71 --- /dev/null +++ b/src/ui/tool/selector.h @@ -0,0 +1,59 @@ +/** @file + * Selector component (click and rubberband) + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_SELECTOR_H +#define SEEN_UI_TOOL_SELECTOR_H + +#include <memory> +#include <gdk/gdk.h> +#include <2geom/rect.h> +#include "display/display-forward.h" +#include "ui/tool/manipulator.h" + +class SPDesktop; +class CtrlRect; + +namespace Inkscape { +namespace UI { + +class SelectorPoint; + +class Selector : public Manipulator { +public: + Selector(SPDesktop *d); + virtual ~Selector(); + virtual bool event(GdkEvent *); + + sigc::signal<void, Geom::Rect const &, GdkEventButton*> signal_area; + sigc::signal<void, Geom::Point const &, GdkEventButton*> signal_point; +private: + SelectorPoint *_dragger; + Geom::Point _start; + CtrlRect *_rubber; + gulong _connection; + bool _cancel; + friend class SelectorPoint; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/shape-record.h b/src/ui/tool/shape-record.h new file mode 100644 index 000000000..edfad1401 --- /dev/null +++ b/src/ui/tool/shape-record.h @@ -0,0 +1,61 @@ +/** @file + * Structures that store data needed for shape editing which are not contained + * directly in the XML node + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_SHAPE_RECORD_H +#define SEEN_UI_TOOL_SHAPE_RECORD_H + +#include <glibmm/ustring.h> +#include <boost/operators.hpp> +#include <2geom/matrix.h> + +class SPItem; +namespace Inkscape { +namespace UI { + +/** Role of the shape in the drawing - affects outline display and color */ +enum ShapeRole { + SHAPE_ROLE_NORMAL, + SHAPE_ROLE_CLIPPING_PATH, + SHAPE_ROLE_MASK, + SHAPE_ROLE_LPE_PARAM // implies edit_original set to true in ShapeRecord +}; + +struct ShapeRecord : + public boost::totally_ordered<ShapeRecord> +{ + SPItem *item; // SP node for the edited shape + Geom::Matrix edit_transform; // how to transform controls - used for clipping paths and masks + ShapeRole role; + Glib::ustring lpe_key; // name of LPE shape param being edited + + inline bool operator==(ShapeRecord const &o) const { + return item == o.item && lpe_key == o.lpe_key; + } + inline bool operator<(ShapeRecord const &o) const { + return item == o.item ? (lpe_key < o.lpe_key) : (item < o.item); + } +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/transform-handle-set.cpp b/src/ui/tool/transform-handle-set.cpp new file mode 100644 index 000000000..1af848b96 --- /dev/null +++ b/src/ui/tool/transform-handle-set.cpp @@ -0,0 +1,657 @@ +/** @file + * Affine transform handles component + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <math.h> +#include <algorithm> +#include <glib.h> +#include <glib/gi18n.h> +#include <gdk/gdk.h> +#include <2geom/transforms.h> +#include "desktop.h" +#include "desktop-handles.h" +#include "display/sodipodi-ctrlrect.h" +#include "preferences.h" +#include "ui/tool/commit-events.h" +#include "ui/tool/control-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/transform-handle-set.h" + +// FIXME BRAIN DAMAGE WARNING: this is a global variable in select-context.cpp +// It should be moved to a header +extern GdkPixbuf *handles[]; +GType sp_select_context_get_type(); + +namespace Inkscape { +namespace UI { + +namespace { +Gtk::AnchorType corner_to_anchor(unsigned c) { + switch (c % 4) { + case 0: return Gtk::ANCHOR_NE; + case 1: return Gtk::ANCHOR_NW; + case 2: return Gtk::ANCHOR_SW; + default: return Gtk::ANCHOR_SE; + } +} +Gtk::AnchorType side_to_anchor(unsigned s) { + switch (s % 4) { + case 0: return Gtk::ANCHOR_N; + case 1: return Gtk::ANCHOR_W; + case 2: return Gtk::ANCHOR_S; + default: return Gtk::ANCHOR_E; + } +} + +// TODO move those two functions into a common place +double snap_angle(double a) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + double unit_angle = M_PI / snaps; + return CLAMP(unit_angle * round(a / unit_angle), -M_PI, M_PI); +} +double snap_increment_degrees() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + return 180.0 / snaps; +} + +ControlPoint::ColorSet thandle_cset = { + {0x000000ff, 0x000000ff}, + {0x00ff6600, 0x000000ff}, + {0x00ff6600, 0x000000ff} +}; + +ControlPoint::ColorSet center_cset = { + {0x00000000, 0x000000ff}, + {0x00000000, 0xff0000b0}, + {0x00000000, 0xff0000b0} +}; +} // anonymous namespace + +/** Base class for node transform handles to simplify implementation */ +class TransformHandle : public ControlPoint { +public: + TransformHandle(TransformHandleSet &th, Gtk::AnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pb) + : ControlPoint(th._desktop, Geom::Point(), anchor, pb, &thandle_cset, + th._transform_handle_group) + , _th(th) + { + setVisible(false); + } +protected: + virtual void startTransform() {} + virtual void endTransform() {} + virtual Geom::Matrix computeTransform(Geom::Point const &pos, GdkEventMotion *event) = 0; + virtual CommitEvent getCommitEvent() = 0; + + Geom::Matrix _last_transform; + Geom::Point _origin; + TransformHandleSet &_th; +private: + virtual bool grabbed(GdkEventMotion *) { + _origin = position(); + _last_transform.setIdentity(); + startTransform(); + + _th._setActiveHandle(this); + _cset = &invisible_cset; + _setState(_state); + return false; + } + virtual void dragged(Geom::Point &new_pos, GdkEventMotion *event) + { + Geom::Matrix t = computeTransform(new_pos, event); + // protect against degeneracies + if (t.isSingular()) return; + Geom::Matrix incr = _last_transform.inverse() * t; + if (incr.isSingular()) return; + _th.signal_transform.emit(incr); + _last_transform = t; + } + virtual void ungrabbed(GdkEventButton *) { + _th._clearActiveHandle(); + _cset = &thandle_cset; + _setState(_state); + endTransform(); + _th.signal_commit.emit(getCommitEvent()); + } +}; + +class ScaleHandle : public TransformHandle { +public: + ScaleHandle(TransformHandleSet &th, Gtk::AnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pb) + : TransformHandle(th, anchor, pb) + {} +protected: + virtual Glib::ustring _getTip(unsigned state) { + if (state_held_control(state)) { + if (state_held_shift(state)) { + return C_("Transform handle tip", + "<b>Shift+Ctrl:</b> scale uniformly about the rotation center"); + } + return C_("Transform handle tip", "<b>Ctrl:</b> scale uniformly"); + } + if (state_held_shift(state)) { + if (state_held_alt(state)) { + return C_("Transform handle tip", + "<b>Shift+Alt:</b> scale using an integer ratio about the rotation center"); + } + return C_("Transform handle tip", "<b>Shift:</b> scale from the rotation center"); + } + if (state_held_alt(state)) { + return C_("Transform handle tip", "<b>Alt:</b> scale using an integer ratio"); + } + return C_("Transform handle tip", "<b>Scale handle:</b> drag to scale the selection"); + } + + virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) { + return format_tip(C_("Transform handle tip", + "Scale by %.2f%% x %.2f%%"), _last_scale_x * 100, _last_scale_y * 100); + } + + virtual bool _hasDragTips() { return true; } + + static double _last_scale_x, _last_scale_y; +}; +double ScaleHandle::_last_scale_x = 1.0; +double ScaleHandle::_last_scale_y = 1.0; + +/// Corner scaling handle for node transforms +class ScaleCornerHandle : public ScaleHandle { +public: + ScaleCornerHandle(TransformHandleSet &th, unsigned corner) + : ScaleHandle(th, corner_to_anchor(corner), _corner_to_pixbuf(corner)) + , _corner(corner) + {} +protected: + virtual void startTransform() { + _sc_center = _th.rotationCenter(); + _sc_opposite = _th.bounds().corner(_corner + 2); + _last_scale_x = _last_scale_y = 1.0; + } + virtual Geom::Matrix computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) { + Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite; + Geom::Point vold = _origin - scc, vnew = new_pos - scc; + // avoid exploding the selection + if (Geom::are_near(vold[Geom::X], 0) || Geom::are_near(vold[Geom::Y], 0)) + return Geom::identity(); + + double scale[2] = { vnew[Geom::X] / vold[Geom::X], vnew[Geom::Y] / vold[Geom::Y] }; + if (held_alt(*event)) { + for (unsigned i = 0; i < 2; ++i) { + if (scale[i] >= 1.0) scale[i] = round(scale[i]); + else scale[i] = 1.0 / round(1.0 / scale[i]); + } + } else if (held_control(*event)) { + scale[0] = scale[1] = std::min(scale[0], scale[1]); + } + _last_scale_x = scale[0]; + _last_scale_y = scale[1]; + Geom::Matrix t = Geom::Translate(-scc) + * Geom::Scale(scale[0], scale[1]) + * Geom::Translate(scc); + return t; + } + virtual CommitEvent getCommitEvent() { + return _last_transform.isUniformScale() + ? COMMIT_MOUSE_SCALE_UNIFORM + : COMMIT_MOUSE_SCALE; + } +private: + static Glib::RefPtr<Gdk::Pixbuf> _corner_to_pixbuf(unsigned c) { + sp_select_context_get_type(); + switch (c % 2) { + case 0: return Glib::wrap(handles[1], true); + default: return Glib::wrap(handles[0], true); + } + } + Geom::Point _sc_center; + Geom::Point _sc_opposite; + unsigned _corner; +}; + +/// Side scaling handle for node transforms +class ScaleSideHandle : public ScaleHandle { +public: + ScaleSideHandle(TransformHandleSet &th, unsigned side) + : ScaleHandle(th, side_to_anchor(side), _side_to_pixbuf(side)) + , _side(side) + {} +protected: + virtual void startTransform() { + _sc_center = _th.rotationCenter(); + Geom::Rect b = _th.bounds(); + _sc_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3)); + _last_scale_x = _last_scale_y = 1.0; + } + virtual Geom::Matrix computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) { + Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite; + Geom::Point vs; + Geom::Dim2 d1 = static_cast<Geom::Dim2>((_side + 1) % 2); + Geom::Dim2 d2 = static_cast<Geom::Dim2>(_side % 2); + + // avoid exploding the selection + if (Geom::are_near(scc[d1], _origin[d1])) + return Geom::identity(); + + vs[d1] = (new_pos - scc)[d1] / (_origin - scc)[d1]; + if (held_alt(*event)) { + if (vs[d1] >= 1.0) vs[d1] = round(vs[d1]); + else vs[d1] = 1.0 / round(1.0 / vs[d1]); + } + vs[d2] = held_control(*event) ? vs[d1] : 1.0; + + _last_scale_x = vs[Geom::X]; + _last_scale_y = vs[Geom::Y]; + Geom::Matrix t = Geom::Translate(-scc) + * Geom::Scale(vs) + * Geom::Translate(scc); + return t; + } + virtual CommitEvent getCommitEvent() { + return _last_transform.isUniformScale() + ? COMMIT_MOUSE_SCALE_UNIFORM + : COMMIT_MOUSE_SCALE; + } +private: + static Glib::RefPtr<Gdk::Pixbuf> _side_to_pixbuf(unsigned c) { + sp_select_context_get_type(); + switch (c % 2) { + case 0: return Glib::wrap(handles[3], true); + default: return Glib::wrap(handles[2], true); + } + } + Geom::Point _sc_center; + Geom::Point _sc_opposite; + unsigned _side; +}; + +/// Rotation handle for node transforms +class RotateHandle : public TransformHandle { +public: + RotateHandle(TransformHandleSet &th, unsigned corner) + : TransformHandle(th, corner_to_anchor(corner), _corner_to_pixbuf(corner)) + , _corner(corner) + {} +protected: + + virtual void startTransform() { + _rot_center = _th.rotationCenter(); + _rot_opposite = _th.bounds().corner(_corner + 2); + _last_angle = 0; + } + + virtual Geom::Matrix computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) + { + Geom::Point rotc = held_shift(*event) ? _rot_opposite : _rot_center; + double angle = Geom::angle_between(_origin - rotc, new_pos - rotc); + if (held_control(*event)) { + angle = snap_angle(angle); + } + _last_angle = angle; + Geom::Matrix t = Geom::Translate(-rotc) + * Geom::Rotate(angle) + * Geom::Translate(rotc); + return t; + } + + virtual CommitEvent getCommitEvent() { return COMMIT_MOUSE_ROTATE; } + + virtual Glib::ustring _getTip(unsigned state) { + if (state_held_shift(state)) { + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Shift+Ctrl:</b> rotate around the opposite corner and snap " + "angle to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", "<b>Shift:</b> rotate around the opposite corner"); + } + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Ctrl:</b> snap angle to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", "<b>Rotation handle:</b> drag to rotate " + "the selection around the rotation center"); + } + + virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) { + return format_tip(C_("Transform handle tip", "Rotate by %.2f°"), + _last_angle * 360.0); + } + + virtual bool _hasDragTips() { return true; } + +private: + static Glib::RefPtr<Gdk::Pixbuf> _corner_to_pixbuf(unsigned c) { + sp_select_context_get_type(); + switch (c % 4) { + case 0: return Glib::wrap(handles[10], true); + case 1: return Glib::wrap(handles[8], true); + case 2: return Glib::wrap(handles[6], true); + default: return Glib::wrap(handles[4], true); + } + } + Geom::Point _rot_center; + Geom::Point _rot_opposite; + unsigned _corner; + static double _last_angle; +}; +double RotateHandle::_last_angle = 0; + +class SkewHandle : public TransformHandle { +public: + SkewHandle(TransformHandleSet &th, unsigned side) + : TransformHandle(th, side_to_anchor(side), _side_to_pixbuf(side)) + , _side(side) + {} + +protected: + + virtual void startTransform() { + _skew_center = _th.rotationCenter(); + Geom::Rect b = _th.bounds(); + _skew_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3)); + _last_angle = 0; + _last_horizontal = _side % 2; + } + + virtual Geom::Matrix computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) + { + Geom::Point scc = held_shift(*event) ? _skew_center : _skew_opposite; + // d1 and d2 are reversed with respect to ScaleSideHandle + Geom::Dim2 d1 = static_cast<Geom::Dim2>(_side % 2); + Geom::Dim2 d2 = static_cast<Geom::Dim2>((_side + 1) % 2); + Geom::Point proj, scale(1.0, 1.0); + + // Skew handles allow scaling up to integer multiples of the original size + // in the second direction; prevent explosions + // TODO should the scaling part be only active with Alt? + if (!Geom::are_near(_origin[d2], scc[d2])) { + scale[d2] = (new_pos - scc)[d2] / (_origin - scc)[d2]; + } + + if (scale[d2] < 1.0) { + scale[d2] = copysign(1.0, scale[d2]); + } else { + scale[d2] = floor(scale[d2]); + } + + // Calculate skew angle. The angle is calculated with regards to the point obtained + // by projecting the handle position on the relevant side of the bounding box. + // This avoids degeneracies when moving the skew angle over the rotation center + proj[d1] = new_pos[d1]; + proj[d2] = scc[d2] + (_origin[d2] - scc[d2]) * scale[d2]; + double angle = 0; + if (!Geom::are_near(proj[d2], scc[d2])) + angle = Geom::angle_between(_origin - scc, proj - scc); + if (held_control(*event)) angle = snap_angle(angle); + + // skew matrix has the from [[1, k],[0, 1]] for horizontal skew + // and [[1,0],[k,1]] for vertical skew. + Geom::Matrix skew = Geom::identity(); + // correct the sign of the tangent + skew[d2 + 1] = (d1 == Geom::X ? -1.0 : 1.0) * tan(angle); + + _last_angle = angle; + Geom::Matrix t = Geom::Translate(-scc) + * Geom::Scale(scale) * skew + * Geom::Translate(scc); + return t; + } + + virtual CommitEvent getCommitEvent() { + return _side % 2 + ? COMMIT_MOUSE_SKEW_Y + : COMMIT_MOUSE_SKEW_X; + } + + virtual Glib::ustring _getTip(unsigned state) { + if (state_held_shift(state)) { + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Shift+Ctrl:</b> skew about the rotation center with snapping " + "to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", "<b>Shift:</b> skew about the rotation center"); + } + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Ctrl:</b> snap skew angle to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", + "<b>Skew handle:</b> drag to skew (shear) selection about " + "the opposite handle"); + } + + virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) { + if (_last_horizontal) { + return format_tip(C_("Transform handle tip", "Skew horizontally by %.2f°"), + _last_angle * 360.0); + } else { + return format_tip(C_("Transform handle tip", "Skew vertically by %.2f°"), + _last_angle * 360.0); + } + } + + virtual bool _hasDragTips() { return true; } + +private: + + static Glib::RefPtr<Gdk::Pixbuf> _side_to_pixbuf(unsigned s) { + sp_select_context_get_type(); + switch (s % 4) { + case 0: return Glib::wrap(handles[9], true); + case 1: return Glib::wrap(handles[7], true); + case 2: return Glib::wrap(handles[5], true); + default: return Glib::wrap(handles[11], true); + } + } + Geom::Point _skew_center; + Geom::Point _skew_opposite; + unsigned _side; + static bool _last_horizontal; + static double _last_angle; +}; +bool SkewHandle::_last_horizontal = false; +double SkewHandle::_last_angle = 0; + +class RotationCenter : public ControlPoint { +public: + RotationCenter(TransformHandleSet &th) + : ControlPoint(th._desktop, Geom::Point(), Gtk::ANCHOR_CENTER, _get_pixbuf(), + ¢er_cset, th._transform_handle_group) + , _th(th) + { + setVisible(false); + } + +protected: + + virtual Glib::ustring _getTip(unsigned /*state*/) { + return C_("Transform handle tip", + "<b>Rotation center:</b> drag to change the origin of transforms"); + } + +private: + + static Glib::RefPtr<Gdk::Pixbuf> _get_pixbuf() { + sp_select_context_get_type(); + return Glib::wrap(handles[12], true); + } + + TransformHandleSet &_th; +}; + +TransformHandleSet::TransformHandleSet(SPDesktop *d, SPCanvasGroup *th_group) + : Manipulator(d) + , _active(0) + , _transform_handle_group(th_group) + , _mode(MODE_SCALE) + , _in_transform(false) + , _visible(true) +{ + _trans_outline = static_cast<CtrlRect*>(sp_canvas_item_new(sp_desktop_controls(_desktop), + SP_TYPE_CTRLRECT, NULL)); + sp_canvas_item_hide(_trans_outline); + _trans_outline->setDashed(true); + + for (unsigned i = 0; i < 4; ++i) { + _scale_corners[i] = new ScaleCornerHandle(*this, i); + _scale_sides[i] = new ScaleSideHandle(*this, i); + _rot_corners[i] = new RotateHandle(*this, i); + _skew_sides[i] = new SkewHandle(*this, i); + } + _center = new RotationCenter(*this); + // when transforming, update rotation center position + signal_transform.connect(sigc::mem_fun(*_center, &RotationCenter::transform)); +} + +TransformHandleSet::~TransformHandleSet() +{ + for (unsigned i = 0; i < 17; ++i) { + delete _handles[i]; + } +} + +/** Sets the mode of transform handles (scale or rotate). */ +void TransformHandleSet::setMode(Mode m) +{ + _mode = m; + _updateVisibility(_visible); +} + +Geom::Rect TransformHandleSet::bounds() +{ + return Geom::Rect(*_scale_corners[0], *_scale_corners[2]); +} + +ControlPoint &TransformHandleSet::rotationCenter() +{ + return *_center; +} + +void TransformHandleSet::setVisible(bool v) +{ + if (_visible != v) { + _visible = v; + _updateVisibility(_visible); + } +} + +void TransformHandleSet::setBounds(Geom::Rect const &r, bool preserve_center) +{ + if (_in_transform) { + _trans_outline->setRectangle(r); + } else { + for (unsigned i = 0; i < 4; ++i) { + _scale_corners[i]->move(r.corner(i)); + _scale_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1))); + _rot_corners[i]->move(r.corner(i)); + _skew_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1))); + } + if (!preserve_center) _center->move(r.midpoint()); + if (_visible) _updateVisibility(true); + } +} + +bool TransformHandleSet::event(GdkEvent*) +{ + return false; +} + +void TransformHandleSet::_emitTransform(Geom::Matrix const &t) +{ + signal_transform.emit(t); + _center->transform(t); +} + +void TransformHandleSet::_setActiveHandle(ControlPoint *th) +{ + _active = th; + if (_in_transform) + throw std::logic_error("Transform initiated when another transform in progress"); + _in_transform = true; + // hide all handles except the active one + _updateVisibility(false); + sp_canvas_item_show(_trans_outline); +} + +void TransformHandleSet::_clearActiveHandle() +{ + // This can only be called from handles, so they had to be visible before _setActiveHandle + sp_canvas_item_hide(_trans_outline); + _active = 0; + _in_transform = false; + _updateVisibility(_visible); +} + +/** Update the visibility of transformation handles according to settings and the dimensions + * of the bounding box. It hides the handles that would have no effect or lead to + * discontinuities. Additionally, side handles for which there is no space are not shown. */ +void TransformHandleSet::_updateVisibility(bool v) +{ + if (v) { + Geom::Rect b = bounds(); + Geom::Point handle_size( + gdk_pixbuf_get_width(handles[0]) / _desktop->current_zoom(), + gdk_pixbuf_get_height(handles[0]) / _desktop->current_zoom()); + Geom::Point bp = b.dimensions(); + + // do not scale when the bounding rectangle has zero width or height + bool show_scale = (_mode == MODE_SCALE) && !Geom::are_near(b.minExtent(), 0); + // do not rotate if the bounding rectangle is degenerate + bool show_rotate = (_mode == MODE_ROTATE_SKEW) && !Geom::are_near(b.maxExtent(), 0); + bool show_scale_side[2], show_skew[2]; + + // show sides if: + // a) there is enough space between corner handles, or + // b) corner handles are not shown, but side handles make sense + // this affects horizontal and vertical scale handles; skew handles never + // make sense if rotate handles are not shown + for (unsigned i = 0; i < 2; ++i) { + Geom::Dim2 d = static_cast<Geom::Dim2>(i); + Geom::Dim2 otherd = static_cast<Geom::Dim2>((i+1)%2); + show_scale_side[i] = (_mode == MODE_SCALE); + show_scale_side[i] &= (show_scale ? bp[d] >= handle_size[d] + : !Geom::are_near(bp[otherd], 0)); + show_skew[i] = (show_rotate && bp[d] >= handle_size[d] + && !Geom::are_near(bp[otherd], 0)); + } + for (unsigned i = 0; i < 4; ++i) { + _scale_corners[i]->setVisible(show_scale); + _rot_corners[i]->setVisible(show_rotate); + _scale_sides[i]->setVisible(show_scale_side[i%2]); + _skew_sides[i]->setVisible(show_skew[i%2]); + } + // show rotation center if there is enough space (?) + _center->setVisible(show_rotate /*&& bp[Geom::X] > handle_size[Geom::X] + && bp[Geom::Y] > handle_size[Geom::Y]*/); + } else { + for (unsigned i = 0; i < 17; ++i) { + if (_handles[i] != _active) + _handles[i]->setVisible(false); + } + } + +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/transform-handle-set.h b/src/ui/tool/transform-handle-set.h new file mode 100644 index 000000000..48ad3af51 --- /dev/null +++ b/src/ui/tool/transform-handle-set.h @@ -0,0 +1,96 @@ +/** @file + * Affine transform handles component + */ +/* Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_TOOL_TRANSFORM_HANDLE_SET_H +#define SEEN_UI_TOOL_TRANSFORM_HANDLE_SET_H + +#include <memory> +#include <gdk/gdk.h> +#include <2geom/forward.h> +#include "display/display-forward.h" +#include "ui/tool/commit-events.h" +#include "ui/tool/manipulator.h" + +class SPDesktop; +class CtrlRect; // this is not present in display-forward.h! +namespace Inkscape { +namespace UI { + +//class TransformHandle; +class RotateHandle; +class SkewHandle; +class ScaleCornerHandle; +class ScaleSideHandle; +class RotationCenter; + +class TransformHandleSet : public Manipulator { +public: + enum Mode { + MODE_SCALE, + MODE_ROTATE_SKEW + }; + + TransformHandleSet(SPDesktop *d, SPCanvasGroup *th_group); + virtual ~TransformHandleSet(); + virtual bool event(GdkEvent *); + + bool visible() { return _visible; } + Mode mode() { return _mode; } + Geom::Rect bounds(); + void setVisible(bool v); + void setMode(Mode); + void setBounds(Geom::Rect const &, bool preserve_center = false); + + bool transforming() { return _in_transform; } + ControlPoint &rotationCenter(); + + sigc::signal<void, Geom::Matrix const &> signal_transform; + sigc::signal<void, CommitEvent> signal_commit; +private: + void _emitTransform(Geom::Matrix const &); + void _setActiveHandle(ControlPoint *h); + void _clearActiveHandle(); + void _updateVisibility(bool v); + union { + ControlPoint *_handles[17]; + struct { + ScaleCornerHandle *_scale_corners[4]; + ScaleSideHandle *_scale_sides[4]; + RotateHandle *_rot_corners[4]; + SkewHandle *_skew_sides[4]; + RotationCenter *_center; + }; + }; + ControlPoint *_active; + SPCanvasGroup *_transform_handle_group; + CtrlRect *_trans_outline; + Mode _mode; + bool _in_transform; + bool _visible; + bool _rot_center_visible; + friend class TransformHandle; + friend class RotationCenter; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/ui/uxmanager.cpp b/src/ui/uxmanager.cpp new file mode 100644 index 000000000..3c6f85b91 --- /dev/null +++ b/src/ui/uxmanager.cpp @@ -0,0 +1,174 @@ +/** \file + * Desktop widget implementation + */ +/* Authors: + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <algorithm> + +#include "uxmanager.h" +#include "util/ege-tags.h" +#include "widgets/toolbox.h" +#include "widgets/desktop-widget.h" +#include "preferences.h" + +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#endif // GDK_WINDOWING_X11 + +using std::map; +using std::vector; + + +gchar const* KDE_WINDOW_MANAGER_NAME = "KWin"; +gchar const* UNKOWN_WINDOW_MANAGER_NAME = "unknown"; + + +static vector<SPDesktop*> desktops; +static vector<SPDesktopWidget*> dtws; +static map<SPDesktop*, vector<GtkWidget*> > trackedBoxes; + + + +namespace Inkscape { +namespace UI { + +UXManager* instance = 0; + +UXManager* UXManager::getInstance() +{ + if (!instance) { + instance = new UXManager(); + } + return instance; +} + + +UXManager::UXManager() : + floatwindowIssues(false) +{ + ege::TagSet tags; + tags.setLang("en"); + + tags.addTag(ege::Tag("General")); + tags.addTag(ege::Tag("Icons")); + +#if defined(GDK_WINDOWING_X11) + char const* wmName = gdk_x11_screen_get_window_manager_name( gdk_screen_get_default() ); + //g_message("Window manager is [%s]", wmName); + + //if (g_ascii_strcasecmp( wmName, UNKOWN_WINDOW_MANAGER_NAME ) == 0) { + if (g_ascii_strcasecmp( wmName, KDE_WINDOW_MANAGER_NAME ) == 0) { + floatwindowIssues = true; + } +#elif defined(GDK_WINDOWING_WIN32) + floatwindowIssues = true; +#endif // GDK_WINDOWING_WIN32 +} + +UXManager::~UXManager() +{ +} + + +bool UXManager::isFloatWindowProblem() const +{ + return floatwindowIssues; +} + +void UXManager::setTask(SPDesktop* dt, gint val) +{ + for (vector<SPDesktopWidget*>::iterator it = dtws.begin(); it != dtws.end(); ++it) { + SPDesktopWidget* dtw = *it; + + gboolean notDone = Inkscape::Preferences::get()->getBool("/options/workarounds/dynamicnotdone", false); + + if (dtw->desktop == dt) { + switch (val) { + default: + case 0: + dtw->setToolboxPosition("ToolToolbar", GTK_POS_LEFT); + dtw->setToolboxPosition("CommandsToolbar", GTK_POS_TOP); + if (notDone) { + dtw->setToolboxPosition("AuxToolbar", GTK_POS_TOP); + } + dtw->setToolboxPosition("SnapToolbar", GTK_POS_TOP); + break; + case 1: + dtw->setToolboxPosition("ToolToolbar", GTK_POS_TOP); + dtw->setToolboxPosition("CommandsToolbar", GTK_POS_LEFT); + if (notDone) { + dtw->setToolboxPosition("AuxToolbar", GTK_POS_TOP); + } + dtw->setToolboxPosition("SnapToolbar", GTK_POS_RIGHT); + break; + case 2: + dtw->setToolboxPosition("ToolToolbar", GTK_POS_LEFT); + dtw->setToolboxPosition("CommandsToolbar", GTK_POS_RIGHT); + dtw->setToolboxPosition("SnapToolbar", GTK_POS_RIGHT); + if (notDone) { + dtw->setToolboxPosition("AuxToolbar", GTK_POS_RIGHT); + } + } + } + } +} + + +void UXManager::addTrack( SPDesktopWidget* dtw ) +{ + if (std::find(dtws.begin(), dtws.end(), dtw) == dtws.end()) { + dtws.push_back(dtw); + } +} + +void UXManager::delTrack( SPDesktopWidget* dtw ) +{ + vector<SPDesktopWidget*>::iterator iter = std::find(dtws.begin(), dtws.end(), dtw); + if (iter != dtws.end()) { + dtws.erase(iter); + } +} + +void UXManager::connectToDesktop( vector<GtkWidget *> const & toolboxes, SPDesktop *desktop ) +{ +//static map<SPDesktop*, vector<GtkWidget*> > trackedBoxes; + + for (vector<GtkWidget*>::const_iterator it = toolboxes.begin(); it != toolboxes.end(); ++it ) { + GtkWidget* toolbox = *it; + + ToolboxFactory::setToolboxDesktop( toolbox, desktop ); + vector<GtkWidget*>& tracked = trackedBoxes[desktop]; + if (find(tracked.begin(), tracked.end(), toolbox) == tracked.end()) { + tracked.push_back(toolbox); + } + } + + if (std::find(desktops.begin(), desktops.end(), desktop) == desktops.end()) { + desktops.push_back(desktop); + } +} + + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/uxmanager.h b/src/ui/uxmanager.h new file mode 100644 index 000000000..aecda2b5e --- /dev/null +++ b/src/ui/uxmanager.h @@ -0,0 +1,65 @@ +#ifndef SEEN_UI_UXMANAGER_H +#define SEEN_UI_UXMANAGER_H +/* + * A simple interface for previewing representations. + * + * Authors: + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include <glib.h> +#include <vector> + +extern "C" +{ + typedef struct _GObject GObject; + typedef struct _GtkWidget GtkWidget; +} + +class SPDesktop; + +struct SPDesktopWidget; + + +namespace Inkscape { +namespace UI { + +class UXManager +{ +public: + static UXManager* getInstance(); + virtual ~UXManager(); + + void addTrack( SPDesktopWidget* dtw ); + void delTrack( SPDesktopWidget* dtw ); + + void connectToDesktop( std::vector<GtkWidget *> const & toolboxes, SPDesktop *desktop ); + + void setTask(SPDesktop* dt, gint val); + + bool isFloatWindowProblem() const; + +private: + UXManager(); + + bool floatwindowIssues; +}; + +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_UI_UXMANAGER_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/view/edit-widget.cpp b/src/ui/view/edit-widget.cpp index d34b18771..770a9bf87 100644 --- a/src/ui/view/edit-widget.cpp +++ b/src/ui/view/edit-widget.cpp @@ -228,12 +228,6 @@ EditWidget::onDialogAlignAndDistribute() } void -EditWidget::onDialogSprayOptionClass() -{ - _dlg_mgr.showDialog("SprayOptionClass"); -} - -void EditWidget::onDialogDocumentProperties() { // manage (Inkscape::UI::Dialog::DocumentPreferences::create()); diff --git a/src/ui/view/edit-widget.h b/src/ui/view/edit-widget.h index 452641e80..2bb708305 100644 --- a/src/ui/view/edit-widget.h +++ b/src/ui/view/edit-widget.h @@ -70,7 +70,6 @@ public: void onDialogAbout(); void onDialogAlignAndDistribute(); - void onDialogSprayOptionClass(); void onDialogInkscapePreferences(); void onDialogDialog(); void onDialogDocumentProperties(); diff --git a/src/ui/widget/color-picker.cpp b/src/ui/widget/color-picker.cpp index d6ae50ae3..34cf1d5e3 100644 --- a/src/ui/widget/color-picker.cpp +++ b/src/ui/widget/color-picker.cpp @@ -116,12 +116,15 @@ ColorPicker::on_changed (guint32) void sp_color_picker_color_mod(SPColorSelector *csel, GObject *cp) { - if (_in_use) return; - else _in_use = true; + if (_in_use) { + return; + } else { + _in_use = true; + } SPColor color; - float alpha; - csel->base->getColorAlpha(color, &alpha); + float alpha = 0; + csel->base->getColorAlpha(color, alpha); guint32 rgba = color.toRGBA32( alpha ); ColorPicker *ptr = (ColorPicker *)(cp); diff --git a/src/ui/widget/labelled.h b/src/ui/widget/labelled.h index 3685944a4..5670af0b6 100644 --- a/src/ui/widget/labelled.h +++ b/src/ui/widget/labelled.h @@ -39,6 +39,8 @@ public: Gtk::Widget const *getWidget() const; Gtk::Label const *getLabel() const; + void setLabelText(const Glib::ustring &str) { _label->set_text(str); }; + protected: Gtk::Widget *_widget; Gtk::Label *_label; diff --git a/src/ui/widget/page-sizer.cpp b/src/ui/widget/page-sizer.cpp index 68f26792a..e604a24ec 100644 --- a/src/ui/widget/page-sizer.cpp +++ b/src/ui/widget/page-sizer.cpp @@ -229,6 +229,11 @@ PageSizer::PageSizer(Registry & _wr) _dimensionUnits( _("U_nits:"), "units", _wr ), _dimensionWidth( _("_Width:"), _("Width of paper"), "width", _dimensionUnits, _wr ), _dimensionHeight( _("_Height:"), _("Height of paper"), "height", _dimensionUnits, _wr ), + _marginTop( _("T_op margin:"), _("Top margin"), "fit-margin-top", _wr ), + _marginLeft( _("L_eft:"), _("Left margin"), "fit-margin-left", _wr), + _marginRight( _("Ri_ght:"), _("Right margin"), "fit-margin-right", _wr), + _marginBottom( _("Botto_m:"), _("Bottom margin"), "fit-margin-bottom", _wr), + _widgetRegistry(&_wr) { //# Set up the Paper Size combo box @@ -273,16 +278,11 @@ PageSizer::PageSizer(Registry & _wr) // _paperSizeListSelection->select(iter); - pack_start (_paperSizeListBox, true, true, 0); - _paperSizeListLabel.set_label(_("P_age size:")); - _paperSizeListLabel.set_use_underline(); - _paperSizeListBox.pack_start (_paperSizeListLabel, false, false, 0); - _paperSizeListLabel.set_mnemonic_widget (_paperSizeList); - _paperSizeListBox.pack_start (_paperSizeListScroller, true, true, 0); + pack_start (_paperSizeListScroller, true, true, 0); //## Set up orientation radio buttons pack_start (_orientationBox, false, false, 0); - _orientationLabel.set_label(_("Page orientation:")); + _orientationLabel.set_label(_("Orientation:")); _orientationBox.pack_start(_orientationLabel, false, false, 0); _landscapeButton.set_use_underline(); _landscapeButton.set_label(_("_Landscape")); @@ -299,19 +299,48 @@ PageSizer::PageSizer(Registry & _wr) //## Set up custom size frame _customFrame.set_label(_("Custom size")); pack_start (_customFrame, false, false, 0); - _customTable.resize(2, 2); - _customTable.set_border_width (4); - _customTable.set_row_spacings (4); - _customTable.set_col_spacings (4); - _customTable.attach(_dimensionWidth, 0,1,0,1); - _customTable.attach(_dimensionUnits, 1,2,0,1); - _customTable.attach(_dimensionHeight, 0,1,1,2); - _customTable.attach(_fitPageButton, 1,2,1,2); - _customFrame.add(_customTable); - + _customFrame.add(_customDimTable); + + _customDimTable.resize(3, 2); + _customDimTable.set_border_width(4); + _customDimTable.set_row_spacings(4); + _customDimTable.set_col_spacings(4); + _customDimTable.attach(_dimensionWidth, 0,1, 0,1); + _customDimTable.attach(_dimensionUnits, 1,2, 0,1); + _customDimTable.attach(_dimensionHeight, 0,1, 1,2); + _customDimTable.attach(_fitPageMarginExpander, 0,2, 2,3); + + //## Set up fit page expander + _fitPageMarginExpander.set_label(_("Resi_ze page to content...")); + _fitPageMarginExpander.set_use_underline(); + _fitPageMarginExpander.add(_marginTable); + + //## Set up margin settings + _marginTable.resize(4, 2); + _marginTable.set_border_width(4); + _marginTable.set_row_spacings(4); + _marginTable.set_col_spacings(4); + _marginTable.attach(_fitPageButtonAlign, 0,2, 0,1); + _marginTable.attach(_marginTopAlign, 0,2, 1,2); + _marginTable.attach(_marginLeftAlign, 0,1, 2,3); + _marginTable.attach(_marginRightAlign, 1,2, 2,3); + _marginTable.attach(_marginBottomAlign, 0,2, 3,4); + + _marginTopAlign.set(0.5, 0.5, 0.0, 1.0); + _marginTopAlign.add(_marginTop); + _marginLeftAlign.set(0.0, 0.5, 0.0, 1.0); + _marginLeftAlign.add(_marginLeft); + _marginRightAlign.set(1.0, 0.5, 0.0, 1.0); + _marginRightAlign.add(_marginRight); + _marginBottomAlign.set(0.5, 0.5, 0.0, 1.0); + _marginBottomAlign.add(_marginBottom); + + _fitPageButtonAlign.set(0.5, 0.5, 0.0, 1.0); + _fitPageButtonAlign.add(_fitPageButton); _fitPageButton.set_use_underline(); - _fitPageButton.set_label(_("_Fit page to selection")); + _fitPageButton.set_label(_("_Resize page to drawing or selection")); _tips.set_tip(_fitPageButton, _("Resize the page to fit the current selection, or the entire drawing if there is no selection")); + } @@ -343,7 +372,7 @@ PageSizer::init () /** * Set document dimensions (if not called by Doc prop's update()) and * set the PageSizer's widgets and text entries accordingly. If - * 'chageList' is true, then adjust the paperSizeList to show the closest + * 'changeList' is true, then adjust the paperSizeList to show the closest * standard page size. * * \param w, h given in px @@ -454,7 +483,7 @@ PageSizer::find_paper_size (double w, double h) const /** - * Tell the desktop to change the page size + * Tell the desktop to fit the page size to the selection or drawing. */ void PageSizer::fire_fit_canvas_to_selection_or_drawing() diff --git a/src/ui/widget/page-sizer.h b/src/ui/widget/page-sizer.h index f970afe44..718eb95b5 100644 --- a/src/ui/widget/page-sizer.h +++ b/src/ui/widget/page-sizer.h @@ -183,24 +183,38 @@ protected: Gtk::HBox _orientationBox; Gtk::Label _orientationLabel; Gtk::RadioButton _portraitButton; - Gtk::RadioButton _landscapeButton; + Gtk::RadioButton _landscapeButton; //callbacks void on_portrait(); void on_landscape(); sigc::connection _portrait_connection; - sigc::connection _landscape_connection; + sigc::connection _landscape_connection; //### Custom size frame Gtk::Frame _customFrame; - Gtk::Table _customTable; + Gtk::Table _customDimTable; RegisteredUnitMenu _dimensionUnits; RegisteredScalarUnit _dimensionWidth; - RegisteredScalarUnit _dimensionHeight; - Gtk::Button _fitPageButton; + RegisteredScalarUnit _dimensionHeight; + + //### Fit Page options + Gtk::Expander _fitPageMarginExpander; + Gtk::Table _marginTable; + Gtk::Alignment _marginTopAlign; + Gtk::Alignment _marginLeftAlign; + Gtk::Alignment _marginRightAlign; + Gtk::Alignment _marginBottomAlign; + RegisteredScalar _marginTop; + RegisteredScalar _marginLeft; + RegisteredScalar _marginRight; + RegisteredScalar _marginBottom; + Gtk::Alignment _fitPageButtonAlign; + Gtk::Button _fitPageButton; + //callback void on_value_changed(); sigc::connection _changedw_connection; - sigc::connection _changedh_connection; + sigc::connection _changedh_connection; Registry *_widgetRegistry; diff --git a/src/ui/widget/point.cpp b/src/ui/widget/point.cpp index 508a8d961..f27cfe8c6 100644 --- a/src/ui/widget/point.cpp +++ b/src/ui/widget/point.cpp @@ -194,7 +194,7 @@ Point::setRange(double min, double max) /** Sets the value of the spin button */ void -Point::setValue(Geom::Point & p) +Point::setValue(Geom::Point const & p) { xwidget.setValue(p[0]); ywidget.setValue(p[1]); diff --git a/src/ui/widget/point.h b/src/ui/widget/point.h index 57a46de76..94477d877 100644 --- a/src/ui/widget/point.h +++ b/src/ui/widget/point.h @@ -64,7 +64,7 @@ public: void setDigits(unsigned digits); void setIncrements(double step, double page); void setRange(double min, double max); - void setValue(Geom::Point & p); + void setValue(Geom::Point const & p); void update(); diff --git a/src/ui/widget/registered-widget.cpp b/src/ui/widget/registered-widget.cpp index 95ddec286..db31d08d3 100644 --- a/src/ui/widget/registered-widget.cpp +++ b/src/ui/widget/registered-widget.cpp @@ -587,6 +587,96 @@ RegisteredTransformedPoint::on_value_changed() } /*######################################### + * Registered TRANSFORMEDPOINT + */ + +RegisteredVector::~RegisteredVector() +{ + _value_x_changed_connection.disconnect(); + _value_y_changed_connection.disconnect(); +} + +RegisteredVector::RegisteredVector ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument* doc_in ) + : RegisteredWidget<Point> (label, tip), + _polar_coords(false) +{ + init_parent(key, wr, repr_in, doc_in); + + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredVector::on_value_changed)); + _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredVector::on_value_changed)); +} + +void +RegisteredVector::setValue(Geom::Point const & p) +{ + if (!_polar_coords) { + Point::setValue(p); + } else { + Geom::Point polar; + polar[Geom::X] = atan2(p) *180/M_PI; + polar[Geom::Y] = p.length(); + Point::setValue(polar); + } +} + +void +RegisteredVector::setValue(Geom::Point const & p, Geom::Point const & origin) +{ + RegisteredVector::setValue(p); + _origin = origin; +} + +/** + * Changes the widgets text to polar coordinates. The SVG output will still be a normal carthesian vector. + * Careful: when calling getValue(), the return value's X-coord will be the angle, Y-value will be the distance/length. + * After changing the coords type (polar/non-polar), the value has to be reset (setValue). + */ +void +RegisteredVector::setPolarCoords(bool polar_coords) +{ + _polar_coords = polar_coords; + if (polar_coords) { + xwidget.setLabelText("Angle:"); + ywidget.setLabelText("Distance:"); + } else { + xwidget.setLabelText("X:"); + ywidget.setLabelText("Y:"); + } +} + +void +RegisteredVector::on_value_changed() +{ + if (setProgrammatically()) { + clearProgrammatically(); + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Geom::Point origin = _origin; + Geom::Point vector = getValue(); + if (_polar_coords) { + vector = Geom::Point::polar(vector[Geom::X]*M_PI/180, vector[Geom::Y]); + } + + Inkscape::SVGOStringStream os; + os << origin << " , " << vector; + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + +/*######################################### * Registered RANDOM */ diff --git a/src/ui/widget/registered-widget.h b/src/ui/widget/registered-widget.h index a5c61f68a..7aefbb90e 100644 --- a/src/ui/widget/registered-widget.h +++ b/src/ui/widget/registered-widget.h @@ -334,6 +334,31 @@ protected: }; +class RegisteredVector : public RegisteredWidget<Point> { +public: + virtual ~RegisteredVector(); + RegisteredVector (const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = NULL, + SPDocument *doc_in = NULL ); + + // redefine setValue, because transform must be applied + void setValue(Geom::Point const & p); + void setValue(Geom::Point const & p, Geom::Point const & origin); + void setPolarCoords(bool polar_coords = true); + +protected: + sigc::connection _value_x_changed_connection; + sigc::connection _value_y_changed_connection; + void on_value_changed(); + + Geom::Point _origin; + bool _polar_coords; +}; + + class RegisteredRandom : public RegisteredWidget<Random> { public: virtual ~RegisteredRandom(); diff --git a/src/ui/widget/selected-style.cpp b/src/ui/widget/selected-style.cpp index e7b0188d8..a8f9f9c60 100644 --- a/src/ui/widget/selected-style.cpp +++ b/src/ui/widget/selected-style.cpp @@ -103,7 +103,7 @@ static guint nui_drop_target_entries = ENTRIES_SIZE(ui_drop_target_entries); static Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop); SelectedStyle::SelectedStyle(bool /*layout*/) - : + : current_stroke_width(0), _desktop (NULL), @@ -955,13 +955,13 @@ SelectedStyle::update() _paintserver_id[i] += ")"; if (SP_IS_LINEARGRADIENT (server)) { - SPGradient *vector = sp_gradient_get_vector(SP_GRADIENT(server), false); + SPGradient *vector = SP_GRADIENT(server)->getVector(); sp_gradient_image_set_gradient ((SPGradientImage *) _gradient_preview_l[i], vector); place->add(_gradient_box_l[i]); _tooltips.set_tip(*place, __lgradient[i]); _mode[i] = SS_LGRADIENT; } else if (SP_IS_RADIALGRADIENT (server)) { - SPGradient *vector = sp_gradient_get_vector(SP_GRADIENT(server), false); + SPGradient *vector = SP_GRADIENT(server)->getVector(); sp_gradient_image_set_gradient ((SPGradientImage *) _gradient_preview_r[i], vector); place->add(_gradient_box_r[i]); _tooltips.set_tip(*place, __rgradient[i]); @@ -1165,7 +1165,7 @@ RotateableSwatch::RotateableSwatch(SelectedStyle *parent, guint mode) : undokey("ssrot1"), cr(0), cr_set(false) - + { } @@ -1426,14 +1426,14 @@ RotateableStrokeWidth::do_release(double by, guint modifier) { Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop) { - if (Dialog::PanelDialogBase *panel_dialog = + if (Dialog::PanelDialogBase *panel_dialog = dynamic_cast<Dialog::PanelDialogBase *>(desktop->_dlg_mgr->getDialog("FillAndStroke"))) { try { - Dialog::FillAndStroke &fill_and_stroke = + Dialog::FillAndStroke &fill_and_stroke = dynamic_cast<Dialog::FillAndStroke &>(panel_dialog->getPanel()); return &fill_and_stroke; } catch (std::exception e) { } - } + } return 0; } |
