diff options
Diffstat (limited to 'src/document.cpp')
-rw-r--r-- | src/document.cpp | 2190 |
1 files changed, 2190 insertions, 0 deletions
diff --git a/src/document.cpp b/src/document.cpp new file mode 100644 index 0000000..a7b1150 --- /dev/null +++ b/src/document.cpp @@ -0,0 +1,2190 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SPDocument manipulation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * MenTaLguY <mental@rydia.net> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2004-2005 MenTaLguY + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/** \class SPDocument + * SPDocument serves as the container of both model trees (agnostic XML + * and typed object tree), and implements all of the document-level + * functionality used by the program. Many document level operations, like + * load, save, print, export and so on, use SPDocument as their basic datatype. + * + * SPDocument implements undo and redo stacks and an id-based object + * dictionary. Thanks to unique id attributes, the latter can be used to + * map from the XML tree back to the object tree. + * + * SPDocument performs the basic operations needed for asynchronous + * update notification (SPObject ::modified virtual method), and implements + * the 'modified' signal, as well. + */ + + +#define noSP_DOCUMENT_DEBUG_IDLE +#define noSP_DOCUMENT_DEBUG_UNDO + +#include <vector> +#include <string> +#include <cstring> + +#include <2geom/transforms.h> + +#include "desktop.h" +#include "document-undo.h" +#include "event-log.h" +#include "file.h" +#include "id-clash.h" +#include "inkscape-version.h" +#include "inkscape.h" +#include "inkscape-window.h" +#include "profile-manager.h" +#include "rdf.h" + +#include "actions/actions-edit-document.h" +#include "actions/actions-undo-document.h" +#include "actions/actions-pages.h" + +#include "display/drawing.h" + +#include "3rdparty/adaptagrams/libavoid/router.h" + +#include "3rdparty/libcroco/cr-parser.h" +#include "3rdparty/libcroco/cr-sel-eng.h" +#include "3rdparty/libcroco/cr-selector.h" + +#include "io/dir-util.h" +#include "layer-manager.h" +#include "page-manager.h" +#include "live_effects/lpeobject.h" +#include "object/persp3d.h" +#include "object/sp-defs.h" +#include "object/sp-factory.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" +#include "object/sp-symbol.h" +#include "object/sp-page.h" + +#include "widgets/desktop-widget.h" + +#include "xml/croco-node-iface.h" +#include "xml/rebase-hrefs.h" +#include "xml/simple-document.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +// Higher number means lower priority. +#define SP_DOCUMENT_UPDATE_PRIORITY (G_PRIORITY_HIGH_IDLE - 2) + +// Should have a lower priority than SP_DOCUMENT_UPDATE_PRIORITY, +// since we want it to happen when there are no more updates. +#define SP_DOCUMENT_REROUTING_PRIORITY (G_PRIORITY_HIGH_IDLE - 1) + +bool sp_no_convert_text_baseline_spacing = false; + +//gboolean sp_document_resource_list_free(gpointer key, gpointer value, gpointer data); + +static gint doc_count = 0; +static gint doc_mem_count = 0; + +static unsigned long next_serial = 0; + +SPDocument::SPDocument() : + keepalive(false), + virgin(true), + rdoc(nullptr), + rroot(nullptr), + root(nullptr), + style_cascade(cr_cascade_new(nullptr, nullptr, nullptr)), + document_filename(nullptr), + document_base(nullptr), + document_name(nullptr), + actionkey(), + _event_log(new Inkscape::EventLog(this)), + profileManager(nullptr), // deferred until after other initialization + router(new Avoid::Router(Avoid::PolyLineRouting|Avoid::OrthogonalRouting)), + _selection(new Inkscape::Selection(this)), + oldSignalsConnected(false), + current_persp3d(nullptr), + current_persp3d_impl(nullptr), + _parent_document(nullptr), + _node_cache_valid(false), + _activexmltree(nullptr) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (!prefs->getBool("/options/yaxisdown", true)) { + _doc2dt[3] = -1; + } + + // Penalise libavoid for choosing paths with needless extra segments. + // This results in much better looking orthogonal connector paths. + router->setRoutingPenalty(Avoid::segmentPenalty); + + _serial = next_serial++; + + sensitive = false; + partial = nullptr; + history_size = 0; + seeking = false; + + // Once things are set, hook in the manager + profileManager = new Inkscape::ProfileManager(this); + + // For undo/redo + undoStackObservers.add(*_event_log); + + // XXX only for testing! + undoStackObservers.add(console_output_undo_observer); + _node_cache = std::deque<SPItem*>(); + + // Actions + action_group = Gio::SimpleActionGroup::create(); + add_actions_edit_document(this); + add_actions_pages(this); + add_actions_undo_document(this); + + _page_manager = std::make_unique<Inkscape::PageManager>(this); +} + +SPDocument::~SPDocument() { + destroySignal.emit(); + + // kill/unhook this first + if ( profileManager ) { + delete profileManager; + profileManager = nullptr; + } + + if (router) { + delete router; + router = nullptr; + } + + if (oldSignalsConnected) { + selChangeConnection.disconnect(); + desktopActivatedConnection.disconnect(); + } else { + _selection_changed_connection.disconnect(); + _desktop_activated_connection.disconnect(); + } + + if (partial) { + sp_repr_free_log(partial); + partial = nullptr; + } + + DocumentUndo::clearRedo(this); + DocumentUndo::clearUndo(this); + + if (root) { + root->releaseReferences(); + sp_object_unref(root); + root = nullptr; + } + + if (rdoc) Inkscape::GC::release(rdoc); + + /* Free resources */ + resources.clear(); + + // This also destroys all attached stylesheets + cr_cascade_unref(style_cascade); + style_cascade = nullptr; + + if (document_name) { + g_free(document_name); + document_name = nullptr; + } + if (document_base) { + g_free(document_base); + document_base = nullptr; + } + if (document_filename) { + g_free(document_filename); + document_filename = nullptr; + } + + modified_connection.disconnect(); + rerouting_connection.disconnect(); + + if (keepalive) { + inkscape_unref(INKSCAPE); + keepalive = false; + } + + if (this->current_persp3d_impl) + delete this->current_persp3d_impl; + this->current_persp3d_impl = nullptr; + + if (_event_log) { + delete _event_log; + } + + // This is at the end of the destructor, because preceding code adds new orphans to the queue + collectOrphans(); +} + +Inkscape::XML::Node *SPDocument::getReprNamedView() +{ + return sp_repr_lookup_name (rroot, "sodipodi:namedview"); +} + +/** + * Get the namedview for this document, creates it if it's not found. + * + * @returns SPNamedView object, existing or created. + */ +SPNamedView *SPDocument::getNamedView() +{ + auto xml = getReprNamedView(); + if (!xml) { + xml = rdoc->createElement("sodipodi:namedview"); + rroot->addChildAtPos(xml, 0); + } + return dynamic_cast<SPNamedView *> (getObjectByRepr(xml)); +} + +SPDefs *SPDocument::getDefs() +{ + if (!root) { + return nullptr; + } + return root->defs; +} + +Persp3D *SPDocument::getCurrentPersp3D() { + // Check if current_persp3d is still valid + std::vector<Persp3D*> plist; + getPerspectivesInDefs(plist); + for (auto & i : plist) { + if (current_persp3d == i) + return current_persp3d; + } + + // If not, return the first perspective in defs (which may be NULL of none exists) + current_persp3d = Persp3D::document_first_persp (this); + + return current_persp3d; +} + +void SPDocument::setCurrentPersp3D(Persp3D * const persp) { + current_persp3d = persp; + //current_persp3d_impl = persp->perspective_impl; +} + +void SPDocument::getPerspectivesInDefs(std::vector<Persp3D*> &list) const +{ + for (auto& i: root->defs->children) { + if (SP_IS_PERSP3D(&i)) { + list.push_back(SP_PERSP3D(&i)); + } + } +} + +/** +void SPDocument::initialize_current_persp3d() +{ + this->current_persp3d = Persp3D::document_first_persp(this); + if (!this->current_persp3d) { + this->current_persp3d = Persp3D::create_xml_element(this); + } +} +**/ + +/** + * Enables or disables document pages, usually used in import code. + */ +void SPDocument::setPages(bool enabled) +{ + if (enabled) { + _page_manager->enablePages(); + } else { + _page_manager->disablePages(); + } +} + +void SPDocument::queueForOrphanCollection(SPObject *object) { + g_return_if_fail(object != nullptr); + g_return_if_fail(object->document == this); + + sp_object_ref(object, nullptr); + _collection_queue.push_back(object); +} + +void SPDocument::collectOrphans() { + while (!_collection_queue.empty()) { + std::vector<SPObject *> objects(_collection_queue); + _collection_queue.clear(); + for (auto object : objects) { + object->collectOrphan(); + sp_object_unref(object, nullptr); + } + } +} + +SPDocument *SPDocument::createDoc(Inkscape::XML::Document *rdoc, + gchar const *filename, + gchar const *document_base, + gchar const *document_name, + bool keepalive, + SPDocument *parent) +{ + SPDocument *document = new SPDocument(); + + Inkscape::XML::Node *rroot = rdoc->root(); + + document->keepalive = keepalive; + + document->rdoc = rdoc; + document->rroot = rroot; + if (parent) { + document->_parent_document = parent; + parent->_child_documents.push_back(document); + } + + if (document->document_filename){ + g_free(document->document_filename); + document->document_filename = nullptr; + } + if (document->document_base){ + g_free(document->document_base); + document->document_base = nullptr; + } + if (document->document_name){ + g_free(document->document_name); + document->document_name = nullptr; + } +#ifndef _WIN32 + document->document_filename = prepend_current_dir_if_relative(filename); +#else + // FIXME: it may be that prepend_current_dir_if_relative works OK on windows too, test! + document->document_filename = filename? g_strdup(filename) : NULL; +#endif + + // base is simply the part of the path before filename; e.g. when running "inkscape ../file.svg" the base is "../" + // which is why we use g_get_current_dir() in calculating the abs path above + //This is NULL for a new document + if (document_base) { + document->document_base = g_strdup(document_base); + } else { + document->document_base = nullptr; + } + document->document_name = g_strdup(document_name); + + // Create SPRoot element + const std::string typeString = NodeTraits::get_type_string(*rroot); + SPObject* rootObj = SPFactory::createObject(typeString); + document->root = dynamic_cast<SPRoot*>(rootObj); + + if (document->root == nullptr) { + // Node is not a valid root element + delete rootObj; + + // fixme: what to do here? + throw; + } + + // Recursively build object tree + document->root->invoke_build(document, rroot, false); + + /* Eliminate obsolete sodipodi:docbase, for privacy reasons */ + rroot->removeAttribute("sodipodi:docbase"); + + /* Eliminate any claim to adhere to a profile, as we don't try to */ + rroot->removeAttribute("baseProfile"); + + // loading or creating namedview. + auto nv = document->getNamedView(); + + // Set each of the defaults in new or existing namedview (allows for per-attr overriding) + nv->setDefaultAttribute("pagecolor", "/template/base/pagecolor", ""); + nv->setDefaultAttribute("bordercolor", "/template/base/bordercolor", ""); + nv->setDefaultAttribute("borderopacity", "/template/base/borderopacity", ""); + nv->setDefaultAttribute("inkscape:showpageshadow", "/template/base/pageshadow", "2"); + nv->setDefaultAttribute("inkscape:pageopacity", "/template/base/pageopacity", "0.0"); + nv->setDefaultAttribute("inkscape:pagecheckerboard", "/template/base/pagecheckerboard", "0"); + if (!nv->getAttribute("inkscape:deskcolor")) { + auto page = nv->getAttribute("pagecolor"); + auto color = "#d1d1d1"; // default gray desk + if (page && strcasecmp(page, "#ffffff") != 0) { + color = page; + } + nv->setDefaultAttribute("inkscape:deskcolor", "/template/base/deskcolor", color); + } + + // If no units are set in the document, try and guess them from the width/height + // XXX Replace these calls with nv->setDocumentUnit(document->root->width.getUnit()); + if (document->root->width.isAbsolute()) { + nv->setDefaultAttribute("inkscape:document-units", "", document->root->width.getUnit()); + } else if (document->root->height.isAbsolute()) { + nv->setDefaultAttribute("inkscape:document-units", "", document->root->height.getUnit()); + } + + // Defs + if (!document->root->defs) { + Inkscape::XML::Node *r = rdoc->createElement("svg:defs"); + rroot->addChild(r, nullptr); + Inkscape::GC::release(r); + g_assert(document->root->defs); + } + + /* Default RDF */ + rdf_set_defaults( document ); + + if (keepalive) { + inkscape_ref(INKSCAPE); + } + + // Check if the document already has a perspective (e.g., when opening an existing + // document). If not, create a new one and set it as the current perspective. + document->setCurrentPersp3D(Persp3D::document_first_persp(document)); + if (!document->getCurrentPersp3D()) { + //document->setCurrentPersp3D(Persp3D::create_xml_element (document)); + Persp3DImpl *persp_impl = new Persp3DImpl(); + document->setCurrentPersp3DImpl(persp_impl); + } + + DocumentUndo::setUndoSensitive(document, true); + + // reset undo key when selection changes, so that same-key actions on different objects are not coalesced + document->selChangeConnection = INKSCAPE.signal_selection_changed.connect( + sigc::hide(sigc::bind( + sigc::ptr_fun(&DocumentUndo::resetKey), document) + )); + document->desktopActivatedConnection = INKSCAPE.signal_activate_desktop.connect( + sigc::hide(sigc::bind( + sigc::ptr_fun(&DocumentUndo::resetKey), document) + )); + document->oldSignalsConnected = true; + + + // ************* Fix Document ************** + // Move to separate function? + + /** Fix baseline spacing (pre-92 files) **/ + if ( (!sp_no_convert_text_baseline_spacing) + && sp_version_inside_range( document->root->version.inkscape, 0, 1, 0, 92 ) ) { + sp_file_convert_text_baseline_spacing(document); + } + + /** Fix font names in legacy documents (pre-92 files) **/ + if ( sp_version_inside_range( document->root->version.inkscape, 0, 1, 0, 92 ) ) { + sp_file_convert_font_name(document); + } + + /** Fix first line spacing in legacy documents (pre-1.0 files) **/ + if (sp_version_inside_range(document->root->version.inkscape, 0, 1, 1, 0)) { + sp_file_fix_empty_lines(document); + } + + /** Fix OSB (pre-1.1 files) **/ + if (sp_version_inside_range(document->root->version.inkscape, 0, 1, 1, 1)) { + sp_file_fix_osb(document->getRoot()); + } + + /** Fix feComposite (pre-1.2 files) **/ + if (sp_version_inside_range(document->root->version.inkscape, 0, 1, 1, 2)) { + sp_file_fix_feComposite(document->getRoot()); + } + + + /** Fix dpi (pre-92 files). With GUI fixed in Inkscape::Application::fix_document. **/ + if ( !(INKSCAPE.use_gui()) && sp_version_inside_range( document->root->version.inkscape, 0, 1, 0, 92 ) ) { + sp_file_convert_dpi(document); + } + + // Update document level action settings + // -- none available so far -- + + return document; +} + +/** + * Create a copy of the document, useful for modifying during save & export. + */ +std::unique_ptr<SPDocument> SPDocument::copy() const +{ + // New SimpleDocument object where we will put all the same data + Inkscape::XML::Document *new_rdoc = new Inkscape::XML::SimpleDocument(); + + // Duplicate the svg root node AND any PI and COMMENT nodes, this should be put + // into xml/simple-document.h at some point to fix it's duplicate implementation. + for (Inkscape::XML::Node *child = rdoc->firstChild(); child; child = child->next()) { + if (child) { + // Get a new xml repr for the svg root node + Inkscape::XML::Node *new_child = child->duplicate(new_rdoc); + + // Add the duplicated svg node as the document's rdoc + new_rdoc->appendChild(new_child); + Inkscape::GC::release(new_child); + } + } + + auto doc = createDoc(new_rdoc, document_filename, document_base, document_name, keepalive, nullptr); + doc->_original_document = this; + + return doc->doRef(); +} + +/** + * Fetches a document and attaches it to the current document as a child href + */ +SPDocument *SPDocument::createChildDoc(std::string const &filename) +{ + SPDocument *parent = this; + SPDocument *document = nullptr; + + while(parent != nullptr && parent->getDocumentFilename() != nullptr && document == nullptr) { + // Check myself and any parents in the chain + if(filename == parent->getDocumentFilename()) { + document = parent; + break; + } + // Then check children of those. + boost::ptr_list<SPDocument>::iterator iter; + for (iter = parent->_child_documents.begin(); + iter != parent->_child_documents.end(); ++iter) { + if(filename == iter->getDocumentFilename()) { + document = &*iter; + break; + } + } + parent = parent->_parent_document; + } + + // Load a fresh document from the svg source. + if(!document) { + std::string path; + if (Glib::path_is_absolute(filename)) { + path = filename; + } else { + path = this->getDocumentBase() + filename; + } + document = createNewDoc(path.c_str(), false, false, this); + } + return document; +} +/** + * Fetches document from filename, or creates new, if NULL; public document + * appears in document list. + */ +SPDocument *SPDocument::createNewDoc(gchar const *filename, bool keepalive, bool make_new, SPDocument *parent) +{ + Inkscape::XML::Document *rdoc = nullptr; + gchar *document_base = nullptr; + gchar *document_name = nullptr; + + if (filename) { + Inkscape::XML::Node *rroot; + /* Try to fetch repr from file */ + rdoc = sp_repr_read_file(filename, SP_SVG_NS_URI); + /* If file cannot be loaded, return NULL without warning */ + if (rdoc == nullptr) return nullptr; + rroot = rdoc->root(); + /* If xml file is not svg, return NULL without warning */ + /* fixme: destroy document */ + if (strcmp(rroot->name(), "svg:svg") != 0) return nullptr; + + // Opening a template that points to a sister file should still work + // this also includes tutorials which point to png files. + document_base = g_path_get_dirname(filename); + + if (make_new) { + filename = nullptr; + document_name = g_strdup_printf(_("New document %d"), ++doc_count); + } else { + document_name = g_path_get_basename(filename); + if (strcmp(document_base, ".") == 0) { + g_free(document_base); + document_base = nullptr; + } + } + } else { + if (make_new) { + document_name = g_strdup_printf(_("Memory document %d"), ++doc_mem_count); + } + + rdoc = sp_repr_document_new("svg:svg"); + } + + //# These should be set by now + g_assert(document_name); + + SPDocument *doc = createDoc(rdoc, filename, document_base, document_name, keepalive, parent); + + g_free(document_base); + g_free(document_name); + + return doc; +} + +SPDocument *SPDocument::createNewDocFromMem(gchar const *buffer, gint length, bool keepalive) +{ + SPDocument *doc = nullptr; + + Inkscape::XML::Document *rdoc = sp_repr_read_mem(buffer, length, SP_SVG_NS_URI); + if ( rdoc ) { + // Only continue to create a non-null doc if it could be loaded + Inkscape::XML::Node *rroot = rdoc->root(); + if ( strcmp(rroot->name(), "svg:svg") != 0 ) { + // If xml file is not svg, return NULL without warning + // TODO fixme: destroy document + } else { + Glib::ustring document_name = Glib::ustring::compose( _("Memory document %1"), ++doc_mem_count ); + doc = createDoc(rdoc, nullptr, nullptr, document_name.c_str(), keepalive, nullptr); + } + } + + return doc; +} + +std::unique_ptr<SPDocument> SPDocument::doRef() +{ + Inkscape::GC::anchor(this); + return std::unique_ptr<SPDocument>(this); +} +std::unique_ptr<SPDocument const> SPDocument::doRef() const +{ + return const_cast<SPDocument*>(this)->doRef(); +} + +/// guaranteed not to return nullptr +Inkscape::Util::Unit const* SPDocument::getDisplayUnit() +{ + return getNamedView()->getDisplayUnit(); +} + +/// Sets document scale (by changing viewBox) +void SPDocument::setDocumentScale(double scaleX, double scaleY) { + if (scaleX <= 0 || scaleY <= 0) { + g_warning("%s: Invalid scale, has to be positive: %f, %f", __func__, scaleX, scaleY); + return; + } + + // since scale is doc size / viewbox size, then it follows that viewbox size is doc size / scale + root->viewBox = Geom::Rect::from_xywh( + root->viewBox.left(), + root->viewBox.top(), + root->width.computed / scaleX, + root->height.computed / scaleY); + root->viewBox_set = true; + root->updateRepr(); +} + +/// Sets document scale (by changing viewBox, x and y scaling equal) +void SPDocument::setDocumentScale(double scale) { + setDocumentScale(scale, scale); +} + +/// Returns document scale as defined by width/height (in pixels) and viewBox (real world to +/// user-units). +Geom::Scale SPDocument::getDocumentScale() const +{ + Geom::Scale scale; + if( root->viewBox_set ) { + double scale_x = 1.0; + double scale_y = 1.0; + if( root->viewBox.width() > 0.0 ) { + scale_x = root->width.computed / root->viewBox.width(); + } + if( root->viewBox.height() > 0.0 ) { + scale_y = root->height.computed / root->viewBox.height(); + } + scale = Geom::Scale(scale_x, scale_y); + } + // std::cout << "SPDocument::getDocumentScale():\n" << scale << std::endl; + return scale; +} + +// Avoid calling root->updateRepr() twice by combining setting width and height. +// (As done on every delete as clipboard calls this via fitToRect()) +void SPDocument::setWidthAndHeight(const Inkscape::Util::Quantity &width, const Inkscape::Util::Quantity &height, bool changeSize) +{ + Inkscape::Util::Unit const *old_width_units = unit_table.getUnit("px"); + if (root->width.unit) + old_width_units = unit_table.getUnit(root->width.unit); + gdouble old_width_converted; // old width converted to new units + if (root->width.unit == SVGLength::PERCENT) + old_width_converted = Inkscape::Util::Quantity::convert(root->width.computed, "px", width.unit); + else + old_width_converted = Inkscape::Util::Quantity::convert(root->width.value, old_width_units, width.unit); + + root->width.computed = width.value("px"); + root->width.value = width.quantity; + root->width.unit = (SVGLength::Unit) width.unit->svgUnit(); + + Inkscape::Util::Unit const *old_height_units = unit_table.getUnit("px"); + if (root->height.unit) + old_height_units = unit_table.getUnit(root->height.unit); + gdouble old_height_converted; // old height converted to new units + if (root->height.unit == SVGLength::PERCENT) + old_height_converted = Inkscape::Util::Quantity::convert(root->height.computed, "px", height.unit); + else + old_height_converted = Inkscape::Util::Quantity::convert(root->height.value, old_height_units, height.unit); + + root->height.computed = height.value("px"); + root->height.value = height.quantity; + root->height.unit = (SVGLength::Unit) height.unit->svgUnit(); + + // viewBox scaled by relative change in page size (maintains document scale). + if (root->viewBox_set && changeSize) { + root->viewBox.setMax(Geom::Point( + root->viewBox.left() + (root->width.value / old_width_converted ) * root->viewBox.width(), + root->viewBox.top() + (root->height.value / old_height_converted) * root->viewBox.height())); + } + root->updateRepr(); +} + +Inkscape::Util::Quantity SPDocument::getWidth() const +{ + g_return_val_if_fail(this->root != nullptr, Inkscape::Util::Quantity(0.0, unit_table.getUnit(""))); + + gdouble result = root->width.value; + SVGLength::Unit u = root->width.unit; + if (root->width.unit == SVGLength::PERCENT && root->viewBox_set) { + result = root->viewBox.width(); + u = SVGLength::PX; + } + if (u == SVGLength::NONE) { + u = SVGLength::PX; + } + return Inkscape::Util::Quantity(result, unit_table.getUnit(u)); +} + +void SPDocument::setWidth(const Inkscape::Util::Quantity &width, bool changeSize) +{ + Inkscape::Util::Unit const *old_width_units = unit_table.getUnit("px"); + if (root->width.unit) + old_width_units = unit_table.getUnit(root->width.unit); + gdouble old_width_converted; // old width converted to new units + if (root->width.unit == SVGLength::PERCENT) + old_width_converted = Inkscape::Util::Quantity::convert(root->width.computed, "px", width.unit); + else + old_width_converted = Inkscape::Util::Quantity::convert(root->width.value, old_width_units, width.unit); + + root->width.computed = width.value("px"); + root->width.value = width.quantity; + root->width.unit = (SVGLength::Unit) width.unit->svgUnit(); + + if (root->viewBox_set && changeSize) + root->viewBox.setMax(Geom::Point(root->viewBox.left() + (root->width.value / old_width_converted) * root->viewBox.width(), root->viewBox.bottom())); + + root->updateRepr(); +} + + +Inkscape::Util::Quantity SPDocument::getHeight() const +{ + g_return_val_if_fail(this->root != nullptr, Inkscape::Util::Quantity(0.0, unit_table.getUnit(""))); + + gdouble result = root->height.value; + SVGLength::Unit u = root->height.unit; + if (root->height.unit == SVGLength::PERCENT && root->viewBox_set) { + result = root->viewBox.height(); + u = SVGLength::PX; + } + if (u == SVGLength::NONE) { + u = SVGLength::PX; + } + return Inkscape::Util::Quantity(result, unit_table.getUnit(u)); +} + +void SPDocument::setHeight(const Inkscape::Util::Quantity &height, bool changeSize) +{ + Inkscape::Util::Unit const *old_height_units = unit_table.getUnit("px"); + if (root->height.unit) + old_height_units = unit_table.getUnit(root->height.unit); + gdouble old_height_converted; // old height converted to new units + if (root->height.unit == SVGLength::PERCENT) + old_height_converted = Inkscape::Util::Quantity::convert(root->height.computed, "px", height.unit); + else + old_height_converted = Inkscape::Util::Quantity::convert(root->height.value, old_height_units, height.unit); + + root->height.computed = height.value("px"); + root->height.value = height.quantity; + root->height.unit = (SVGLength::Unit) height.unit->svgUnit(); + + if (root->viewBox_set && changeSize) + root->viewBox.setMax(Geom::Point(root->viewBox.right(), root->viewBox.top() + (root->height.value / old_height_converted) * root->viewBox.height())); + + root->updateRepr(); +} + +const Geom::Affine &SPDocument::doc2dt() const +{ + if (root && !is_yaxisdown()) { + _doc2dt[5] = root->height.computed; + } + + return _doc2dt; +} + +Geom::Rect SPDocument::getViewBox() const +{ + Geom::Rect viewBox; + if (root->viewBox_set) { + viewBox = root->viewBox; + } else { + viewBox = Geom::Rect::from_xywh( 0, 0, getWidth().value("px"), getHeight().value("px")); + } + return viewBox; +} + +/** + * Set default viewbox calculated from document properties. + */ +void SPDocument::setViewBox() +{ + setViewBox(Geom::Rect(0, + 0, + getWidth().value(getDisplayUnit()), + getHeight().value(getDisplayUnit()))); +} + +void SPDocument::setViewBox(const Geom::Rect &viewBox) +{ + root->viewBox_set = true; + root->viewBox = viewBox; + root->updateRepr(); +} + +Geom::Point SPDocument::getDimensions() const +{ + return Geom::Point(getWidth().value("px"), getHeight().value("px")); +} + +Geom::OptRect SPDocument::preferredBounds() const +{ + return Geom::OptRect( Geom::Point(0, 0), getDimensions() ); +} + +/** + * Returns the position of the selected page or the preferredBounds() + */ +Geom::OptRect SPDocument::pageBounds() +{ + if (auto page = _page_manager->getSelected()) { + return page->getDesktopRect(); + } + return preferredBounds(); +} + +/** + * Given a Geom::Rect that may, for example, correspond to the bbox of an object, + * this function fits the canvas to that rect by resizing the canvas + * and translating the document root into position. + * \param rect fit document size to this, in document coordinates + * \param with_margins add margins to rect, by taking margins from this + * document's namedview (<sodipodi:namedview> "fit-margin-..." + * attributes, and "units") + */ +void SPDocument::fitToRect(Geom::Rect const &rect, bool with_margins) +{ + double const w = rect.width(); + double const h = rect.height(); + + Inkscape::Util::Unit const *nv_units = unit_table.getUnit("px"); + if (root->height.unit && (root->height.unit != SVGLength::PERCENT)) + nv_units = unit_table.getUnit(root->height.unit); + SPNamedView *nv = this->getNamedView(); + + /* in px */ + double margin_top = 0.0; + double margin_left = 0.0; + double margin_right = 0.0; + double margin_bottom = 0.0; + + if (with_margins && nv) { + margin_top = nv->getMarginLength("fit-margin-top", nv_units, unit_table.getUnit("px"), w, h, false); + margin_left = nv->getMarginLength("fit-margin-left", nv_units, unit_table.getUnit("px"), w, h, true); + margin_right = nv->getMarginLength("fit-margin-right", nv_units, unit_table.getUnit("px"), w, h, true); + margin_bottom = nv->getMarginLength("fit-margin-bottom", nv_units, unit_table.getUnit("px"), w, h, false); + margin_top = Inkscape::Util::Quantity::convert(margin_top, nv_units, "px"); + margin_left = Inkscape::Util::Quantity::convert(margin_left, nv_units, "px"); + margin_right = Inkscape::Util::Quantity::convert(margin_right, nv_units, "px"); + margin_bottom = Inkscape::Util::Quantity::convert(margin_bottom, nv_units, "px"); + } + + Geom::Rect const rect_with_margins( + rect.min() - Geom::Point(margin_left, margin_top), + rect.max() + Geom::Point(margin_right, margin_bottom)); + + // rect in desktop coordinates before changing document dimensions + auto rect_with_margins_dt_old = rect_with_margins * doc2dt(); + + setWidthAndHeight( + Inkscape::Util::Quantity(Inkscape::Util::Quantity::convert(rect_with_margins.width(), "px", nv_units), nv_units), + Inkscape::Util::Quantity(Inkscape::Util::Quantity::convert(rect_with_margins.height(), "px", nv_units), nv_units) + ); + + // rect in desktop coordinates after changing document dimensions + auto rect_with_margins_dt_new = rect_with_margins * doc2dt(); + + bool y_down = is_yaxisdown(); + + // Translate drawing contents to the origin + double const box_x = rect_with_margins_dt_new[Geom::X].min(); + double const box_y = y_down ? rect_with_margins_dt_new[Geom::Y].min() + : rect_with_margins_dt_new.height() - rect_with_margins_dt_old[Geom::Y].max(); + auto const corrective_translation = Geom::Translate(-box_x, -box_y); + root->translateChildItems(corrective_translation); + + if (nv) { + // Calculate the required translation of on-canvas gadgets + double const delta_x = rect_with_margins_dt_old[Geom::X].min(); + double const delta_y = y_down ? rect_with_margins_dt_old[Geom::Y].min() + : rect_with_margins_dt_new.height() - rect_with_margins_dt_new[Geom::Y].max(); + auto const on_canvas_gadget_translation = Geom::Translate(-delta_x, -delta_y); + + nv->translateGuides(on_canvas_gadget_translation); + nv->translateGrids(on_canvas_gadget_translation); + _page_manager->movePages(corrective_translation); + nv->scrollAllDesktops(delta_x, delta_y * yaxisdir(), false); + } +} + +void SPDocument::setDocumentBase( gchar const* document_base ) +{ + if (this->document_base) { + g_free(this->document_base); + this->document_base = nullptr; + } + if (document_base) { + this->document_base = g_strdup(document_base); + } +} + +void SPDocument::do_change_filename(gchar const *const filename, bool const rebase) +{ + gchar *new_document_base = nullptr; + gchar *new_document_name = nullptr; + gchar *new_document_filename = nullptr; + if (filename) { + +#ifndef _WIN32 + new_document_filename = prepend_current_dir_if_relative(filename); +#else + // FIXME: it may be that prepend_current_dir_if_relative works OK on windows too, test! + new_document_filename = g_strdup(filename); +#endif + + new_document_base = g_path_get_dirname(new_document_filename); + new_document_name = g_path_get_basename(new_document_filename); + } else { + new_document_filename = g_strdup_printf(_("Unnamed document %d"), ++doc_count); + new_document_base = nullptr; + new_document_name = g_strdup(this->document_filename); + } + + // Update saveable repr attributes. + Inkscape::XML::Node *repr = getReprRoot(); + + // Changing filename in the document repr must not be not undoable. + bool const saved = DocumentUndo::getUndoSensitive(this); + DocumentUndo::setUndoSensitive(this, false); + + if (rebase) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool use_sodipodi_absref = prefs->getBool("/options/svgoutput/usesodipodiabsref", false); + Inkscape::XML::rebase_hrefs(this, new_document_base, use_sodipodi_absref); + } + + if (strncmp(new_document_name, "ink_ext_XXXXXX", 14)) // do not use temporary filenames + repr->setAttribute("sodipodi:docname", new_document_name); + DocumentUndo::setUndoSensitive(this, saved); + + + g_free(this->document_name); + g_free(this->document_base); + g_free(this->document_filename); + this->document_name = new_document_name; + this->document_base = new_document_base; + this->document_filename = new_document_filename; + + this->filename_set_signal.emit(this->document_filename); +} + +/** + * Sets base, name and filename members of \a document. Doesn't update + * any relative hrefs in the document: thus, this is primarily for + * newly-created documents. + * + * \see SPDocument::changeFilenameAndHrefs + */ +void SPDocument::setDocumentFilename(gchar const *filename) +{ + do_change_filename(filename, false); +} + +/** + * Changes the base, name and filename members of \a document, and updates any + * relative hrefs in the document to be relative to the new base. + */ +void SPDocument::changeFilenameAndHrefs(gchar const *filename) +{ + do_change_filename(filename, true); +} + +void SPDocument::bindObjectToId(gchar const *id, SPObject *object) { + GQuark idq = g_quark_from_string(id); + + if (object) { + if(object->getId()) + iddef.erase(object->getId()); + g_assert(iddef.find(id)==iddef.end()); + iddef[id] = object; + } else { + g_assert(iddef.find(id)!=iddef.end()); + iddef.erase(id); + } + + SPDocument::IDChangedSignalMap::iterator pos; + + pos = id_changed_signals.find(idq); + if ( pos != id_changed_signals.end() ) { + if (!(*pos).second.empty()) { + (*pos).second.emit(object); + } else { // discard unused signal + id_changed_signals.erase(pos); + } + } +} + +SPObject *SPDocument::getObjectById(Glib::ustring const &id) const +{ + if (iddef.empty()) { + return nullptr; + } + + std::map<std::string, SPObject *>::const_iterator rv = iddef.find(id); + if (rv != iddef.end()) { + return (rv->second); + } else if (_parent_document) { + return _parent_document->getObjectById(id); + } + else if (_ref_document) { + return _ref_document->getObjectById(id); + } else { + return nullptr; + } +} + +SPObject *SPDocument::getObjectById(gchar const *id) const +{ + if (id == nullptr) { + return nullptr; + } + + return getObjectById(Glib::ustring(id)); +} + +SPObject *SPDocument::getObjectByHref(Glib::ustring const &href) const +{ + if (iddef.empty()) { + return nullptr; + } + Glib::ustring id = href; + id = id.erase(0, 1); + return getObjectById(id); +} + +SPObject *SPDocument::getObjectByHref(gchar const *href) const +{ + if (href == nullptr) { + return nullptr; + } + + return getObjectByHref(Glib::ustring(href)); +} + +void _getObjectsByClassRecursive(Glib::ustring const &klass, SPObject *parent, std::vector<SPObject *> &objects) +{ + if (parent) { + char const *temp = parent->getAttribute("class"); + if (temp) { + std::istringstream classes(temp); + Glib::ustring token; + while (classes >> token) { + // we can have multiple class + if (classes.str() == " ") { + token = ""; + continue; + } + if (token == klass) { + objects.push_back(parent); + break; + } + } + } + + // Check children + for (auto& child : parent->children) { + _getObjectsByClassRecursive( klass, &child, objects ); + } + } +} + +std::vector<SPObject *> SPDocument::getObjectsByClass(Glib::ustring const &klass) const +{ + std::vector<SPObject *> objects; + g_return_val_if_fail(!klass.empty(), objects); + + _getObjectsByClassRecursive(klass, root, objects); + return objects; +} + +void _getObjectsByElementRecursive(Glib::ustring const &element, SPObject *parent, std::vector<SPObject *> &objects, + bool custom) +{ + if (parent) { + Glib::ustring prefixed = custom ? "inkscape:" : "svg:"; + prefixed += element; + if (parent->getRepr()->name() == prefixed) { + objects.push_back(parent); + } + + // Check children + for (auto& child : parent->children) { + _getObjectsByElementRecursive(element, &child, objects, custom); + } + } +} + +std::vector<SPObject *> SPDocument::getObjectsByElement(Glib::ustring const &element, bool custom) const +{ + std::vector<SPObject *> objects; + g_return_val_if_fail(!element.empty(), objects); + + _getObjectsByElementRecursive(element, root, objects, custom); + return objects; +} + +void _getObjectsBySelectorRecursive(SPObject *parent, + CRSelEng *sel_eng, CRSimpleSel *simple_sel, + std::vector<SPObject *> &objects) +{ + if (parent) { + gboolean result = false; + cr_sel_eng_matches_node( sel_eng, simple_sel, parent->getRepr(), &result ); + if (result) { + objects.push_back(parent); + } + + // Check children + for (auto& child : parent->children) { + _getObjectsBySelectorRecursive(&child, sel_eng, simple_sel, objects); + } + } +} + +std::vector<SPObject *> SPDocument::getObjectsBySelector(Glib::ustring const &selector) const +{ + // std::cout << "\nSPDocument::getObjectsBySelector: " << selector << std::endl; + + std::vector<SPObject *> objects; + g_return_val_if_fail(!selector.empty(), objects); + + static CRSelEng *sel_eng = nullptr; + if (!sel_eng) { + sel_eng = cr_sel_eng_new(&Inkscape::XML::croco_node_iface); + } + + CRSelector *cr_selector = cr_selector_parse_from_buf((guchar const *)selector.c_str(), CR_UTF_8); + // char * cr_string = (char*)cr_selector_to_string( cr_selector ); + // std::cout << " selector: |" << (cr_string?cr_string:"Empty") << "|" << std::endl; + CRSelector const *cur = nullptr; + for (cur = cr_selector; cur; cur = cur->next) { + if (cur->simple_sel ) { + _getObjectsBySelectorRecursive(root, sel_eng, cur->simple_sel, objects); + } + } + return objects; +} + +void SPDocument::bindObjectToRepr(Inkscape::XML::Node *repr, SPObject *object) +{ + if (object) { + g_assert(reprdef.find(repr)==reprdef.end()); + reprdef[repr] = object; + } else { + g_assert(reprdef.find(repr)!=reprdef.end()); + reprdef.erase(repr); + } +} + +SPObject *SPDocument::getObjectByRepr(Inkscape::XML::Node *repr) const +{ + g_return_val_if_fail(repr != nullptr, NULL); + std::map<Inkscape::XML::Node *, SPObject *>::const_iterator rv = reprdef.find(repr); + if(rv != reprdef.end()) + return (rv->second); + else + return nullptr; +} + +/** Returns preferred document languages (from most to least preferred) + * + * This currently includes (in order): + * - language set in RDF metadata + * - languages suitable for system locale (influenced by Inkscape GUI locale preference) + */ +std::vector<Glib::ustring> SPDocument::getLanguages() const +{ + std::vector<Glib::ustring> document_languages; + + // get language from RDF + gchar const *rdf_language = rdf_get_work_entity(this, rdf_find_entity("language")); + if (rdf_language) { + gchar *rdf_language_stripped = g_strstrip(g_strdup(rdf_language)); + if (strcmp(rdf_language_stripped, "") != 0) { + document_languages.emplace_back(rdf_language_stripped); + } + g_free(rdf_language_stripped); + } + + // add languages from parent document + if (_parent_document) { + auto parent_languages = _parent_document->getLanguages(); + + // return parent languages directly if we aren't contributing any + if (document_languages.empty()) { + return parent_languages; + } + + // otherwise append parent's languages to what we already have + std::move(parent_languages.begin(), parent_languages.end(), + std::back_insert_iterator(document_languages)); + + // don't add languages from locale; parent already did that + return document_languages; + } + + // get language from system locale (will also match the interface language preference as we set LANG accordingly) + // TODO: This includes locales with encodings like "de_DE.UTF-8" - is this useful or should we skip these? + // TODO: This includes the default "C" locale - is this useful or should we skip it? + const gchar * const * names = g_get_language_names(); + for (int i=0; names[i]; i++) { + document_languages.emplace_back(names[i]); + } + + return document_languages; +} + +/* Object modification root handler */ + +void SPDocument::requestModified() +{ + if (modified_connection.empty()) { + modified_connection = + Glib::signal_idle().connect(sigc::mem_fun(*this, &SPDocument::idle_handler), + SP_DOCUMENT_UPDATE_PRIORITY); + } + + if (rerouting_connection.empty()) { + rerouting_connection = + Glib::signal_idle().connect(sigc::mem_fun(*this, &SPDocument::rerouting_handler), + SP_DOCUMENT_REROUTING_PRIORITY); + } +} + +void SPDocument::setupViewport(SPItemCtx *ctx) +{ + ctx->flags = 0; + ctx->i2doc = Geom::identity(); + // Set up viewport in case svg has it defined as percentages + if (root->viewBox_set) { // if set, take from viewBox + ctx->viewport = root->viewBox; + } else { // as a last resort, set size to A4 + ctx->viewport = Geom::Rect::from_xywh(0, 0, Inkscape::Util::Quantity::convert(210, "mm", "px"), Inkscape::Util::Quantity::convert(297, "mm", "px")); + } + ctx->i2vp = Geom::identity(); +} + +/** + * Tries to update the document state based on the modified and + * "update required" flags, and return true if the document has + * been brought fully up to date. + */ +bool +SPDocument::_updateDocument(int update_flags) +{ + /* Process updates */ + if (this->root->uflags || this->root->mflags) { + if (this->root->uflags) { + SPItemCtx ctx; + setupViewport(&ctx); + + DocumentUndo::ScopedInsensitive _no_undo(this); + + this->root->updateDisplay((SPCtx *)&ctx, update_flags); + } + this->_emitModified(); + } + + return !(this->root->uflags || this->root->mflags); +} + + +/** + * Repeatedly works on getting the document updated, since sometimes + * it takes more than one pass to get the document updated. But it + * usually should not take more than a few loops, and certainly never + * more than 32 iterations. So we bail out if we hit 32 iterations, + * since this typically indicates we're stuck in an update loop. + */ +gint SPDocument::ensureUpToDate() +{ + // Bring the document up-to-date, specifically via the following: + // 1a) Process all document updates. + // 1b) When completed, process connector routing changes. + // 2a) Process any updates resulting from connector reroutings. + int counter = 32; + for (unsigned int pass = 1; pass <= 2; ++pass) { + // Process document updates. + while (!_updateDocument(0)) { + if (counter == 0) { + g_warning("More than 32 iteration while updating document '%s'", document_filename); + break; + } + counter--; + } + if (counter == 0) + { + break; + } + + // After updates on the first pass we get libavoid to process all the + // changed objects and provide new routings. This may cause some objects + // to be modified, hence the second update pass. + if (pass == 1) { + router->processTransaction(); + } + } + + // Remove handlers + modified_connection.disconnect(); + rerouting_connection.disconnect(); + + return (counter > 0); +} + +/** + * An idle handler to update the document. Returns true if + * the document needs further updates. + */ +bool +SPDocument::idle_handler() +{ + bool status = !_updateDocument(0); // method TRUE if it does NOT need further modification, so invert + if (!status) { + modified_connection.disconnect(); + } + return status; +} + +/** + * An idle handler to reroute connectors in the document. + */ +bool +SPDocument::rerouting_handler() +{ + // Process any queued movement actions and determine new routings for + // object-avoiding connectors. Callbacks will be used to update and + // redraw affected connectors. + router->processTransaction(); + + // We don't need to handle rerouting again until there are further + // diagram updates. + return false; +} + +static bool is_within(Geom::Rect const &area, Geom::Rect const &box) +{ + return area.contains(box); +} + +static bool overlaps(Geom::Rect const &area, Geom::Rect const &box) +{ + return area.intersects(box); +} + +/** + * @param area Area in document coordinates + */ +static std::vector<SPItem*> &find_items_in_area(std::vector<SPItem*> &s, + SPGroup *group, unsigned int dkey, + Geom::Rect const &area, + bool (*test)(Geom::Rect const &, Geom::Rect const &), + bool take_hidden = false, + bool take_insensitive = false, + bool take_groups = true, + bool enter_groups = false) +{ + g_return_val_if_fail(SP_IS_GROUP(group), s); + + for (auto& o: group->children) { + if (SPItem *item = dynamic_cast<SPItem *>(&o)) { + if (!take_insensitive && item->isLocked()) { + continue; + } + + if (!take_hidden && item->isHidden()) { + continue; + } + + if (SPGroup * childgroup = dynamic_cast<SPGroup *>(item)) { + bool is_layer = childgroup->effectiveLayerMode(dkey) == SPGroup::LAYER; + if (is_layer || (enter_groups)) { + s = find_items_in_area(s, childgroup, dkey, area, test, take_hidden, take_insensitive, take_groups, enter_groups); + } + if (!take_groups || is_layer) { + continue; + } + } + Geom::OptRect box = item->documentVisualBounds(); + if (box && test(area, *box)) { + s.push_back(item); + } + } + } + return s; +} + +SPItem *SPDocument::getItemFromListAtPointBottom(unsigned int dkey, SPGroup *group, std::vector<SPItem*> const &list,Geom::Point const &p, bool take_insensitive) +{ + g_return_val_if_fail(group, NULL); + SPItem *bottomMost = nullptr; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble delta = prefs->getDouble("/options/cursortolerance/value", 1.0); + + for (auto& o: group->children) { + if (bottomMost) { + break; + } + if (SP_IS_ITEM(&o)) { + SPItem *item = SP_ITEM(&o); + Inkscape::DrawingItem *arenaitem = item->get_arenaitem(dkey); + + if (arenaitem) { + arenaitem->drawing().update(); + } + + if (arenaitem && arenaitem->pick(p, delta, 1) != nullptr + && (take_insensitive || item->isVisibleAndUnlocked(dkey))) { + if (find(list.begin(), list.end(), item) != list.end()) { + bottomMost = item; + } + } + + if (!bottomMost && SP_IS_GROUP(&o)) { + // return null if not found: + bottomMost = getItemFromListAtPointBottom(dkey, SP_GROUP(&o), list, p, take_insensitive); + } + } + } + return bottomMost; +} + +/** +Turn the SVG DOM into a flat list of nodes that can be searched from top-down. +The list can be persisted, which improves "find at multiple points" speed. +*/ +// TODO: study add `gboolean with_groups = false` as parameter. +void SPDocument::build_flat_item_list(unsigned int dkey, SPGroup *group, gboolean into_groups) const +{ + for (auto& o: group->children) { + if (!SP_IS_ITEM(&o)) { + continue; + } + + if (SP_IS_GROUP(&o) && (SP_GROUP(&o)->effectiveLayerMode(dkey) == SPGroup::LAYER || into_groups)) { + build_flat_item_list(dkey, SP_GROUP(&o), into_groups); + } else { + SPItem *child = SP_ITEM(&o); + if (child->isVisibleAndUnlocked(dkey)) { + _node_cache.push_front(child); + } + } + } +} + +/** +Returns the items from the descendants of group (recursively) which are at the +point p, or NULL if none. Honors into_groups on whether to recurse into non-layer +groups or not. Honors take_insensitive on whether to return insensitive items. +If upto != NULL, then if item upto is encountered (at any level), stops searching +upwards in z-order and returns what it has found so far (i.e. the found items are +guaranteed to be lower than upto). Requires a list of nodes built by build_flat_item_list. +If items_count > 0, it'll return the topmost (in z-order) items_count items. + */ +static std::vector<SPItem*> find_items_at_point(std::deque<SPItem*> *nodes, unsigned int dkey, + Geom::Point const &p, int items_count=0, SPItem* upto=nullptr) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble delta = prefs->getDouble("/options/cursortolerance/value", 1.0); + + SPItem *child; + std::vector<SPItem*> result; + bool seen_upto = (!upto); + for (auto node : *nodes) { + child = node; + if (!seen_upto){ + if(child == upto) + seen_upto = true; + continue; + } + Inkscape::DrawingItem *arenaitem = child->get_arenaitem(dkey); + if (arenaitem) { + arenaitem->drawing().update(); + if (arenaitem->pick(p, delta, 1) != nullptr) { + result.push_back(child); + if (--items_count == 0) { + break; + } + } + } + } + + return result; +} + +static SPItem *find_item_at_point(std::deque<SPItem*> *nodes, unsigned int dkey, Geom::Point const &p, SPItem* upto=nullptr) +{ + auto items = find_items_at_point(nodes, dkey, p, 1, upto); + if (items.empty()) { + return nullptr; + } + return items.back(); +} + +/** +Returns the topmost non-layer group from the descendants of group which is at point +p, or NULL if none. Recurses into layers but not into groups. + */ +static SPItem *find_group_at_point(unsigned int dkey, SPGroup *group, Geom::Point const &p) +{ + SPItem *seen = nullptr; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble delta = prefs->getDouble("/options/cursortolerance/value", 1.0); + + for (auto& o: group->children) { + if (!SP_IS_ITEM(&o)) { + continue; + } + if (SP_IS_GROUP(&o) && SP_GROUP(&o)->effectiveLayerMode(dkey) == SPGroup::LAYER) { + SPItem *newseen = find_group_at_point(dkey, SP_GROUP(&o), p); + if (newseen) { + seen = newseen; + } + } + if (SP_IS_GROUP(&o) && SP_GROUP(&o)->effectiveLayerMode(dkey) != SPGroup::LAYER ) { + SPItem *child = SP_ITEM(&o); + Inkscape::DrawingItem *arenaitem = child->get_arenaitem(dkey); + if (arenaitem) { + arenaitem->drawing().update(); + } + + // seen remembers the last (topmost) of groups pickable at this point + if (arenaitem && arenaitem->pick(p, delta, 1) != nullptr) { + seen = child; + } + } + } + return seen; +} + + +/** + * Return list of items, contained in box + * + * @param box area to find items, in document coordinates + */ + +std::vector<SPItem*> SPDocument::getItemsInBox(unsigned int dkey, Geom::Rect const &box, bool take_hidden, bool take_insensitive, bool take_groups, bool enter_groups) const +{ + std::vector<SPItem*> x; + return find_items_in_area(x, this->root, dkey, box, is_within, take_hidden, take_insensitive, take_groups, enter_groups); +} + +/** + * Get items whose bounding box overlaps with given area. + * @param dkey desktop view in use + * @param box area to find items, in document coordinates + * @param take_hidden get hidden items + * @param take_insensitive get insensitive items + * @param take_groups get also the groups + * @param enter_groups get items inside groups + * @return Return list of items, that the parts of the item contained in box + */ + +std::vector<SPItem*> SPDocument::getItemsPartiallyInBox(unsigned int dkey, Geom::Rect const &box, bool take_hidden, bool take_insensitive, bool take_groups, bool enter_groups) const +{ + std::vector<SPItem*> x; + return find_items_in_area(x, this->root, dkey, box, overlaps, take_hidden, take_insensitive, take_groups, enter_groups); +} + +std::vector<SPItem*> SPDocument::getItemsAtPoints(unsigned const key, std::vector<Geom::Point> points, bool all_layers, bool topmost_only, size_t limit) const +{ + std::vector<SPItem*> result; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // When picking along the path, we don't want small objects close together + // (such as hatching strokes) to obscure each other by their deltas, + // so we temporarily set delta to a small value + gdouble saved_delta = prefs->getDouble("/options/cursortolerance/value", 1.0); + prefs->setDouble("/options/cursortolerance/value", 0.25); + + // Cache a flattened SVG DOM to speed up selection. + if(!_node_cache_valid){ + _node_cache.clear(); + build_flat_item_list(key, this->root, true); + _node_cache_valid=true; + } + SPObject *current_layer = nullptr; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(desktop){ + current_layer = desktop->layerManager().currentLayer(); + } + size_t item_counter = 0; + for(int i = points.size()-1;i>=0; i--) { + std::vector<SPItem*> items = find_items_at_point(&_node_cache, key, points[i], topmost_only); + for (SPItem *item : items) { + if (item && result.end()==find(result.begin(), result.end(), item)) + if(all_layers || (desktop && desktop->layerManager().layerForObject(item) == current_layer)){ + result.push_back(item); + item_counter++; + //limit 0 = no limit + if(item_counter == limit){ + prefs->setDouble("/options/cursortolerance/value", saved_delta); + return result; + } + } + } + } + + // and now we restore it back + prefs->setDouble("/options/cursortolerance/value", saved_delta); + + return result; +} + +SPItem *SPDocument::getItemAtPoint( unsigned const key, Geom::Point const &p, + bool const into_groups, SPItem *upto) const +{ + // Build a flattened SVG DOM for find_item_at_point. + std::deque<SPItem*> bak(_node_cache); + if(!into_groups){ + _node_cache.clear(); + build_flat_item_list(key, this->root, into_groups); + } + if(!_node_cache_valid && into_groups){ + _node_cache.clear(); + build_flat_item_list(key, this->root, true); + _node_cache_valid=true; + } + + SPItem *res = find_item_at_point(&_node_cache, key, p, upto); + if(!into_groups) + _node_cache = bak; + return res; +} + +SPItem *SPDocument::getGroupAtPoint(unsigned int key, Geom::Point const &p) const +{ + return find_group_at_point(key, this->root, p); +} + +// Resource management + +bool SPDocument::addResource(gchar const *key, SPObject *object) +{ + g_return_val_if_fail(key != nullptr, false); + g_return_val_if_fail(*key != '\0', false); + g_return_val_if_fail(object != nullptr, false); + g_return_val_if_fail(SP_IS_OBJECT(object), false); + + bool result = false; + + if ( !object->cloned ) { + std::vector<SPObject *> rlist = resources[key]; + g_return_val_if_fail(std::find(rlist.begin(),rlist.end(),object) == rlist.end(), false); + resources[key].insert(resources[key].begin(),object); + + GQuark q = g_quark_from_string(key); + + /*in general, do not send signal if the object has no id (yet), + it means the object is not completely built. + (happens when pasting swatches across documents, cf bug 1495106) + [this check should be more generally presend on emit() calls since + the backtrace is unusable with crashed from this cause] + */ + if(object->getId() || dynamic_cast<SPGroup*>(object) || dynamic_cast<SPPage*>(object) ) + resources_changed_signals[q].emit(); + + result = true; + } + + return result; +} + +bool SPDocument::removeResource(gchar const *key, SPObject *object) +{ + g_return_val_if_fail(key != nullptr, false); + g_return_val_if_fail(*key != '\0', false); + g_return_val_if_fail(object != nullptr, false); + g_return_val_if_fail(SP_IS_OBJECT(object), false); + + bool result = false; + + if ( !object->cloned ) { + std::vector<SPObject *> rlist = resources[key]; + g_return_val_if_fail(!rlist.empty(), false); + std::vector<SPObject*>::iterator it = std::find(resources[key].begin(),resources[key].end(),object); + g_return_val_if_fail(it != rlist.end(), false); + resources[key].erase(it); + + GQuark q = g_quark_from_string(key); + resources_changed_signals[q].emit(); + + result = true; + } + + return result; +} + +std::vector<SPObject *> const SPDocument::getResourceList(gchar const *key) +{ + std::vector<SPObject *> emptyset; + g_return_val_if_fail(key != nullptr, emptyset); + g_return_val_if_fail(*key != '\0', emptyset); + + return resources[key]; +} + +/* Helpers */ + +static unsigned int count_objects_recursive(SPObject *obj, unsigned int count) +{ + count++; // obj itself + + for (auto& i: obj->children) { + count = count_objects_recursive(&i, count); + } + + return count; +} + +/** + * Count the number of objects in a given document recursively using the count_objects_recursive helper function + * + * @param[in] document Pointer to the document for counting objects + * @return Number of objects in the document + */ +static unsigned int objects_in_document(SPDocument *document) +{ + return count_objects_recursive(document->getRoot(), 0); +} + +/** + * Remove unused definitions etc. recursively from an object and its siblings + * + * @param[inout] obj Object which shall be "cleaned" + */ +static void vacuum_document_recursive(SPObject *obj) +{ + if (SP_IS_DEFS(obj)) { + for (auto& def: obj->children) { + // fixme: some inkscape-internal nodes in the future might not be collectable + def.requestOrphanCollection(); + } + } else { + for (auto& i: obj->children) { + vacuum_document_recursive(&i); + } + } +} + +/** + * Remove unused definitions etc. recursively from an entire document. + * + * @return Number of removed objects + */ +unsigned int SPDocument::vacuumDocument() +{ + unsigned int start = objects_in_document(this); + unsigned int end; + unsigned int newend = start; + + unsigned int iterations = 0; + + do { + end = newend; + + vacuum_document_recursive(root); + this->collectOrphans(); + iterations++; + + newend = objects_in_document(this); + + } while (iterations < 100 && newend < end); + // We stop if vacuum_document_recursive doesn't remove any more objects or after 100 iterations, whichever occurs first. + + return start - newend; +} + +/** + * Indicate to the user if the document has been modified since the last save by displaying a "*" in front of the name of the file in the window title. + * + * @param[in] modified True if the document has been modified. + */ +void SPDocument::setModifiedSinceSave(bool modified) { + this->modified_since_save = modified; + this->modified_since_autosave = modified; + if (SP_ACTIVE_DESKTOP) { + InkscapeWindow *window = SP_ACTIVE_DESKTOP->getInkscapeWindow(); + if (window) { // during load, SP_ACTIVE_DESKTOP may be !nullptr, but parent might still be nullptr + SPDesktopWidget *dtw = window->get_desktop_widget(); + dtw->updateTitle( this->getDocumentName() ); + } + } +} + + +/** + * Paste SVG defs from the document retrieved from the clipboard or imported document into the active document. + * @param clipdoc The document to paste. + * @pre @c clipdoc != NULL and pasting into the active document is possible. + */ +void SPDocument::importDefs(SPDocument *source) +{ + Inkscape::XML::Node *root = source->getReprRoot(); + Inkscape::XML::Node *target_defs = this->getDefs()->getRepr(); + std::vector<Inkscape::XML::Node const *> defsNodes = sp_repr_lookup_name_many(root, "svg:defs"); + + prevent_id_clashes(source, this); + + for (auto & defsNode : defsNodes) { + _importDefsNode(source, const_cast<Inkscape::XML::Node *>(defsNode), target_defs); + } +} + +void SPDocument::_importDefsNode(SPDocument *source, Inkscape::XML::Node *defs, Inkscape::XML::Node *target_defs) +{ + int stagger=0; + + /* Note, "clipboard" throughout the comments means "the document that is either the clipboard + or an imported document", as importDefs is called in both contexts. + + The order of the records in the clipboard is unpredictable and there may be both + forward and backwards references to other records within it. There may be definitions in + the clipboard that duplicate definitions in the present document OR that duplicate other + definitions in the clipboard. (Inkscape will not have created these, but they may be read + in from other SVG sources.) + + There are 3 passes to clean this up: + + In the first find and mark definitions in the clipboard that are duplicates of those in the + present document. Change the ID to "RESERVED_FOR_INKSCAPE_DUPLICATE_DEF_XXXXXXXXX". + (Inkscape will not reuse an ID, and the XXXXXXXXX keeps it from automatically creating new ones.) + References in the clipboard to the old clipboard name are converted to the name used + in the current document. + + In the second find and mark definitions in the clipboard that are duplicates of earlier + definitions in the clipbard. Unfortunately this is O(n^2) and could be very slow for a large + SVG with thousands of definitions. As before, references are adjusted to reflect the name + going forward. + + In the final cycle copy over those records not marked with that ID. + + If an SVG file uses the special ID it will cause problems! + + If this function is called because of the paste of a true clipboard the caller will have passed in a + COPY of the clipboard items. That is good, because this routine modifies that document. If the calling + behavior ever changes, so that the same document is passed in on multiple pastes, this routine will break + as in the following example: + 1. Paste clipboard containing B same as A into document containing A. Result, B is dropped + and all references to it will point to A. + 2. Paste same clipboard into a new document. It will not contain A, so there will be unsatisfied + references in that window. + */ + + std::string DuplicateDefString = "RESERVED_FOR_INKSCAPE_DUPLICATE_DEF"; + + /* First pass: remove duplicates in clipboard of definitions in document */ + for (Inkscape::XML::Node *def = defs->firstChild() ; def ; def = def->next()) { + if(def->type() != Inkscape::XML::NodeType::ELEMENT_NODE)continue; + /* If this clipboard has been pasted into one document, and is now being pasted into another, + or pasted again into the same, it will already have been processed. If we detect that then + skip the rest of this pass. */ + Glib::ustring defid = def->attribute("id"); + if( defid.find( DuplicateDefString ) != Glib::ustring::npos )break; + + SPObject *src = source->getObjectByRepr(def); + + // Prevent duplicates of solid swatches by checking if equivalent swatch already exists + SPGradient *s_gr = dynamic_cast<SPGradient *>(src); + LivePathEffectObject *s_lpeobj = dynamic_cast<LivePathEffectObject *>(src); + if (src && (s_gr || s_lpeobj)) { + for (auto& trg: getDefs()->children) { + SPGradient *t_gr = dynamic_cast<SPGradient *>(&trg); + if (src != &trg && s_gr && t_gr) { + if (s_gr->isEquivalent(t_gr)) { + // Change object references to the existing equivalent gradient + Glib::ustring newid = trg.getId(); + if (newid != defid) { // id could be the same if it is a second paste into the same document + change_def_references(src, &trg); + } + gchar *longid = g_strdup_printf("%s_%9.9d", DuplicateDefString.c_str(), stagger++); + def->setAttribute("id", longid); + g_free(longid); + // do NOT break here, there could be more than 1 duplicate! + } + } + LivePathEffectObject *t_lpeobj = dynamic_cast<LivePathEffectObject *>(&trg); + if (src != &trg && s_lpeobj && t_lpeobj) { + if (t_lpeobj->is_similar(s_lpeobj)) { + // Change object references to the existing equivalent gradient + Glib::ustring newid = trg.getId(); + if (newid != defid) { // id could be the same if it is a second paste into the same document + change_def_references(src, &trg); + } + gchar *longid = g_strdup_printf("%s_%9.9d", DuplicateDefString.c_str(), stagger++); + def->setAttribute("id", longid); + g_free(longid); + // do NOT break here, there could be more than 1 duplicate! + } + } + } + } + } + + /* Second pass: remove duplicates in clipboard of earlier definitions in clipboard */ + for (Inkscape::XML::Node *def = defs->firstChild() ; def ; def = def->next()) { + if(def->type() != Inkscape::XML::NodeType::ELEMENT_NODE)continue; + Glib::ustring defid = def->attribute("id"); + if( defid.find( DuplicateDefString ) != Glib::ustring::npos )continue; // this one already handled + SPObject *src = source->getObjectByRepr(def); + LivePathEffectObject *s_lpeobj = dynamic_cast<LivePathEffectObject *>(src); + SPGradient *s_gr = dynamic_cast<SPGradient *>(src); + if (src && (s_gr || s_lpeobj)) { + for (Inkscape::XML::Node *laterDef = def->next() ; laterDef ; laterDef = laterDef->next()) { + SPObject *trg = source->getObjectByRepr(laterDef); + SPGradient *t_gr = dynamic_cast<SPGradient *>(trg); + if (trg && (src != trg) && s_gr && t_gr) { + Glib::ustring newid = trg->getId(); + if (newid.find(DuplicateDefString) != Glib::ustring::npos) + continue; // this one already handled + if (t_gr && s_gr->isEquivalent(t_gr)) { + // Change object references to the existing equivalent gradient + // two id's in the clipboard should never be the same, so always change references + change_def_references(trg, src); + gchar *longid = g_strdup_printf("%s_%9.9d", DuplicateDefString.c_str(), stagger++); + laterDef->setAttribute("id", longid); + g_free(longid); + // do NOT break here, there could be more than 1 duplicate! + } + } + LivePathEffectObject *t_lpeobj = dynamic_cast<LivePathEffectObject *>(trg); + if (trg && (src != trg) && s_lpeobj && t_lpeobj) { + Glib::ustring newid = trg->getId(); + if (newid.find(DuplicateDefString) != Glib::ustring::npos) + continue; // this one already handled + if (t_lpeobj->is_similar(s_lpeobj)) { + // Change object references to the existing equivalent gradient + // two id's in the clipboard should never be the same, so always change references + change_def_references(trg, src); + gchar *longid = g_strdup_printf("%s_%9.9d", DuplicateDefString.c_str(), stagger++); + laterDef->setAttribute("id", longid); + g_free(longid); + // do NOT break here, there could be more than 1 duplicate! + } + } + } + } + } + + /* Final pass: copy over those parts which are not duplicates */ + for (Inkscape::XML::Node *def = defs->firstChild() ; def ; def = def->next()) { + if(def->type() != Inkscape::XML::NodeType::ELEMENT_NODE)continue; + + /* Ignore duplicate defs marked in the first pass */ + Glib::ustring defid = def->attribute("id"); + if( defid.find( DuplicateDefString ) != Glib::ustring::npos )continue; + + bool duplicate = false; + SPObject *src = source->getObjectByRepr(def); + + // Prevent duplication of symbols... could be more clever. + // The tag "_inkscape_duplicate" is added to "id" by ClipboardManagerImpl::copySymbol(). + // We assume that symbols are in defs section (not required by SVG spec). + if (src && SP_IS_SYMBOL(src)) { + + Glib::ustring id = src->getRepr()->attribute("id"); + size_t pos = id.find( "_inkscape_duplicate" ); + if( pos != Glib::ustring::npos ) { + + // This is our symbol, now get rid of tag + id.erase( pos ); + + // Check that it really is a duplicate + for (auto& trg: getDefs()->children) { + if (SP_IS_SYMBOL(&trg) && src != &trg) { + std::string id2 = trg.getRepr()->attribute("id"); + + if( !id.compare( id2 ) ) { + duplicate = true; + break; + } + } + } + if ( !duplicate ) { + src->setAttribute("id", id); + } + } + } + + if (!duplicate) { + Inkscape::XML::Node * dup = def->duplicate(this->getReprDoc()); + target_defs->appendChild(dup); + Inkscape::GC::release(dup); + } + } +} + +// Signals ------------------------------ + +void +SPDocument::addUndoObserver(Inkscape::UndoStackObserver& observer) +{ + this->undoStackObservers.add(observer); +} + +void +SPDocument::removeUndoObserver(Inkscape::UndoStackObserver& observer) +{ + this->undoStackObservers.remove(observer); +} + +sigc::connection SPDocument::connectDestroy(sigc::signal<void>::slot_type slot) +{ + return destroySignal.connect(slot); +} + +sigc::connection SPDocument::connectModified(SPDocument::ModifiedSignal::slot_type slot) +{ + return modified_signal.connect(slot); +} + +sigc::connection SPDocument::connectFilenameSet(SPDocument::FilenameSetSignal::slot_type slot) +{ + return filename_set_signal.connect(slot); +} + +sigc::connection SPDocument::connectCommit(SPDocument::CommitSignal::slot_type slot) +{ + return commit_signal.connect(slot); +} + +sigc::connection SPDocument::connectBeforeCommit(SPDocument::BeforeCommitSignal::slot_type slot) +{ + return before_commit_signal.connect(slot); +} + +sigc::connection SPDocument::connectIdChanged(gchar const *id, + SPDocument::IDChangedSignal::slot_type slot) +{ + return id_changed_signals[g_quark_from_string(id)].connect(slot); +} + +sigc::connection SPDocument::connectResourcesChanged(gchar const *key, + SPDocument::ResourcesChangedSignal::slot_type slot) +{ + GQuark q = g_quark_from_string(key); + return resources_changed_signals[q].connect(slot); +} + +sigc::connection +SPDocument::connectReconstructionStart(SPDocument::ReconstructionStart::slot_type slot) +{ + return _reconstruction_start_signal.connect(slot); +} + +sigc::connection +SPDocument::connectReconstructionFinish(SPDocument::ReconstructionFinish::slot_type slot) +{ + return _reconstruction_finish_signal.connect(slot); +} + +void SPDocument::_emitModified() { + static guint const flags = SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG; + root->emitModified(0); + modified_signal.emit(flags); + _node_cache_valid=false; +} + +void +SPDocument::emitReconstructionStart() +{ + // printf("Starting Reconstruction\n"); + _reconstruction_start_signal.emit(); +} + +void +SPDocument::emitReconstructionFinish() +{ + // printf("Finishing Reconstruction\n"); + _reconstruction_finish_signal.emit(); + // indicates that gradients are reloaded (to rebuild the Auto palette) + resources_changed_signals[g_quark_from_string("gradient")].emit(); + resources_changed_signals[g_quark_from_string("filter")].emit(); + +/** + // Reference to the old persp3d object is invalid after reconstruction. + initialize_current_persp3d(); +**/ +} + +void SPDocument::set_reference_document(SPDocument* document) { + _ref_document = document; +} + +SPDocument* SPDocument::get_reference_document() { + return _ref_document; +} + +SPDocument::install_reference_document::install_reference_document(SPDocument* inject_into, SPDocument* reference) { + g_assert(inject_into); + _parent = inject_into; + _parent->set_reference_document(reference); +} + +SPDocument::install_reference_document::~install_reference_document() { + _parent->set_reference_document(nullptr); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : |