diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/ui/clipboard.cpp | |
parent | Initial commit. (diff) | |
download | inkscape-upstream/1.2.2.tar.xz inkscape-upstream/1.2.2.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/ui/clipboard.cpp | 1886 |
1 files changed, 1886 insertions, 0 deletions
diff --git a/src/ui/clipboard.cpp b/src/ui/clipboard.cpp new file mode 100644 index 0000000..a144af2 --- /dev/null +++ b/src/ui/clipboard.cpp @@ -0,0 +1,1886 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * System-wide clipboard management - implementation. + *//* + * Authors: + * see git history + * 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. + * Abhishek Sharma + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "clipboard.h" + +#include <giomm/application.h> +#include <glib/gstdio.h> // for g_file_set_contents etc., used in _onGet and paste +#include <glibmm/i18n.h> +#include <gtkmm/clipboard.h> + +#include <2geom/transforms.h> +#include <2geom/path-sink.h> + +// TODO: reduce header bloat if possible + +#include "context-fns.h" +#include "desktop-style.h" // for sp_desktop_set_style, used in _pasteStyle +#include "desktop.h" +#include "display/curve.h" +#include "document.h" +#include "extension/db.h" // extension database +#include "extension/find_extension_by_mime.h" +#include "extension/input.h" +#include "extension/output.h" +#include "file.h" // for file_import, used in _pasteImage +#include "filter-chemistry.h" +#include "gradient-drag.h" +#include "helper/png-write.h" +#include "id-clash.h" +#include "inkgc/gc-core.h" +#include "inkscape.h" +#include "live_effects/lpe-bspline.h" +#include "live_effects/lpe-spiro.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" +#include "live_effects/parameter/path.h" +#include "message-stack.h" +#include "object/box3d.h" +#include "object/persp3d.h" +#include "object/sp-clippath.h" +#include "object/sp-defs.h" +#include "object/sp-flowtext.h" +#include "object/sp-gradient-reference.h" +#include "object/sp-hatch.h" +#include "object/sp-item-transform.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-marker.h" +#include "object/sp-mask.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-path.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-rect.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-textpath.h" +#include "object/sp-use.h" +#include "path-chemistry.h" +#include "selection-chemistry.h" +#include "style.h" +#include "svg/css-ostringstream.h" // used in copy +#include "svg/svg-color.h" +#include "svg/svg.h" // for sp_svg_transform_write, used in _copySelection +#include "text-chemistry.h" +#include "text-editing.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tools/dropper-tool.h" // used in copy() +#include "ui/tools/node-tool.h" +#include "ui/tools/text-tool.h" +#include "util/units.h" +#include "xml/repr.h" +#include "xml/sp-css-attr.h" + +/// Made up mimetype to represent Gdk::Pixbuf clipboard contents. +#define CLIPBOARD_GDK_PIXBUF_TARGET "image/x-gdk-pixbuf" + +#define CLIPBOARD_TEXT_TARGET "text/plain" + +#ifdef _WIN32 +#include <windows.h> +#endif + +namespace Inkscape { +namespace UI { + +/** + * Default implementation of the clipboard manager. + */ +class ClipboardManagerImpl : public ClipboardManager { +public: + void copy(ObjectSet *set) override; + void copyPathParameter(Inkscape::LivePathEffect::PathParam *) override; + void copySymbol(Inkscape::XML::Node* symbol, gchar const* style, SPDocument *source) override; + bool paste(SPDesktop *desktop, bool in_place) override; + bool pasteStyle(ObjectSet *set) override; + bool pasteSize(ObjectSet *set, bool separately, bool apply_x, bool apply_y) override; + bool pastePathEffect(ObjectSet *set) override; + Glib::ustring getPathParameter(SPDesktop* desktop) override; + Glib::ustring getShapeOrTextObjectId(SPDesktop *desktop) override; + std::vector<Glib::ustring> getElementsOfType(SPDesktop *desktop, gchar const* type = "*", gint maxdepth = -1) override; + Glib::ustring getFirstObjectID() override; + + ClipboardManagerImpl(); + ~ClipboardManagerImpl() override; + +private: + void _cleanStyle(SPCSSAttr *); + void _copySelection(ObjectSet *); + void _copyCompleteStyle(SPItem *item, Inkscape::XML::Node *target, bool child = false); + void _copyUsedDefs(SPItem *); + void _copyGradient(SPGradient *); + void _copyPattern(SPPattern *); + void _copyHatch(SPHatch *); + void _copyTextPath(SPTextPath *); + bool _copyNodes(SPDesktop *desktop, ObjectSet *set); + Inkscape::XML::Node *_copyNode(Inkscape::XML::Node *, Inkscape::XML::Document *, Inkscape::XML::Node *); + Inkscape::XML::Node *_copyIgnoreDup(Inkscape::XML::Node *, Inkscape::XML::Document *, Inkscape::XML::Node *); + + bool _pasteImage(SPDocument *doc); + bool _pasteText(SPDesktop *desktop); + bool _pasteNodes(SPDesktop *desktop, SPDocument *clipdoc, bool in_place); + void _applyPathEffect(SPItem *, gchar const *); + std::unique_ptr<SPDocument> _retrieveClipboard(Glib::ustring = ""); + + // clipboard callbacks + void _onGet(Gtk::SelectionData &, guint); + void _onClear(); + + // various helpers + void _createInternalClipboard(); + void _discardInternalClipboard(); + Inkscape::XML::Node *_createClipNode(); + 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); + void _userWarn(SPDesktop *, char const *); + + // private properties + std::unique_ptr<SPDocument> _clipboardSPDoc; ///< Document that stores the clipboard until someone requests it + Inkscape::XML::Node *_defs; ///< Reference to the clipboard document's defs node + Inkscape::XML::Node *_root; ///< Reference to the clipboard's root node + Inkscape::XML::Node *_clipnode; ///< The node that holds extra information + Inkscape::XML::Document *_doc; ///< Reference to the clipboard's Inkscape::XML::Document + std::set<SPItem*> cloned_elements; + std::vector<SPCSSAttr*> te_selected_style; + std::vector<unsigned> te_selected_style_positions; + int nr_blocks = 0; + + + // we need a way to copy plain text AND remember its style; + // the standard _clipnode is only available in an SVG tree, hence this special storage + SPCSSAttr *_text_style; ///< Style copied along with plain text fragment + + Glib::RefPtr<Gtk::Clipboard> _clipboard; ///< Handle to the system wide clipboard - for convenience + std::list<Glib::ustring> _preferred_targets; ///< List of supported clipboard targets +}; + + +ClipboardManagerImpl::ClipboardManagerImpl() + : _clipboardSPDoc(nullptr), + _defs(nullptr), + _root(nullptr), + _clipnode(nullptr), + _doc(nullptr), + _text_style(nullptr), + _clipboard( Gtk::Clipboard::get() ) +{ + // Clipboard Formats: http://msdn.microsoft.com/en-us/library/ms649013(VS.85).aspx + // On Windows, most graphical applications can handle CF_DIB/CF_BITMAP and/or CF_ENHMETAFILE + // GTK automatically presents an "image/bmp" target as CF_DIB/CF_BITMAP + // Presenting "image/x-emf" as CF_ENHMETAFILE must be done by Inkscape ? + + // push supported clipboard targets, in order of preference + _preferred_targets.emplace_back("image/x-inkscape-svg"); + _preferred_targets.emplace_back("image/svg+xml"); + _preferred_targets.emplace_back("image/svg+xml-compressed"); + _preferred_targets.emplace_back("image/x-emf"); + _preferred_targets.emplace_back("CF_ENHMETAFILE"); + _preferred_targets.emplace_back("WCF_ENHMETAFILE"); // seen on Wine + _preferred_targets.emplace_back("application/pdf"); + _preferred_targets.emplace_back("image/x-adobe-illustrator"); + + // Clipboard requests on app termination can cause undesired extension + // popup windows. Clearing the clipboard can prevent this. + auto application = Gio::Application::get_default(); + if (application) { + application->signal_shutdown().connect_notify([this]() { this->_discardInternalClipboard(); }); + } +} + + +ClipboardManagerImpl::~ClipboardManagerImpl() = default; + + +/** + * Copy selection contents to the clipboard. + */ +void ClipboardManagerImpl::copy(ObjectSet *set) +{ + if ( set->desktop() ) { + SPDesktop *desktop = set->desktop(); + + // Special case for when the gradient dragger is active - copies gradient color + if (desktop->event_context->get_drag()) { + GrDrag *drag = desktop->event_context->get_drag(); + if (drag->hasSelection()) { + guint32 col = drag->getColor(); + + // set the color as clipboard content (text in RRGGBBAA format) + _setClipboardColor(col); + + // create a style with this color on fill and opacity in master opacity, so it can be + // pasted on other stops or objects + if (_text_style) { + sp_repr_css_attr_unref(_text_style); + _text_style = nullptr; + } + _text_style = sp_repr_css_attr_new(); + // print and set properties + gchar color_str[16]; + 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 + } + Inkscape::CSSOStringStream opcss; + opcss << opacity; + sp_repr_css_set_property(_text_style, "opacity", opcss.str().data()); + + _discardInternalClipboard(); + return; + } + } + + // Special case for when the color picker ("dropper") is active - copies color under cursor + auto dt = dynamic_cast<Inkscape::UI::Tools::DropperTool *>(desktop->event_context); + if (dt) { + _setClipboardColor(SP_DROPPER_CONTEXT(desktop->event_context)->get_color(false, true)); + _discardInternalClipboard(); + return; + } + + // Special case for when the text tool is active - if some text is selected, copy plain text, + // not the object that holds it; also copy the style at cursor into + auto tt = dynamic_cast<Inkscape::UI::Tools::TextTool *>(desktop->event_context); + if (tt) { + _discardInternalClipboard(); + Glib::ustring selected_text = Inkscape::UI::Tools::sp_text_get_selected_text(desktop->event_context); + _clipboard->set_text(selected_text); + if (_text_style) { + sp_repr_css_attr_unref(_text_style); + _text_style = nullptr; + } + _text_style = Inkscape::UI::Tools::sp_text_get_style_at_cursor(desktop->event_context); + return; + } + + // Special case for copying part of a path instead of the whole selected object. + if (_copyNodes(desktop, set)) { + return; + } + } + if (set->isEmpty()) { // check whether something is selected + _userWarn(set->desktop(), _("Nothing was copied.")); + return; + } + _discardInternalClipboard(); + + _createInternalClipboard(); // construct a new clipboard document + _copySelection(set); // copy all items in the selection to the internal clipboard + fit_canvas_to_drawing(_clipboardSPDoc.get()); + + _setClipboardTargets(); +} + + +/** + * Copy a Live Path Effect path parameter to the clipboard. + * @param pp The path parameter to store in the clipboard. + */ +void ClipboardManagerImpl::copyPathParameter(Inkscape::LivePathEffect::PathParam *pp) +{ + if ( pp == nullptr ) { + return; + } + SPItem * item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + Geom::PathVector pv = pp->get_pathvector(); + if (item != nullptr) { + pv *= item->i2doc_affine(); + } + auto svgd = sp_svg_write_path(pv); + + if (svgd.empty()) { + return; + } + + _discardInternalClipboard(); + _createInternalClipboard(); + + Inkscape::XML::Node *pathnode = _doc->createElement("svg:path"); + pathnode->setAttribute("d", svgd); + _root->appendChild(pathnode); + Inkscape::GC::release(pathnode); + + fit_canvas_to_drawing(_clipboardSPDoc.get()); + _setClipboardTargets(); +} + +/** + * Copy a symbol from the symbol dialog. + * @param symbol The Inkscape::XML::Node for the symbol. + */ +void ClipboardManagerImpl::copySymbol(Inkscape::XML::Node* symbol, gchar const* style, SPDocument *source) +{ + if (!symbol) + return; + + _discardInternalClipboard(); + _createInternalClipboard(); + + // We add "_duplicate" to have a well defined symbol name that + // bypasses the "prevent_id_classes" routine. We'll get rid of it + // when we paste. + Inkscape::XML::Node *repr = symbol->duplicate(_doc); + Glib::ustring symbol_name = repr->attribute("id"); + + symbol_name += "_inkscape_duplicate"; + repr->setAttribute("id", symbol_name); + _defs->appendChild(repr); + + auto scale = _clipboardSPDoc->getDocumentScale(); + if (auto group = dynamic_cast<SPGroup *>(_clipboardSPDoc->getObjectByRepr(repr))) { + // Convert scale from source to clipboard user units + group->scaleChildItemsRec(scale, Geom::Point(0, 0), false); + } + + auto href = Glib::ustring("#") + symbol->attribute("id"); + Inkscape::XML::Node *use_repr = _doc->createElement("svg:use"); + use_repr->setAttribute("xlink:href", href); + + /** + * If the symbol has a viewBox but no width or height, then take width and + * height from the viewBox and set them on the use element. Otherwise, the + * use element will have 100% document width and height! + */ + { + auto widthAttr = symbol->attribute("width"); + auto heightAttr = symbol->attribute("height"); + auto viewBoxAttr = symbol->attribute("viewBox"); + + if (viewBoxAttr && !(heightAttr || widthAttr)) { + SPViewBox vb; + vb.set_viewBox(viewBoxAttr); + if (vb.viewBox_set) { + use_repr->setAttributeSvgDouble("width", vb.viewBox.width()); + use_repr->setAttributeSvgDouble("height", vb.viewBox.height()); + } + } + } + + // Set a default style in <use> rather than <symbol> so it can be changed. + use_repr->setAttribute("style", style); + _root->appendChild(use_repr); + + if (auto use = dynamic_cast<SPUse *>(_clipboardSPDoc->getObjectByRepr(use_repr))) { + Geom::Affine affine = source->getDocumentScale(); + use->doWriteTransform(affine, &affine, false); + } + + // This min and max sets offsets, we don't have any so set to zero. + _clipnode->setAttributePoint("min", Geom::Point(0, 0)); + _clipnode->setAttributePoint("max", Geom::Point(0, 0)); + + fit_canvas_to_drawing(_clipboardSPDoc.get()); + _setClipboardTargets(); +} + +/** + * 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(SPDesktop *desktop, bool in_place) +{ + // do any checking whether we really are able to paste before requesting the contents + if ( desktop == nullptr ) { + return false; + } + if ( Inkscape::have_viable_layer(desktop, desktop->getMessageStack()) == false ) { + return false; + } + + Glib::ustring target = _getBestTarget(); + + // Special cases of clipboard content handling go here + // Note that target priority is determined in _getBestTarget. + // 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(desktop->doc()); + } + if (target == CLIPBOARD_TEXT_TARGET ) { + // It was text, and we did paste it. If not, continue on. + if (_pasteText(desktop)) { + return true; + } + // If the clipboard contains text/plain, but is an svg document + // then we'll try and detect it and then paste it if possible. + } + + auto tempdoc = _retrieveClipboard(target); + + if ( tempdoc == nullptr ) { + if (target == CLIPBOARD_TEXT_TARGET ) { + _userWarn(desktop, _("Can't paste text outside of the text tool.")); + return false; + } else { + _userWarn(desktop, _("Nothing on the clipboard.")); + return false; + } + } + + if (_pasteNodes(desktop, tempdoc.get(), in_place)) { + return true; + } + + // copy definitions + prevent_id_clashes(tempdoc.get(), desktop->getDocument(), true); + sp_import_document(desktop, tempdoc.get(), in_place); + // _copySelection() has put all items in groups, now ungroup them (preserves transform + // relationships of clones, text-on-path, etc.) + if (target == "image/x-inkscape-svg") { + desktop->selection->ungroup(true); + std::vector<SPItem *> vec2(desktop->selection->items().begin(), desktop->selection->items().end()); + for (auto item : vec2) { + // just a bit beauty on paste hidden items unselect + if (vec2.size() > 1 && item->isHidden()) { + desktop->selection->remove(item); + } + SPLPEItem *pasted_lpe_item = dynamic_cast<SPLPEItem *>(item); + if (pasted_lpe_item) { + remove_hidder_filter(pasted_lpe_item); + } + } + } + + return true; +} + +/** + * Copy any selected nodes and return true if there were nodes. + */ +bool ClipboardManagerImpl::_copyNodes(SPDesktop *desktop, ObjectSet *set) +{ + auto node_tool = dynamic_cast<Inkscape::UI::Tools::NodeTool *>(desktop->event_context); + if (!node_tool || !node_tool->_selected_nodes) + return false; + + SPObject *first_path = nullptr; + for (auto obj : set->items()) { + if(SP_IS_PATH(obj)) { + first_path = obj; + break; + } + } + + auto builder = new Geom::PathBuilder(); + node_tool->_multipath->copySelectedPath(builder); + Geom::PathVector pathv = builder->peek(); + + // discardInternalClipboard done after copy, as deleting clipboard + // document may trigger tool switch (as in PathParam::~PathParam) + _discardInternalClipboard(); + _createInternalClipboard(); + + // Were any nodes actually copied? + if (pathv.empty() || !first_path) + return false; + + Inkscape::XML::Node *pathRepr = _doc->createElement("svg:path"); + + // Remove the source document's scale from path as clipboard is 1:1 + auto source_scale = first_path->document->getDocumentScale(); + pathRepr->setAttribute("d", sp_svg_write_path(pathv * source_scale.inverse())); + + // Group the path to make it consistant with other copy processes + auto group = _doc->createElement("svg:g"); + _root->appendChild(group); + Inkscape::GC::release(group); + + // Store the style for paste-as-object operations. Ignored if pasting into an other path. + pathRepr->setAttribute("style", first_path->style->write(SP_STYLE_FLAG_IFSET) ); + group->appendChild(pathRepr); + Inkscape::GC::release(pathRepr); + + // Store the parent transformation, and scaling factor of the copied object + if (auto parent = dynamic_cast<SPItem *>(first_path->parent)) { + auto transform_str = sp_svg_transform_write(parent->i2doc_affine()); + group->setAttributeOrRemoveIfEmpty("transform", transform_str); + } + + // Set the translation for paste-in-place operation, must be done after repr appends + if (auto path_obj = dynamic_cast<SPPath *>(_clipboardSPDoc->getObjectByRepr(pathRepr))) { + // we could use pathv.boundsFast here, but that box doesn't include stroke width + // so we must take the value from the visualBox of the new shape instead. + auto bbox = *(path_obj->visualBounds()) * source_scale; + _clipnode->setAttributePoint("min", bbox.min()); + _clipnode->setAttributePoint("max", bbox.max()); + } + _setClipboardTargets(); + return true; +} + +/** + * Paste nodes into a selected path and return true if it's possible. + * if the node tool selected + * and one path selected in target + * and one path in source + */ +bool ClipboardManagerImpl::_pasteNodes(SPDesktop *desktop, SPDocument *clipdoc, bool in_place) +{ + auto node_tool = dynamic_cast<Inkscape::UI::Tools::NodeTool *>(desktop->event_context); + if (!node_tool || desktop->selection->objects().size() != 1) + return false; + + SPObject *obj = desktop->selection->objects().back(); + auto target_path = dynamic_cast<SPPath *>(obj); + if (!target_path) + return false; + + auto source_scale = clipdoc->getDocumentScale(); + auto target_trans = target_path->i2doc_affine(); + // Select all nodes prior to pasting in, for later inversion. + node_tool->_selected_nodes->selectAll(); + + for (auto node = clipdoc->getReprRoot()->firstChild(); node ;node = node->next()) { + + auto source_obj = clipdoc->getObjectByRepr(node); + auto group_affine = Geom::Affine(); + + // Unpack group that may have a transformation inside it. + if (auto source_group = dynamic_cast<SPGroup *>(source_obj)) { + if (source_group->children.size() == 1) { + source_obj = source_group->firstChild(); + group_affine = source_group->i2doc_affine(); + } + } + + if (auto source_path = dynamic_cast<SPPath *>(source_obj)) { + auto source_curve = SPCurve::copy(source_path->curveForEdit()); + auto target_curve = SPCurve::copy(target_path->curveForEdit()); + + // Apply group transformation which is usually the old translation plus document scaling factor + source_curve->transform(group_affine); + // Convert curve from source units (usually px so 1:1) + source_curve->transform(source_scale); + + if (!in_place) { + // Move the source curve to the mouse pointer, units are px so do before target_trans + auto bbox = *(source_path->geometricBounds()) * group_affine; + auto to_mouse = Geom::Translate(desktop->point() - bbox.midpoint()); + source_curve->transform(to_mouse); + } else if (auto clipnode = sp_repr_lookup_name(clipdoc->getReprRoot(), "inkscape:clipboard", 1)) { + // Force translation so a foreign path will end up in the right place. + auto bbox = *(source_path->visualBounds()) * group_affine; + auto to_origin = Geom::Translate(clipnode->getAttributePoint("min") - bbox.min()); + source_curve->transform(to_origin); + } + + // Finally convert the curve into path item's coordinate system + source_curve->transform(target_trans.inverse()); + + // Add the source curve to the target copy + target_curve->append(*source_curve); + + // Set the attribute to keep the document up to date (fixes undo) + auto str = sp_svg_write_path(target_curve->get_pathvector()); + target_path->setAttribute("d", str); + } + } + // Finally we invert the selection, this selects all newly added nodes. + node_tool->_selected_nodes->invertSelection(); + return true; +} + +/** + * Returns the id of the first visible copied object. + */ +Glib::ustring ClipboardManagerImpl::getFirstObjectID() +{ + auto tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc == nullptr ) { + return {}; + } + + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + + if (!root) { + return {}; + } + + Inkscape::XML::Node *ch = root->firstChild(); + Inkscape::XML::Node *child = nullptr; + // now clipboard is wrapped on copy since 202d57ea fix + while (ch != nullptr && + g_strcmp0(ch->name(), "svg:g") && + g_strcmp0(child?child->name():nullptr, "svg:g") && + g_strcmp0(child?child->name():nullptr, "svg:path") && + g_strcmp0(child?child->name():nullptr, "svg:use") && + g_strcmp0(child?child->name():nullptr, "svg:text") && + g_strcmp0(child?child->name():nullptr, "svg:image") && + g_strcmp0(child?child->name():nullptr, "svg:rect") && + g_strcmp0(child?child->name():nullptr, "svg:ellipse") && + g_strcmp0(child?child->name():nullptr, "svg:circle") + ) { + ch = ch->next(); + child = ch ? ch->firstChild(): nullptr; + } + + if (child) { + char const *id = child->attribute("id"); + if (id) { + return id; + } + } + + return {}; +} + +/** + * Remove certain css elements which are not useful for pasteStyle + */ +void ClipboardManagerImpl::_cleanStyle(SPCSSAttr *style) +{ + if (style) { + /* Clean text 'position' properties */ + sp_repr_css_unset_property(style, "text-anchor"); + sp_repr_css_unset_property(style, "shape-inside"); + sp_repr_css_unset_property(style, "shape-subtract"); + sp_repr_css_unset_property(style, "shape-padding"); + sp_repr_css_unset_property(style, "shape-margin"); + sp_repr_css_unset_property(style, "inline-size"); + } +} + +/** + * Implements the Paste Style action. + */ +bool ClipboardManagerImpl::pasteStyle(ObjectSet *set) +{ + if (set->desktop() == nullptr) { + return false; + } + + // check whether something is selected + if (set->isEmpty()) { + _userWarn(set->desktop(), _("Select <b>object(s)</b> to paste style to.")); + return false; + } + + auto tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc == nullptr ) { + // no document, but we can try _text_style + if (_text_style) { + _cleanStyle(_text_style); + sp_desktop_set_style(set, set->desktop(), _text_style); + return true; + } else { + _userWarn(set->desktop(), _("No style on the clipboard.")); + return false; + } + } + + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + + bool pasted = false; + + if (clipnode) { + set->document()->importDefs(tempdoc.get()); + SPCSSAttr *style = sp_repr_css_attr(clipnode, "style"); + sp_desktop_set_style(set, set->desktop(), style); + pasted = true; + } + else { + _userWarn(set->desktop(), _("No style on the clipboard.")); + } + + return pasted; +} + + +/** + * Resize the selection or each object in the selection to match the clipboard's size. + * @param separately Whether to scale each object in the selection separately + * @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(ObjectSet *set, bool separately, bool apply_x, bool apply_y) +{ + if (!apply_x && !apply_y) { + return false; // pointless parameters + } + +/* if ( desktop == NULL ) { + return false; + } + Inkscape::Selection *selection = desktop->getSelection();*/ + if (set->isEmpty()) { + if(set->desktop()) + _userWarn(set->desktop(), _("Select <b>object(s)</b> to paste size to.")); + return false; + } + + // FIXME: actually, this should accept arbitrary documents + auto tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc == nullptr ) { + if(set->desktop()) + _userWarn(set->desktop(), _("No size on the clipboard.")); + return false; + } + + // retrieve size information from the clipboard + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + bool pasted = false; + if (clipnode) { + Geom::Point min, max; + bool visual_bbox = !Inkscape::Preferences::get()->getInt("/tools/bounding_box"); + min = clipnode->getAttributePoint((visual_bbox ? "min" : "geom-min"), min); + max = clipnode->getAttributePoint((visual_bbox ? "max" : "geom-max"), max); + + // resize each object in the selection + if (separately) { + auto itemlist= set->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (item) { + Geom::OptRect obj_size = item->desktopPreferredBounds(); + if ( obj_size ) { + item->scale_rel(_getScale(set->desktop(), min, max, *obj_size, apply_x, apply_y)); + } + } else { + g_assert_not_reached(); + } + } + } + // resize the selection as a whole + else { + Geom::OptRect sel_size = set->preferredBounds(); + if ( sel_size ) { + set->setScaleRelative(sel_size->midpoint(), + _getScale(set->desktop(), min, max, *sel_size, apply_x, apply_y)); + } + } + pasted = true; + } + return pasted; +} + + +/** + * Applies a path effect from the clipboard to the selected path. + */ +bool ClipboardManagerImpl::pastePathEffect(ObjectSet *set) +{ + /** @todo FIXME: pastePathEffect crashes when moving the path with the applied effect, + segfaulting in fork_private_if_necessary(). */ + + if ( set->desktop() == nullptr ) { + return false; + } + + //Inkscape::Selection *selection = desktop->getSelection(); + if (!set || set->isEmpty()) { + _userWarn(set->desktop(), _("Select <b>object(s)</b> to paste live path effect to.")); + return false; + } + + auto tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc ) { + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + if ( clipnode ) { + gchar const *effectstack = clipnode->attribute("inkscape:path-effect"); + if ( effectstack ) { + set->document()->importDefs(tempdoc.get()); + // make sure all selected items are converted to paths first (i.e. rectangles) + set->toLPEItems(); + auto itemlist= set->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + _applyPathEffect(item, effectstack); + item->doWriteTransform(item->transform); + } + + return true; + } + } + } + + // no_effect: + _userWarn(set->desktop(), _("No effect on the clipboard.")); + return false; +} + + +/** + * 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(SPDesktop* desktop) +{ + auto doc = _retrieveClipboard(); // any target will do here + if (!doc) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return ""; + } + + // unlimited search depth + auto repr = sp_repr_lookup_name(doc->getReprRoot(), "svg:path", -1); + SPItem *item = dynamic_cast<SPItem *>(doc->getObjectByRepr(repr)); + + if (!item) { + _userWarn(desktop, _("Clipboard does not contain a path.")); + return ""; + } + + // Adjust any copied path into the target document transform. + auto tr_p = item->i2doc_affine(); + auto tr_s = doc->getDocumentScale().inverse(); + auto pathv = sp_svg_read_pathv(repr->attribute("d")); + return sp_svg_write_path(pathv * tr_s * tr_p); +} + + +/** + * 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(SPDesktop *desktop) +{ + // https://bugs.launchpad.net/inkscape/+bug/1293979 + // basically, when we do a depth-first search, we're stopping + // at the first object to be <svg:path> or <svg:text>. + // but that could then return the id of the object's + // clip path or mask, not the original path! + + auto tempdoc = _retrieveClipboard(); // any target will do here + if ( tempdoc == nullptr ) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return ""; + } + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + + // 1293979: strip out the defs of the document + root->removeChild(tempdoc->getDefs()->getRepr()); + + Inkscape::XML::Node *repr = sp_repr_lookup_name(root, "svg:path", -1); // unlimited search depth + if ( repr == nullptr ) { + repr = sp_repr_lookup_name(root, "svg:text", -1); + } + if (repr == nullptr) { + repr = sp_repr_lookup_name(root, "svg:ellipse", -1); + } + if (repr == nullptr) { + repr = sp_repr_lookup_name(root, "svg:rect", -1); + } + if (repr == nullptr) { + repr = sp_repr_lookup_name(root, "svg:circle", -1); + } + + + if ( repr == nullptr ) { + _userWarn(desktop, _("Clipboard does not contain a path.")); + return ""; + } + gchar const *svgd = repr->attribute("id"); + return svgd ? svgd : ""; +} + +/** + * Get all objects id from the clipboard. + * @return A vector containing all IDs or empty if no shape or text item was found. + * type. Set to "*" to retrieve all elements of the types vector inside, feel free to populate more + */ +std::vector<Glib::ustring> ClipboardManagerImpl::getElementsOfType(SPDesktop *desktop, gchar const* type, gint maxdepth) +{ + std::vector<Glib::ustring> result; + auto tempdoc = _retrieveClipboard(); // any target will do here + if ( tempdoc == nullptr ) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return result; + } + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + + // 1293979: strip out the defs of the document + root->removeChild(tempdoc->getDefs()->getRepr()); + std::vector<Inkscape::XML::Node const *> reprs; + if (strcmp(type, "*") == 0){ + //TODO:Fill vector with all possible elements + std::vector<Glib::ustring> types; + types.push_back((Glib::ustring)"svg:path"); + types.push_back((Glib::ustring)"svg:circle"); + types.push_back((Glib::ustring)"svg:rect"); + types.push_back((Glib::ustring)"svg:ellipse"); + types.push_back((Glib::ustring)"svg:text"); + types.push_back((Glib::ustring)"svg:use"); + types.push_back((Glib::ustring)"svg:g"); + types.push_back((Glib::ustring)"svg:image"); + for (auto type_elem : types) { + std::vector<Inkscape::XML::Node const *> reprs_found = sp_repr_lookup_name_many(root, type_elem.c_str(), maxdepth); // unlimited search depth + reprs.insert(reprs.end(), reprs_found.begin(), reprs_found.end()); + } + } else { + reprs = sp_repr_lookup_name_many(root, type, maxdepth); + } + for (auto node : reprs) { + result.emplace_back(node->attribute("id")); + } + if ( result.empty() ) { + _userWarn(desktop, (Glib::ustring::compose(_("Clipboard does not contain any objects of type \"%1\"."), type)).c_str()); + return result; + } + return result; +} + +/** + * Iterate over a list of items and copy them to the clipboard. + */ +void ClipboardManagerImpl::_copySelection(ObjectSet *selection) +{ + // copy the defs used by all items + auto itemlist= selection->items(); + cloned_elements.clear(); + std::vector<SPItem *> items(itemlist.begin(), itemlist.end()); + for (auto item : itemlist) { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem) { + for (auto satellite : lpeitem->get_satellites(false, true)) { + if (satellite) { + SPItem *item2 = dynamic_cast<SPItem *>(satellite); + if (item2 && std::find(items.begin(), items.end(), item2) == items.end()) { + items.push_back(item2); + } + } + } + } + } + cloned_elements.clear(); + for (auto item : items) { + if (item) { + _copyUsedDefs(item); + } else { + g_assert_not_reached(); + } + } + + // copy the representation of the items + std::vector<SPObject *> sorted_items(items.begin(), items.end()); + { + // Get external text references and add them to sorted_items + auto ext_refs = text_categorize_refs(selection->document(), + sorted_items.begin(), sorted_items.end(), + TEXT_REF_EXTERNAL); + for (auto const &ext_ref : ext_refs) { + sorted_items.push_back(selection->document()->getObjectById(ext_ref.first)); + } + } + sort(sorted_items.begin(), sorted_items.end(), sp_object_compare_position_bool); + + //remove already copied elements from cloned_elements + std::vector<SPItem*>tr; + for(auto cloned_element : cloned_elements){ + if(std::find(sorted_items.begin(),sorted_items.end(),cloned_element)!=sorted_items.end()) + tr.push_back(cloned_element); + } + for(auto & it : tr){ + cloned_elements.erase(it); + } + + // One group per shared parent + std::map<SPObject const *, Inkscape::XML::Node *> groups; + + sorted_items.insert(sorted_items.end(),cloned_elements.begin(),cloned_elements.end()); + for(auto sorted_item : sorted_items){ + SPItem *item = dynamic_cast<SPItem*>(sorted_item); + if (item) { + // Create a group with the parent transform. This group will be ungrouped when pasting + // und takes care of transform relationships of clones, text-on-path, etc. + auto &group = groups[item->parent]; + if (!group) { + group = _doc->createElement("svg:g"); + _root->appendChild(group); + Inkscape::GC::release(group); + + if (auto parent = dynamic_cast<SPItem *>(item->parent)) { + auto transform_str = sp_svg_transform_write(parent->i2doc_affine()); + group->setAttributeOrRemoveIfEmpty("transform", transform_str); + } + } + + Inkscape::XML::Node *obj = item->getRepr(); + Inkscape::XML::Node *obj_copy; + if(cloned_elements.find(item)==cloned_elements.end()) + obj_copy = _copyNode(obj, _doc, group); + else + obj_copy = _copyNode(obj, _doc, _clipnode); + + // copy complete inherited style + _copyCompleteStyle(item, obj_copy); + } + } + // copy style for Paste Style action + if (auto item = selection->singleItem()) { + SPCSSAttr *style = take_style_from_item(item); + _cleanStyle(style); + sp_repr_css_set(_clipnode, style, "style"); + sp_repr_css_attr_unref(style); + + // copy path effect from the first path + if (gchar const *effect = item->getRepr()->attribute("inkscape:path-effect")) { + _clipnode->setAttribute("inkscape:path-effect", effect); + } + } + + if (Geom::OptRect size = selection->visualBounds()) { + _clipnode->setAttributePoint("min", size->min()); + _clipnode->setAttributePoint("max", size->max()); + } + if (Geom::OptRect geom_size = selection->geometricBounds()) { + _clipnode->setAttributePoint("geom-min", geom_size->min()); + _clipnode->setAttributePoint("geom-max", geom_size->max()); + } +} + +/** + * Copies the style from the stylesheet to preserve it. + * + * @param item - The source item (connected to it's document) + * @param target - The target xml node to store the style in. + * @param child - Flag to indicate a recursive call, do not use. + */ +void ClipboardManagerImpl::_copyCompleteStyle(SPItem *item, Inkscape::XML::Node *target, bool child) +{ + auto source = item->getRepr(); + SPCSSAttr *css; + if (child) { + // Child styles shouldn't copy their parent's existing cascaded style. + css = sp_repr_css_attr(source, "style"); + } else { + css = sp_repr_css_attr_inherited(source, "style"); + } + for (auto iter : item->style->properties()) { + if (iter->style_src == SPStyleSrc::STYLE_SHEET) { + css->setAttributeOrRemoveIfEmpty(iter->name(), iter->get_value()); + } + } + sp_repr_css_set(target, css, "style"); + sp_repr_css_attr_unref(css); + + if (dynamic_cast<SPGroup *>(item)) { + // Recursively go through chldren too + auto source_child = source->firstChild(); + auto target_child = target->firstChild(); + while (source_child && target_child) { + if (auto child_item = dynamic_cast<SPItem *>(item->document->getObjectByRepr(source_child))) { + _copyCompleteStyle(child_item, target_child, true); + } + source_child = source_child->next(); + target_child = target_child->next(); + } + } +} + +/** + * Recursively copy all the definitions used by a given item to the clipboard defs. + */ +void ClipboardManagerImpl::_copyUsedDefs(SPItem *item) +{ + SPUse *use=dynamic_cast<SPUse *>(item); + if (use && use->get_original()) { + if(cloned_elements.insert(use->get_original()).second) + _copyUsedDefs(use->get_original()); + } + + // copy fill and stroke styles (patterns and gradients) + SPStyle *style = item->style; + + if (style && (style->fill.isPaintserver())) { + SPPaintServer *server = item->style->getFillPaintServer(); + if ( dynamic_cast<SPLinearGradient *>(server) || dynamic_cast<SPRadialGradient *>(server) || dynamic_cast<SPMeshGradient *>(server) ) { + _copyGradient(dynamic_cast<SPGradient *>(server)); + } + SPPattern *pattern = dynamic_cast<SPPattern *>(server); + if (pattern) { + _copyPattern(pattern); + } + SPHatch *hatch = dynamic_cast<SPHatch *>(server); + if (hatch) { + _copyHatch(hatch); + } + } + if (style && (style->stroke.isPaintserver())) { + SPPaintServer *server = item->style->getStrokePaintServer(); + if ( dynamic_cast<SPLinearGradient *>(server) || dynamic_cast<SPRadialGradient *>(server) || dynamic_cast<SPMeshGradient *>(server) ) { + _copyGradient(dynamic_cast<SPGradient *>(server)); + } + SPPattern *pattern = dynamic_cast<SPPattern *>(server); + if (pattern) { + _copyPattern(pattern); + } + SPHatch *hatch = dynamic_cast<SPHatch *>(server); + if (hatch) { + _copyHatch(hatch); + } + } + + // For shapes, copy all of the shape's markers + SPShape *shape = dynamic_cast<SPShape *>(item); + if (shape) { + for (auto & i : shape->_marker) { + if (i) { + _copyNode(i->getRepr(), _doc, _defs); + } + } + } + + // For 3D boxes, copy perspectives + if (SPBox3D *box = dynamic_cast<SPBox3D *>(item)) { + if (auto perspective = box->get_perspective()) { + _copyNode(perspective->getRepr(), _doc, _defs); + } + } + + // Copy text paths + { + SPText *text = dynamic_cast<SPText *>(item); + SPTextPath *textpath = (text) ? dynamic_cast<SPTextPath *>(text->firstChild()) : nullptr; + if (textpath) { + _copyTextPath(textpath); + } + if (text) { + for (auto &&shape_prop_ptr : { + reinterpret_cast<SPIShapes SPStyle::*>(&SPStyle::shape_inside), + reinterpret_cast<SPIShapes SPStyle::*>(&SPStyle::shape_subtract) }) { + for (auto *href : (text->style->*shape_prop_ptr).hrefs) { + auto shape_obj = href->getObject(); + if (!shape_obj) + continue; + auto shape_repr = shape_obj->getRepr(); + if (sp_repr_is_def(shape_repr)) { + _copyIgnoreDup(shape_repr, _doc, _defs); + } + } + } + } + } + + // Copy clipping objects + if (SPObject *clip = item->getClipObject()) { + _copyNode(clip->getRepr(), _doc, _defs); + } + // Copy mask objects + if (SPObject *mask = item->getMaskObject()) { + _copyNode(mask->getRepr(), _doc, _defs); + // recurse into the mask for its gradients etc. + for(auto& o: mask->children) { + SPItem *childItem = dynamic_cast<SPItem *>(&o); + if (childItem) { + _copyUsedDefs(childItem); + } + } + } + + // Copy filters + if (style->getFilter()) { + SPObject *filter = style->getFilter(); + if (dynamic_cast<SPFilter *>(filter)) { + _copyNode(filter->getRepr(), _doc, _defs); + } + } + + // For lpe items, copy lpe stack if applicable + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem) { + if (lpeitem->hasPathEffect()) { + PathEffectList path_effect_list( *lpeitem->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + _copyNode(lpeobj->getRepr(), _doc, _defs); + } + } + } + } + + // recurse + for(auto& o: item->children) { + SPItem *childItem = dynamic_cast<SPItem *>(&o); + if (childItem) { + _copyUsedDefs(childItem); + } + } +} + +/** + * Copy a single gradient to the clipboard's defs element. + */ +void ClipboardManagerImpl::_copyGradient(SPGradient *gradient) +{ + while (gradient) { + // climb up the refs, copying each one in the chain + _copyNode(gradient->getRepr(), _doc, _defs); + if (gradient->ref){ + gradient = gradient->ref->getObject(); + } + else { + gradient = nullptr; + } + } +} + + +/** + * Copy a single pattern to the clipboard document's defs element. + */ +void ClipboardManagerImpl::_copyPattern(SPPattern *pattern) +{ + // climb up the references, copying each one in the chain + while (pattern) { + _copyNode(pattern->getRepr(), _doc, _defs); + + // items in the pattern may also use gradients and other patterns, so recurse + for (auto& child: pattern->children) { + SPItem *childItem = dynamic_cast<SPItem *>(&child); + if (childItem) { + _copyUsedDefs(childItem); + } + } + if (pattern->ref){ + pattern = pattern->ref->getObject(); + } + else{ + pattern = nullptr; + } + } +} + +/** + * Copy a single hatch to the clipboard document's defs element. + */ +void ClipboardManagerImpl::_copyHatch(SPHatch *hatch) +{ + // climb up the references, copying each one in the chain + while (hatch) { + _copyNode(hatch->getRepr(), _doc, _defs); + + for (auto &child : hatch->children) { + SPItem *childItem = dynamic_cast<SPItem *>(&child); + if (childItem) { + _copyUsedDefs(childItem); + } + } + if (hatch->ref) { + hatch = hatch->ref->getObject(); + } else { + hatch = nullptr; + } + } +} + + +/** + * Copy a text path to the clipboard's defs element. + */ +void ClipboardManagerImpl::_copyTextPath(SPTextPath *tp) +{ + SPItem *path = sp_textpath_get_path_item(tp); + if (!path) { + return; + } + // textpaths that aren't in defs (on the canvas) shouldn't be copied because if + // both objects are being copied already, this ends up stealing the refs id. + if(path->parent && SP_IS_DEFS(path->parent)) { + _copyIgnoreDup(path->getRepr(), _doc, _defs); + } +} + + +/** + * Copy a single XML node from one document to another. + * @param node The node to be copied + * @param target_doc The document to which the node is to be copied + * @param parent The node in the target document which will become the parent of the copied node + * @return Pointer to the copied node + */ +Inkscape::XML::Node *ClipboardManagerImpl::_copyNode(Inkscape::XML::Node *node, Inkscape::XML::Document *target_doc, Inkscape::XML::Node *parent) +{ + Inkscape::XML::Node *dup = node->duplicate(target_doc); + parent->appendChild(dup); + Inkscape::GC::release(dup); + return dup; +} + +Inkscape::XML::Node *ClipboardManagerImpl::_copyIgnoreDup(Inkscape::XML::Node *node, Inkscape::XML::Document *target_doc, Inkscape::XML::Node *parent) +{ + if (sp_repr_lookup_child(_root, "id", node->attribute("id"))) { + // node already copied + return nullptr; + } + Inkscape::XML::Node *dup = node->duplicate(target_doc); + parent->appendChild(dup); + Inkscape::GC::release(dup); + return dup; +} + + +/** + * Retrieve a bitmap image from the clipboard and paste it into the active document. + */ +bool ClipboardManagerImpl::_pasteImage(SPDocument *doc) +{ + if ( doc == nullptr ) { + return false; + } + + // retrieve image data + Glib::RefPtr<Gdk::Pixbuf> img = _clipboard->wait_for_image(); + if (!img) { + return false; + } + + Inkscape::Extension::Extension *png = Inkscape::Extension::find_by_mime("image/png"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring attr_saved = prefs->getString("/dialogs/import/link"); + bool ask_saved = prefs->getBool("/dialogs/import/ask"); + prefs->setString("/dialogs/import/link", "embed"); + prefs->setBool("/dialogs/import/ask", false); + png->set_gui(false); + + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-import", nullptr ); + img->save(filename, "png"); + file_import(doc, filename, png); + g_free(filename); + prefs->setString("/dialogs/import/link", attr_saved); + prefs->setBool("/dialogs/import/ask", ask_saved); + png->set_gui(true); + + return true; +} + +/** + * Paste text into the selected text object or create a new one to hold it. + */ +bool ClipboardManagerImpl::_pasteText(SPDesktop *desktop) +{ + if ( desktop == nullptr ) { + return false; + } + + // if the text editing tool is active, paste the text into the active text object + if (dynamic_cast<Inkscape::UI::Tools::TextTool *>(desktop->event_context)) { + return Inkscape::UI::Tools::sp_text_paste_inline(desktop->event_context); + } + + // Parse the clipboard text as if it was a color string. + Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get(); + Glib::ustring const clip_text = clipboard->wait_for_text(); + if (clip_text.length() < 30) { + // Zero makes it impossible to paste a 100% transparent black, but it's useful. + guint32 const rgb0 = sp_svg_read_color(clip_text.c_str(), 0x0); + if (rgb0) { + SPCSSAttr *color_css = sp_repr_css_attr_new(); + sp_repr_css_set_property(color_css, "fill", SPColor(rgb0).toString().c_str()); + // In the future this could parse opacity, but sp_svg_read_color lacks this. + sp_repr_css_set_property(color_css, "fill-opacity", "1.0"); + sp_desktop_set_style(desktop, color_css); + return true; + } + } + return false; +} + + +/** + * Applies a pasted path effect to a given item. + */ +void ClipboardManagerImpl::_applyPathEffect(SPItem *item, gchar const *effectstack) +{ + if ( item == nullptr ) { + return; + } + + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem && effectstack) { + std::istringstream iss(effectstack); + std::string href; + while (std::getline(iss, href, ';')) + { + SPObject *obj = sp_uri_reference_resolve(_clipboardSPDoc.get(), href.c_str()); + if (!obj) { + return; + } + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(obj); + if (lpeobj) { + Inkscape::LivePathEffect::LPESpiro *spiroto = dynamic_cast<Inkscape::LivePathEffect::LPESpiro *>(lpeobj->get_lpe()); + bool has_spiro = lpeitem->hasPathEffectOfType(Inkscape::LivePathEffect::SPIRO); + Inkscape::LivePathEffect::LPEBSpline *bsplineto = dynamic_cast<Inkscape::LivePathEffect::LPEBSpline *>(lpeobj->get_lpe()); + bool has_bspline = lpeitem->hasPathEffectOfType(Inkscape::LivePathEffect::BSPLINE); + if ((!spiroto || !has_spiro) && (!bsplineto || !has_bspline)) { + lpeitem->addPathEffect(lpeobj); + } + } + } + // for each effect in the stack, check if we need to fork it before adding it to the item + lpeitem->forkPathEffectsIfNecessary(1); + } +} + + +/** + * Retrieve the clipboard contents as a document. + * @return Clipboard contents converted to SPDocument, or NULL if no suitable content was present + */ +std::unique_ptr<SPDocument> ClipboardManagerImpl::_retrieveClipboard(Glib::ustring required_target) +{ + Glib::ustring best_target; + if ( required_target == "" ) { + best_target = _getBestTarget(); + } else { + best_target = required_target; + } + + if ( best_target == "" ) { + return nullptr; + } + + // FIXME: Temporary hack until we add memory input. + // Save the clipboard contents to some file, then read it + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-import", nullptr ); + + bool file_saved = false; + Glib::ustring target = best_target; + +#ifdef _WIN32 + if (best_target == "CF_ENHMETAFILE" || best_target == "WCF_ENHMETAFILE") + { // Try to save clipboard data as en emf file (using win32 api) + if (OpenClipboard(NULL)) { + HGLOBAL hglb = GetClipboardData(CF_ENHMETAFILE); + if (hglb) { + HENHMETAFILE hemf = CopyEnhMetaFile((HENHMETAFILE) hglb, filename); + if (hemf) { + file_saved = true; + target = "image/x-emf"; + DeleteEnhMetaFile(hemf); + } + } + CloseClipboard(); + } + } +#endif + + if (!file_saved) { + if ( !_clipboard->wait_is_target_available(best_target) ) { + return nullptr; + } + + // doing this synchronously makes better sense + // TODO: use another method because this one is badly broken imo. + // from documentation: "Returns: A SelectionData object, which will be invalid if retrieving the given target failed." + // I don't know how to check whether an object is 'valid' or not, unusable if that's not possible... + Gtk::SelectionData sel = _clipboard->wait_for_contents(best_target); + target = sel.get_target(); // this can crash if the result was invalid of last function. No way to check for this :( + + // FIXME: Temporary hack until we add memory input. + // Save the clipboard contents to some file, then read it + g_file_set_contents(filename, (const gchar *) sel.get_data(), sel.get_length(), nullptr); + } + + // 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" || target == "text/plain") { + target = "image/svg+xml"; + } + // Use the EMF extension to import metafiles + if (target == "CF_ENHMETAFILE" || target == "WCF_ENHMETAFILE") { + target = "image/x-emf"; + } + + 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() ) { + return nullptr; // this shouldn't happen unless _getBestTarget returns something bogus + } + + SPDocument *tempdoc = nullptr; + try { + tempdoc = (*in)->open(filename); + } catch (...) { + } + g_unlink(filename); + g_free(filename); + + return std::unique_ptr<SPDocument>(tempdoc); +} + + +/** + * Callback called when some other application requests data from Inkscape. + * + * Finds a suitable output extension to save the internal clipboard document, + * then saves it to memory and sets the clipboard contents. + */ +void ClipboardManagerImpl::_onGet(Gtk::SelectionData &sel, guint /*info*/) +{ + if (_clipboardSPDoc == nullptr) + return; + + Glib::ustring target = sel.get_target(); + if (target == "") { + return; // this shouldn't happen + } + + if (target == CLIPBOARD_TEXT_TARGET) { + target = "image/x-inkscape-svg"; + } + + // FIXME: Temporary hack until we add support for memory output. + // Save to a temporary file, read it back and then set the clipboard contents + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-export", nullptr ); + gchar *data = nullptr; + gsize len; + + // XXX This is a crude fix for clipboards accessing extensions + // Remove when gui is extracted from extension execute and uses exceptions. + bool previous_gui = INKSCAPE.use_gui(); + INKSCAPE.use_gui(false); + + try { + // TODO: In the future we may want to detect raster image types such as jpeg + // and use export_raster to get the right output for some programs. + if (target == "image/png") + { + gdouble dpi = Inkscape::Util::Quantity::convert(1, "in", "px"); + guint32 bgcolor = 0x00000000; + + Geom::Point origin (_clipboardSPDoc->getRoot()->x.computed, _clipboardSPDoc->getRoot()->y.computed); + Geom::Rect area = Geom::Rect(origin, origin + _clipboardSPDoc->getDimensions()); + + unsigned long int width = (unsigned long int) (area.width() + 0.5); + unsigned long int height = (unsigned long int) (area.height() + 0.5); + + // read from namedview + Inkscape::XML::Node *nv = _clipboardSPDoc->getReprNamedView(); + if (nv && nv->attribute("pagecolor")) { + bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); + } + if (nv && nv->attribute("inkscape:pageopacity")) { + double opacity = nv->getAttributeDouble("inkscape:pageopacity", 1.0); + bgcolor |= SP_COLOR_F_TO_U(opacity); + } + std::vector<SPItem*> x; + sp_export_png_file(_clipboardSPDoc.get(), filename, area, width, height, dpi, dpi, bgcolor, nullptr, + nullptr, true, x); + } + else + { + 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)->loaded()) { + // Need to load the extension. + (*out)->set_state(Inkscape::Extension::Extension::STATE_LOADED); + } + + (*out)->save(_clipboardSPDoc.get(), filename, true); + } + g_file_get_contents(filename, &data, &len, nullptr); + + sel.set(8, (guint8 const *) data, len); + } catch (...) { + } + + INKSCAPE.use_gui(previous_gui); + g_unlink(filename); // delete the temporary file + g_free(filename); + g_free(data); +} + + +/** + * Callback when someone else takes the clipboard. + * + * When the clipboard owner changes, this callback clears the internal clipboard document + * to reduce memory usage. + */ +void ClipboardManagerImpl::_onClear() +{ + // why is this called before _onGet??? + //_discardInternalClipboard(); +} + + +/** + * Creates an internal clipboard document from scratch. + */ +void ClipboardManagerImpl::_createInternalClipboard() +{ + if ( _clipboardSPDoc == nullptr ) { + _clipboardSPDoc.reset(SPDocument::createNewDoc(nullptr, false, true)); + //g_assert( _clipboardSPDoc != NULL ); + _defs = _clipboardSPDoc->getDefs()->getRepr(); + _doc = _clipboardSPDoc->getReprDoc(); + _root = _clipboardSPDoc->getReprRoot(); + + // Preserve ANY copied text kerning + _root->setAttribute("xml:space", "preserve"); + + if (SP_ACTIVE_DOCUMENT) { + _clipboardSPDoc->setDocumentBase(SP_ACTIVE_DOCUMENT->getDocumentBase()); + } + + _clipnode = _doc->createElement("inkscape:clipboard"); + _root->appendChild(_clipnode); + Inkscape::GC::release(_clipnode); + + // once we create a SVG document, style will be stored in it, so flush _text_style + if (_text_style) { + sp_repr_css_attr_unref(_text_style); + _text_style = nullptr; + } + } +} + + +/** + * Deletes the internal clipboard document. + */ +void ClipboardManagerImpl::_discardInternalClipboard() +{ + if ( _clipboardSPDoc != nullptr ) { + // Explicit delete required to free SPDocument + // see https://gitlab.com/inkscape/inkscape/-/issues/2723 + delete _clipboardSPDoc.release(); + _defs = nullptr; + _doc = nullptr; + _root = nullptr; + _clipnode = nullptr; + } +} + + +/** + * Get the scale to resize an item, based on the command and desktop state. + */ +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) +{ + double scale_x = 1.0; + double scale_y = 1.0; + + if (apply_x) { + scale_x = (max[Geom::X] - min[Geom::X]) / obj_rect[Geom::X].extent(); + } + if (apply_y) { + scale_y = (max[Geom::Y] - min[Geom::Y]) / obj_rect[Geom::Y].extent(); + } + // 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 && desktop->isToolboxButtonActive("lock")) { + 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); +} + + +/** + * Find the most suitable clipboard target. + */ +Glib::ustring ClipboardManagerImpl::_getBestTarget() +{ + auto targets = _clipboard->wait_for_targets(); + + // clipboard target debugging snippet + /* + g_message("Begin clipboard targets"); + for ( std::list<Glib::ustring>::iterator x = targets.begin() ; x != targets.end(); ++x ) + g_message("Clipboard target: %s", (*x).data()); + g_message("End clipboard targets\n"); + //*/ + + for (auto & _preferred_target : _preferred_targets) + { + if ( std::find(targets.begin(), targets.end(), _preferred_target) != targets.end() ) { + return _preferred_target; + } + } +#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) { + break; + } + format = EnumClipboardFormats(format); + } + CloseClipboard(); + + if (format == CF_ENHMETAFILE) { + return "CF_ENHMETAFILE"; + } + if (format == CF_DIB || format == CF_BITMAP) { + return CLIPBOARD_GDK_PIXBUF_TARGET; + } + } + + if (IsClipboardFormatAvailable(CF_ENHMETAFILE)) { + return "CF_ENHMETAFILE"; + } +#endif + if (_clipboard->wait_is_image_available()) { + return CLIPBOARD_GDK_PIXBUF_TARGET; + } + if (_clipboard->wait_is_text_available()) { + return CLIPBOARD_TEXT_TARGET; + } + + return ""; +} + + +/** + * Set the clipboard targets to reflect the mimetypes Inkscape can output. + */ +void ClipboardManagerImpl::_setClipboardTargets() +{ + Inkscape::Extension::DB::OutputList outlist; + Inkscape::Extension::db.get_output_list(outlist); + std::vector<Gtk::TargetEntry> target_list; + + bool plaintextSet = false; + for (Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin() ; out != outlist.end() ; ++out) { + if ( !(*out)->deactivated() ) { + Glib::ustring mime = (*out)->get_mimetype(); + if (mime != CLIPBOARD_TEXT_TARGET) { + if ( !plaintextSet && (mime.find("svg") == Glib::ustring::npos) ) { + target_list.emplace_back(CLIPBOARD_TEXT_TARGET); + plaintextSet = true; + } + target_list.emplace_back(mime); + } + } + } + + // Add PNG export explicitly since there is no extension for this... + // On Windows, GTK will also present this as a CF_DIB/CF_BITMAP + target_list.emplace_back( "image/png" ); + + _clipboard->set(target_list, + sigc::mem_fun(*this, &ClipboardManagerImpl::_onGet), + sigc::mem_fun(*this, &ClipboardManagerImpl::_onClear)); + +#ifdef _WIN32 + // If the "image/x-emf" target handled by the emf extension would be + // presented as a CF_ENHMETAFILE automatically (just like an "image/bmp" + // is presented as a CF_BITMAP) this code would not be needed.. ??? + // Or maybe there is some other way to achieve the same? + + // Note: Metafile is the only format that is rendered and stored in clipboard + // on Copy, all other formats are rendered only when needed by a Paste command. + + // FIXME: This should at least be rewritten to use "delayed rendering". + // If possible make it delayed rendering by using GTK API only. + + if (OpenClipboard(NULL)) { + if ( _clipboardSPDoc != NULL ) { + const Glib::ustring target = "image/x-emf"; + + 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() ) { + // FIXME: Temporary hack until we add support for memory output. + // Save to a temporary file, read it back and then set the clipboard contents + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-export.emf", nullptr ); + + try { + (*out)->save(_clipboardSPDoc.get(), filename); + HENHMETAFILE hemf = GetEnhMetaFileA(filename); + if (hemf) { + SetClipboardData(CF_ENHMETAFILE, hemf); + DeleteEnhMetaFile(hemf); + } + } catch (...) { + } + g_unlink(filename); // delete the temporary file + g_free(filename); + } + } + CloseClipboard(); + } +#endif +} + + +/** + * Set the string representation of a 32-bit RGBA color as the clipboard contents. + */ +void ClipboardManagerImpl::_setClipboardColor(guint32 color) +{ + gchar colorstr[16]; + g_snprintf(colorstr, 16, "%08x", color); + _clipboard->set_text(colorstr); +} + + +/** + * Put a notification on the message stack. + */ +void ClipboardManagerImpl::_userWarn(SPDesktop *desktop, char const *msg) +{ + if(desktop) + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, msg); +} + +/* ####################################### + ClipboardManager class + ####################################### */ + +ClipboardManager *ClipboardManager::_instance = nullptr; + +ClipboardManager::ClipboardManager() = default; +ClipboardManager::~ClipboardManager() = default; +ClipboardManager *ClipboardManager::get() +{ + if ( _instance == nullptr ) { + _instance = new ClipboardManagerImpl; + } + + return _instance; +} + +} // namespace Inkscape +} // namespace IO + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : |