summaryrefslogtreecommitdiffstats
path: root/src/object/sp-object.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/object/sp-object.cpp')
-rw-r--r--src/object/sp-object.cpp1848
1 files changed, 1848 insertions, 0 deletions
diff --git a/src/object/sp-object.cpp b/src/object/sp-object.cpp
new file mode 100644
index 0000000..b93cc22
--- /dev/null
+++ b/src/object/sp-object.cpp
@@ -0,0 +1,1848 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * SPObject implementation.
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Stephen Silver <sasilver@users.sourceforge.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * Adrian Boguszewski
+ *
+ * Copyright (C) 1999-2016 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <string>
+#include <vector>
+#include <limits>
+#include <glibmm.h>
+
+#include <boost/range/adaptor/transformed.hpp>
+
+#include "helper/sp-marshal.h"
+#include "xml/node-event-vector.h"
+#include "attributes.h"
+#include "attribute-rel-util.h"
+#include "color-profile.h"
+#include "document.h"
+#include "io/fix-broken-links.h"
+#include "preferences.h"
+#include "style.h"
+#include "live_effects/lpeobject.h"
+#include "sp-factory.h"
+#include "sp-font.h"
+#include "sp-paint-server.h"
+#include "sp-root.h"
+#include "sp-style-elem.h"
+#include "sp-script.h"
+#include "streq.h"
+#include "strneq.h"
+#include "xml/node-fns.h"
+#include "debug/event-tracker.h"
+#include "debug/simple-event.h"
+#include "debug/demangle.h"
+#include "svg/css-ostringstream.h"
+#include "util/format.h"
+#include "util/longest-common-suffix.h"
+
+#define noSP_OBJECT_DEBUG_CASCADE
+
+#define noSP_OBJECT_DEBUG
+
+#ifdef SP_OBJECT_DEBUG
+# define debug(f, a...) { g_print("%s(%d) %s:", \
+ __FILE__,__LINE__,__FUNCTION__); \
+ g_print(f, ## a); \
+ g_print("\n"); \
+ }
+#else
+# define debug(f, a...) /* */
+#endif
+
+// Define to enable indented tracing of SPObject.
+//#define OBJECT_TRACE
+unsigned SPObject::indent_level = 0;
+
+Inkscape::XML::NodeEventVector object_event_vector = {
+ SPObject::repr_child_added,
+ SPObject::repr_child_removed,
+ SPObject::repr_attr_changed,
+ SPObject::repr_content_changed,
+ SPObject::repr_order_changed,
+ SPObject::repr_name_changed
+};
+
+/**
+ * A friend class used to set internal members on SPObject so as to not expose settors in SPObject's public API
+ */
+class SPObjectImpl
+{
+public:
+
+/**
+ * Null's the id member of an SPObject without attempting to free prior contents.
+ *
+ * @param[inout] obj Pointer to the object which's id shall be nulled.
+ */
+ static void setIdNull( SPObject* obj ) {
+ if (obj) {
+ obj->id = nullptr;
+ }
+ }
+
+/**
+ * Sets the id member of an object, freeing any prior content.
+ *
+ * @param[inout] obj Pointer to the object which's id shall be set.
+ * @param[in] id New id
+ */
+ static void setId( SPObject* obj, gchar const* id ) {
+ if (obj && (id != obj->id) ) {
+ if (obj->id) {
+ g_free(obj->id);
+ obj->id = nullptr;
+ }
+ if (id) {
+ obj->id = g_strdup(id);
+ }
+ }
+ }
+};
+
+/**
+ * Constructor, sets all attributes to default values.
+ */
+SPObject::SPObject()
+ : cloned(0), clone_original(nullptr), uflags(0), mflags(0), hrefcount(0), _total_hrefcount(0),
+ document(nullptr), parent(nullptr), id(nullptr), repr(nullptr), refCount(1), hrefList(std::list<SPObject*>()),
+ _successor(nullptr), _collection_policy(SPObject::COLLECT_WITH_PARENT),
+ _label(nullptr), _default_label(nullptr)
+{
+ debug("id=%p, typename=%s",this, g_type_name_from_instance((GTypeInstance*)this));
+
+ //used XML Tree here.
+ this->getRepr(); // TODO check why this call is made
+
+ SPObjectImpl::setIdNull(this);
+
+ // FIXME: now we create style for all objects, but per SVG, only the following can have style attribute:
+ // vg, g, defs, desc, title, symbol, use, image, switch, path, rect, circle, ellipse, line, polyline,
+ // polygon, text, tspan, tref, textPath, altGlyph, glyphRef, marker, linearGradient, radialGradient,
+ // stop, pattern, clipPath, mask, filter, feImage, a, font, glyph, missing-glyph, foreignObject
+ this->style = new SPStyle( nullptr, this ); // Is it necessary to call with "this"?
+ this->context_style = nullptr;
+}
+
+/**
+ * Destructor, frees the used memory and unreferences a potential successor of the object.
+ */
+SPObject::~SPObject() {
+ g_free(this->_label);
+ g_free(this->_default_label);
+
+ this->_label = nullptr;
+ this->_default_label = nullptr;
+
+ if (this->_successor) {
+ sp_object_unref(this->_successor, nullptr);
+ this->_successor = nullptr;
+ }
+ if (parent) {
+ parent->children.erase(parent->children.iterator_to(*this));
+ }
+
+ if( style == nullptr ) {
+ // style pointer could be NULL if unreffed too many times.
+ // Conjecture: style pointer is never NULL.
+ std::cerr << "SPObject::~SPObject(): style pointer is NULL" << std::endl;
+ } else if( style->refCount() > 1 ) {
+ // Conjecture: style pointer should be unreffed by other classes before reaching here.
+ // Conjecture is false for SPTSpan where ref is held by InputStreamTextSource.
+ // As an additional note:
+ // The outer tspan of a nested tspan will result in a ref count of five: one for the
+ // TSpan itself, one for the InputStreamTextSource instance before the inner tspan and
+ // one for the one after, along with one for each corresponding DrawingText instance.
+ // std::cerr << "SPObject::~SPObject(): someone else still holding ref to style" << std::endl;
+ //
+ sp_style_unref( this->style );
+ } else {
+ delete this->style;
+ }
+}
+
+// CPPIFY: make pure virtual
+void SPObject::read_content() {
+ //throw;
+}
+
+void SPObject::update(SPCtx* /*ctx*/, unsigned int /*flags*/) {
+ //throw;
+}
+
+void SPObject::modified(unsigned int /*flags*/) {
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::modified (default) (empty function)" );
+ objectTrace( "SPObject::modified (default)", false );
+#endif
+ //throw;
+}
+
+namespace {
+
+namespace Debug = Inkscape::Debug;
+namespace Util = Inkscape::Util;
+
+typedef Debug::SimpleEvent<Debug::Event::REFCOUNT> BaseRefCountEvent;
+
+class RefCountEvent : public BaseRefCountEvent {
+public:
+ RefCountEvent(SPObject *object, int bias, char const *name)
+ : BaseRefCountEvent(name)
+ {
+ _addProperty("object", Util::format("%p", object).pointer());
+ _addProperty("class", Debug::demangle(typeid(*object).name()));
+ _addProperty("new-refcount", Util::format("%d", object->refCount + bias).pointer());
+ }
+};
+
+class RefEvent : public RefCountEvent {
+public:
+ RefEvent(SPObject *object)
+ : RefCountEvent(object, 1, "sp-object-ref")
+ {}
+};
+
+class UnrefEvent : public RefCountEvent {
+public:
+ UnrefEvent(SPObject *object)
+ : RefCountEvent(object, -1, "sp-object-unref")
+ {}
+};
+
+}
+
+gchar const* SPObject::getId() const {
+ return id;
+}
+
+/**
+ * Accumulate this id and all it's descendants ids
+ */
+void SPObject::getIds(std::set<std::string> &ret) const {
+ if (id) {
+ ret.insert(std::string(id));
+ }
+ for (auto &child : children) {
+ child.getIds(ret);
+ }
+}
+
+/**
+ * Returns the id as a url param, in the form 'url(#{id})'
+ */
+std::string SPObject::getUrl() const {
+ if (id) {
+ return std::string("url(#") + id + ")";
+ }
+ return "";
+}
+
+Inkscape::XML::Node * SPObject::getRepr() {
+ return repr;
+}
+
+Inkscape::XML::Node const* SPObject::getRepr() const{
+ return repr;
+}
+
+
+SPObject *sp_object_ref(SPObject *object, SPObject *owner)
+{
+ g_return_val_if_fail(object != nullptr, NULL);
+ g_return_val_if_fail(SP_IS_OBJECT(object), NULL);
+ g_return_val_if_fail(!owner || SP_IS_OBJECT(owner), NULL);
+
+ Inkscape::Debug::EventTracker<RefEvent> tracker(object);
+
+ object->refCount++;
+
+ return object;
+}
+
+SPObject *sp_object_unref(SPObject *object, SPObject *owner)
+{
+ g_return_val_if_fail(object != nullptr, NULL);
+ g_return_val_if_fail(SP_IS_OBJECT(object), NULL);
+ g_return_val_if_fail(!owner || SP_IS_OBJECT(owner), NULL);
+
+ Inkscape::Debug::EventTracker<UnrefEvent> tracker(object);
+
+ object->refCount--;
+
+ if (object->refCount <= 0) {
+ delete object;
+ }
+
+ return nullptr;
+}
+
+void SPObject::hrefObject(SPObject* owner)
+{
+ // if (owner) std::cout << " owner: " << *owner << std::endl;
+
+ // If owner is a clone, do not increase hrefcount, it's already href'ed by original.
+ if (!owner || !owner->cloned) {
+ hrefcount++;
+ _updateTotalHRefCount(1);
+ }
+
+ if(owner)
+ hrefList.push_front(owner);
+}
+
+void SPObject::unhrefObject(SPObject* owner)
+{
+ if (!owner || !owner->cloned) {
+ g_return_if_fail(hrefcount > 0);
+
+ hrefcount--;
+ _updateTotalHRefCount(-1);
+ }
+
+ if(owner)
+ hrefList.remove(owner);
+}
+
+void SPObject::_updateTotalHRefCount(int increment) {
+ SPObject *topmost_collectable = nullptr;
+ for ( SPObject *iter = this ; iter ; iter = iter->parent ) {
+ iter->_total_hrefcount += increment;
+ if ( iter->_total_hrefcount < iter->hrefcount ) {
+ g_critical("HRefs overcounted");
+ }
+ if ( iter->_total_hrefcount == 0 &&
+ iter->_collection_policy != COLLECT_WITH_PARENT )
+ {
+ topmost_collectable = iter;
+ }
+ }
+ if (topmost_collectable) {
+ topmost_collectable->requestOrphanCollection();
+ }
+}
+
+bool SPObject::isAncestorOf(SPObject const *object) const {
+ g_return_val_if_fail(object != nullptr, false);
+ object = object->parent;
+ while (object) {
+ if ( object == this ) {
+ return true;
+ }
+ object = object->parent;
+ }
+ return false;
+}
+
+SPObject const *SPObject::nearestCommonAncestor(SPObject const *object) const {
+ g_return_val_if_fail(object != nullptr, NULL);
+
+ using Inkscape::Algorithms::nearest_common_ancestor;
+ return nearest_common_ancestor<SPObject::ConstParentIterator>(this, object, nullptr);
+}
+
+static SPObject const *AncestorSon(SPObject const *obj, SPObject const *ancestor) {
+ SPObject const *result = nullptr;
+ if ( obj && ancestor ) {
+ if (obj->parent == ancestor) {
+ result = obj;
+ } else {
+ result = AncestorSon(obj->parent, ancestor);
+ }
+ }
+ return result;
+}
+
+int sp_object_compare_position(SPObject const *first, SPObject const *second)
+{
+ int result = 0;
+ if (first != second) {
+ SPObject const *ancestor = first->nearestCommonAncestor(second);
+ // Need a common ancestor to be able to compare
+ if ( ancestor ) {
+ // we have an object and its ancestor (should not happen when sorting selection)
+ if (ancestor == first) {
+ result = 1;
+ } else if (ancestor == second) {
+ result = -1;
+ } else {
+ SPObject const *to_first = AncestorSon(first, ancestor);
+ SPObject const *to_second = AncestorSon(second, ancestor);
+
+ g_assert(to_second->parent == to_first->parent);
+
+ result = sp_repr_compare_position(to_first->getRepr(), to_second->getRepr());
+ }
+ }
+ }
+ return result;
+}
+
+bool sp_object_compare_position_bool(SPObject const *first, SPObject const *second){
+ return sp_object_compare_position(first,second)<0;
+}
+
+
+SPObject *SPObject::appendChildRepr(Inkscape::XML::Node *repr) {
+ if ( !cloned ) {
+ getRepr()->appendChild(repr);
+ return document->getObjectByRepr(repr);
+ } else {
+ g_critical("Attempt to append repr as child of cloned object");
+ return nullptr;
+ }
+}
+
+void SPObject::setCSS(SPCSSAttr *css, gchar const *attr)
+{
+ g_assert(this->getRepr() != nullptr);
+ sp_repr_css_set(this->getRepr(), css, attr);
+}
+
+void SPObject::changeCSS(SPCSSAttr *css, gchar const *attr)
+{
+ g_assert(this->getRepr() != nullptr);
+ sp_repr_css_change(this->getRepr(), css, attr);
+}
+
+std::vector<SPObject*> SPObject::childList(bool add_ref, Action) {
+ std::vector<SPObject*> l;
+ for (auto& child: children) {
+ if (add_ref) {
+ sp_object_ref(&child);
+ }
+ l.push_back(&child);
+ }
+ return l;
+}
+
+std::vector<SPObject*> SPObject::ancestorList(bool root_to_tip)
+{
+ std::vector<SPObject *> ancestors;
+ for (SPObject::ParentIterator iter=parent ; iter ; ++iter) {
+ ancestors.push_back(iter);
+ }
+ if (root_to_tip) {
+ std::reverse(ancestors.begin(), ancestors.end());
+ }
+ return ancestors;
+}
+
+gchar const *SPObject::label() const {
+ return _label;
+}
+
+gchar const *SPObject::defaultLabel() const {
+ if (_label) {
+ return _label;
+ } else {
+ if (!_default_label) {
+ if (getId()) {
+ _default_label = g_strdup_printf("#%s", getId());
+ } else if (getRepr()) {
+ _default_label = g_strdup_printf("<%s>", getRepr()->name());
+ } else {
+ _default_label = g_strdup("Default label");
+ }
+ }
+ return _default_label;
+ }
+}
+
+void SPObject::setLabel(gchar const *label)
+{
+ getRepr()->setAttribute("inkscape:label", label);
+}
+
+
+void SPObject::requestOrphanCollection() {
+ g_return_if_fail(document != nullptr);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // do not remove style or script elements (Bug #276244)
+ if (dynamic_cast<SPStyleElem *>(this)) {
+ // leave it
+ } else if (dynamic_cast<SPScript *>(this)) {
+ // leave it
+ } else if (dynamic_cast<SPFont*>(this)) {
+ // leave it
+ } else if ((! prefs->getBool("/options/cleanupswatches/value", false)) && SP_IS_PAINT_SERVER(this) && static_cast<SPPaintServer*>(this)->isSwatch() ) {
+ // leave it
+ } else if (IS_COLORPROFILE(this)) {
+ // leave it
+ } else if (dynamic_cast<LivePathEffectObject *>(this)) {
+ document->queueForOrphanCollection(this);
+ } else {
+ document->queueForOrphanCollection(this);
+
+ /** \todo
+ * This is a temporary hack added to make fill&stroke rebuild its
+ * gradient list when the defs are vacuumed. gradient-vector.cpp
+ * listens to the modified signal on defs, and now we give it that
+ * signal. Mental says that this should be made automatic by
+ * merging SPObjectGroup with SPObject; SPObjectGroup would issue
+ * this signal automatically. Or maybe just derive SPDefs from
+ * SPObjectGroup?
+ */
+
+ this->requestModified(SP_OBJECT_CHILD_MODIFIED_FLAG);
+ }
+}
+
+void SPObject::_sendDeleteSignalRecursive() {
+ for (auto& child: children) {
+ child._delete_signal.emit(&child);
+ child._sendDeleteSignalRecursive();
+ }
+}
+
+void SPObject::deleteObject(bool propagate, bool propagate_descendants)
+{
+ sp_object_ref(this, nullptr);
+ if (SP_IS_LPE_ITEM(this)) {
+ SP_LPE_ITEM(this)->removeAllPathEffects(false, propagate_descendants);
+ }
+ if (propagate) {
+ _delete_signal.emit(this);
+ }
+ if (propagate_descendants) {
+ this->_sendDeleteSignalRecursive();
+ }
+
+ Inkscape::XML::Node *repr = getRepr();
+ if (repr && repr->parent()) {
+ sp_repr_unparent(repr);
+ }
+
+ if (_successor) {
+ _successor->deleteObject(propagate, propagate_descendants);
+ }
+ sp_object_unref(this, nullptr);
+}
+
+void SPObject::cropToObject(SPObject *except)
+{
+ std::vector<SPObject *> toDelete;
+ for (auto &child : children) {
+ if (SP_IS_ITEM(&child)) {
+ if (child.isAncestorOf(except)) {
+ child.cropToObject(except);
+ } else if (&child != except) {
+ sp_object_ref(&child, nullptr);
+ toDelete.push_back(&child);
+ }
+ }
+ }
+ for (auto &i : toDelete) {
+ i->deleteObject(true, true);
+ sp_object_unref(i, nullptr);
+ }
+}
+
+/**
+ * Removes objects which are not related to given list of objects.
+ *
+ * Use Case: Group[MyRect1 , MyRect2] , MyRect3
+ * List Provided: MyRect1, MyRect3
+ * Output doc: Group[MyRect1], MyRect3
+ * List Provided: MyRect1, Group
+ * Output doc: Group[MyRect1, MyRect2] (notice MyRect2 is not deleted as it is related to Group)
+ */
+void SPObject::cropToObjects(std::vector<SPObject *> except_objects)
+{
+ if (except_objects.empty()) {
+ return;
+ }
+ std::vector<SPObject *> toDelete;
+ for (auto &child : children) {
+ if (SP_IS_ITEM(&child)) {
+ std::vector<SPObject *> except_in_child;
+ bool child_delete_flag = true;
+ for (auto except : except_objects) {
+ if (&child == except) {
+ child_delete_flag = false;
+ except_in_child.clear();
+ break;
+ }
+ if (child.isAncestorOf(except)) {
+ except_in_child.push_back(except);
+ child_delete_flag = false;
+ }
+ }
+ if (child_delete_flag) {
+ sp_object_ref(&child, nullptr);
+ toDelete.push_back(&child);
+ } else {
+ child.cropToObjects(except_in_child);
+ }
+ }
+ }
+ for (auto &i : toDelete) {
+ i->deleteObject(true, true);
+ sp_object_unref(i, nullptr);
+ }
+}
+
+void SPObject::attach(SPObject *object, SPObject *prev)
+{
+ //g_return_if_fail(parent != NULL);
+ //g_return_if_fail(SP_IS_OBJECT(parent));
+ g_return_if_fail(object != nullptr);
+ g_return_if_fail(SP_IS_OBJECT(object));
+ g_return_if_fail(!prev || SP_IS_OBJECT(prev));
+ g_return_if_fail(!prev || prev->parent == this);
+ g_return_if_fail(!object->parent);
+
+ sp_object_ref(object, this);
+ object->parent = this;
+ this->_updateTotalHRefCount(object->_total_hrefcount);
+
+ auto it = children.begin();
+ if (prev != nullptr) {
+ it = ++children.iterator_to(*prev);
+ }
+ children.insert(it, *object);
+
+ if (!object->xml_space.set)
+ object->xml_space.value = this->xml_space.value;
+}
+
+void SPObject::reorder(SPObject* obj, SPObject* prev) {
+ g_return_if_fail(obj != nullptr);
+ g_return_if_fail(obj->parent);
+ g_return_if_fail(obj->parent == this);
+ g_return_if_fail(obj != prev);
+ g_return_if_fail(!prev || prev->parent == obj->parent);
+
+ auto it = children.begin();
+ if (prev != nullptr) {
+ it = ++children.iterator_to(*prev);
+ }
+
+ children.splice(it, children, children.iterator_to(*obj));
+}
+
+void SPObject::detach(SPObject *object)
+{
+ //g_return_if_fail(parent != NULL);
+ //g_return_if_fail(SP_IS_OBJECT(parent));
+ g_return_if_fail(object != nullptr);
+ g_return_if_fail(SP_IS_OBJECT(object));
+ g_return_if_fail(object->parent == this);
+
+ children.erase(children.iterator_to(*object));
+ object->releaseReferences();
+
+ object->parent = nullptr;
+
+ this->_updateTotalHRefCount(-object->_total_hrefcount);
+ sp_object_unref(object, this);
+}
+
+SPObject *SPObject::get_child_by_repr(Inkscape::XML::Node *repr)
+{
+ g_return_val_if_fail(repr != nullptr, NULL);
+ SPObject *result = nullptr;
+
+ if (children.size() > 0 && children.back().getRepr() == repr) {
+ result = &children.back(); // optimization for common scenario
+ } else {
+ for (auto& child: children) {
+ if (child.getRepr() == repr) {
+ result = &child;
+ break;
+ }
+ }
+ }
+ return result;
+}
+
+/**
+ * Get closest child to a reference representation. May traverse backwards
+ * until it finds a child SPObject node.
+ *
+ * @param obj Parent object
+ * @param ref Reference node, may be NULL
+ * @return Child, or NULL if not found
+ */
+static SPObject *get_closest_child_by_repr(SPObject &obj, Inkscape::XML::Node *ref)
+{
+ for (; ref; ref = ref->prev()) {
+ // The most likely situation is that `ref` is indeed a child of `obj`,
+ // so try that first, before checking getObjectByRepr.
+ if (auto result = obj.get_child_by_repr(ref)) {
+ return result;
+ }
+
+ // Only continue if `ref` is not an SPObject, but e.g. an XML comment
+ if (obj.document->getObjectByRepr(ref)) {
+ break;
+ }
+ }
+
+ return nullptr;
+}
+
+void SPObject::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) {
+ SPObject* object = this;
+
+ const std::string type_string = NodeTraits::get_type_string(*child);
+
+ SPObject* ochild = SPFactory::createObject(type_string);
+ if (ochild == nullptr) {
+ // Currently, there are many node types that do not have
+ // corresponding classes in the SPObject tree.
+ // (rdf:RDF, inkscape:clipboard, ...)
+ // Thus, simply ignore this case for now.
+ return;
+ }
+
+ SPObject *prev = get_closest_child_by_repr(*object, ref);
+ object->attach(ochild, prev);
+ sp_object_unref(ochild, nullptr);
+
+ ochild->invoke_build(object->document, child, object->cloned);
+}
+
+void SPObject::release() {
+ SPObject* object = this;
+ debug("id=%p, typename=%s", object, g_type_name_from_instance((GTypeInstance*)object));
+ auto tmp = children | boost::adaptors::transformed([](SPObject& obj){return &obj;});
+ std::vector<SPObject *> toRelease(tmp.begin(), tmp.end());
+
+ for (auto& p: toRelease) {
+ object->detach(p);
+ }
+}
+
+void SPObject::remove_child(Inkscape::XML::Node* child) {
+ debug("id=%p, typename=%s", this, g_type_name_from_instance((GTypeInstance*)this));
+
+ SPObject *ochild = this->get_child_by_repr(child);
+
+ // If the xml node has got a corresponding child in the object tree
+ if (ochild) {
+ this->detach(ochild);
+ }
+}
+
+void SPObject::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node * /*old_ref*/, Inkscape::XML::Node *new_ref) {
+ SPObject* object = this;
+
+ SPObject *ochild = object->get_child_by_repr(child);
+ g_return_if_fail(ochild != nullptr);
+ SPObject *prev = get_closest_child_by_repr(*object, new_ref);
+ object->reorder(ochild, prev);
+ ochild->_position_changed_signal.emit(ochild);
+}
+
+void SPObject::tag_name_changed(gchar const* oldname, gchar const* newname) {
+ g_warning("XML Element renamed from %s to %s!", oldname, newname);
+}
+
+void SPObject::build(SPDocument *document, Inkscape::XML::Node *repr) {
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::build" );
+#endif
+ SPObject* object = this;
+
+ /* Nothing specific here */
+ debug("id=%p, typename=%s", object, g_type_name_from_instance((GTypeInstance*)object));
+
+ object->readAttr(SPAttr::XML_SPACE);
+ object->readAttr(SPAttr::LANG);
+ object->readAttr(SPAttr::XML_LANG); // "xml:lang" overrides "lang" per spec, read it last.
+ object->readAttr(SPAttr::INKSCAPE_LABEL);
+ object->readAttr(SPAttr::INKSCAPE_COLLECT);
+
+ // Inherit if not set
+ if (lang.empty() && object->parent) {
+ lang = object->parent->lang;
+ }
+
+ if(object->cloned && (repr->attribute("id")) ) // The cases where this happens are when the "original" has no id. This happens
+ // if it is a SPString (a TextNode, e.g. in a <title>), or when importing
+ // stuff externally modified to have no id.
+ object->clone_original = document->getObjectById(repr->attribute("id"));
+
+ for (Inkscape::XML::Node *rchild = repr->firstChild() ; rchild != nullptr; rchild = rchild->next()) {
+ const std::string typeString = NodeTraits::get_type_string(*rchild);
+
+ SPObject* child = SPFactory::createObject(typeString);
+ if (child == nullptr) {
+ // Currently, there are many node types that do not have
+ // corresponding classes in the SPObject tree.
+ // (rdf:RDF, inkscape:clipboard, ...)
+ // Thus, simply ignore this case for now.
+ continue;
+ }
+
+ object->attach(child, object->lastChild());
+ sp_object_unref(child, nullptr);
+ child->invoke_build(document, rchild, object->cloned);
+ }
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::build", false );
+#endif
+}
+
+void SPObject::invoke_build(SPDocument *document, Inkscape::XML::Node *repr, unsigned int cloned)
+{
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::invoke_build" );
+#endif
+ debug("id=%p, typename=%s", this, g_type_name_from_instance((GTypeInstance*)this));
+
+ //g_assert(object != NULL);
+ //g_assert(SP_IS_OBJECT(object));
+ g_assert(document != nullptr);
+ g_assert(repr != nullptr);
+
+ g_assert(this->document == nullptr);
+ g_assert(this->repr == nullptr);
+ g_assert(this->getId() == nullptr);
+
+ /* Bookkeeping */
+
+ this->document = document;
+ this->repr = repr;
+ if (!cloned) {
+ Inkscape::GC::anchor(repr);
+ }
+ this->cloned = cloned;
+
+ /* Invoke derived methods, if any */
+ this->build(document, repr);
+
+ if ( !cloned ) {
+ this->document->bindObjectToRepr(this->repr, this);
+
+ if (Inkscape::XML::id_permitted(this->repr)) {
+ /* If we are not cloned, and not seeking, force unique id */
+ gchar const *id = this->repr->attribute("id");
+ if (!document->isSeeking()) {
+ {
+ gchar *realid = sp_object_get_unique_id(this, id);
+ g_assert(realid != nullptr);
+
+ this->document->bindObjectToId(realid, this);
+ SPObjectImpl::setId(this, realid);
+ g_free(realid);
+ }
+
+ /* Redefine ID, if required */
+ if ((id == nullptr) || (std::strcmp(id, this->getId()) != 0)) {
+ this->repr->setAttribute("id", this->getId());
+ }
+ } else if (id) {
+ // bind if id, but no conflict -- otherwise, we can expect
+ // a subsequent setting of the id attribute
+ if (!this->document->getObjectById(id)) {
+ this->document->bindObjectToId(id, this);
+ SPObjectImpl::setId(this, id);
+ }
+ }
+ }
+ } else {
+ g_assert(this->getId() == nullptr);
+ }
+
+
+ /* Signalling (should be connected AFTER processing derived methods */
+ sp_repr_add_listener(repr, &object_event_vector, this);
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::invoke_build", false );
+#endif
+}
+
+int SPObject::getIntAttribute(char const *key, int def)
+{
+ return getRepr()->getAttributeInt(key, def);
+}
+
+unsigned SPObject::getPosition(){
+ g_assert(this->repr);
+
+ return repr->position();
+}
+
+void SPObject::appendChild(Inkscape::XML::Node *child) {
+ g_assert(this->repr);
+
+ repr->appendChild(child);
+}
+
+SPObject* SPObject::nthChild(unsigned index) {
+ g_assert(this->repr);
+ if (hasChildren()) {
+ std::vector<SPObject*> l;
+ unsigned counter = 0;
+ for (auto& child: children) {
+ if (counter == index) {
+ return &child;
+ }
+ counter++;
+ }
+ }
+ return nullptr;
+}
+
+void SPObject::addChild(Inkscape::XML::Node *child, Inkscape::XML::Node * prev)
+{
+ g_assert(this->repr);
+
+ repr->addChild(child,prev);
+}
+
+void SPObject::releaseReferences() {
+ g_assert(this->document);
+ g_assert(this->repr);
+
+ sp_repr_remove_listener_by_data(this->repr, this);
+
+ this->_release_signal.emit(this);
+
+ this->release();
+
+ /* all hrefs should be released by the "release" handlers */
+ g_assert(this->hrefcount == 0);
+
+ if (!cloned) {
+ if (this->id) {
+ this->document->bindObjectToId(this->id, nullptr);
+ }
+ g_free(this->id);
+ this->id = nullptr;
+
+ g_free(this->_default_label);
+ this->_default_label = nullptr;
+
+ this->document->bindObjectToRepr(this->repr, nullptr);
+
+ Inkscape::GC::release(this->repr);
+ } else {
+ g_assert(!this->id);
+ }
+
+ // style belongs to SPObject, we should not need to unref here.
+ // if (this->style) {
+ // this->style = sp_style_unref(this->style);
+ // }
+
+ this->document = nullptr;
+ this->repr = nullptr;
+}
+
+
+SPObject *SPObject::getPrev()
+{
+ SPObject *prev = nullptr;
+ if (parent && !parent->children.empty() && &parent->children.front() != this) {
+ prev = &*(--parent->children.iterator_to(*this));
+ }
+ return prev;
+}
+
+SPObject* SPObject::getNext()
+{
+ SPObject *next = nullptr;
+ if (parent && !parent->children.empty() && &parent->children.back() != this) {
+ next = &*(++parent->children.iterator_to(*this));
+ }
+ return next;
+}
+
+void SPObject::repr_child_added(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, gpointer data)
+{
+ auto object = static_cast<SPObject *>(data);
+
+ object->child_added(child, ref);
+}
+
+void SPObject::repr_child_removed(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node *child, Inkscape::XML::Node * /*ref*/, gpointer data)
+{
+ auto object = static_cast<SPObject *>(data);
+
+ object->remove_child(child);
+}
+
+void SPObject::repr_order_changed(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node *child, Inkscape::XML::Node *old, Inkscape::XML::Node *newer, gpointer data)
+{
+ auto object = static_cast<SPObject *>(data);
+
+ object->order_changed(child, old, newer);
+}
+
+void SPObject::repr_name_changed(Inkscape::XML::Node* repr, gchar const* oldname, gchar const* newname, void *data)
+{
+ auto object = static_cast<SPObject *>(data);
+
+ object->tag_name_changed(oldname, newname);
+}
+
+void SPObject::set(SPAttr key, gchar const* value) {
+
+#ifdef OBJECT_TRACE
+ std::stringstream temp;
+ temp << "SPObject::set: " << sp_attribute_name(key) << " " << (value?value:"null");
+ objectTrace( temp.str() );
+#endif
+
+ g_assert(key != SPAttr::INVALID);
+
+ SPObject* object = this;
+
+ switch (key) {
+
+ case SPAttr::ID:
+
+ //XML Tree being used here.
+ if ( !object->cloned && object->getRepr()->type() == Inkscape::XML::NodeType::ELEMENT_NODE ) {
+ SPDocument *document=object->document;
+ SPObject *conflict=nullptr;
+
+ gchar const *new_id = value;
+
+ if (new_id) {
+ conflict = document->getObjectById((char const *)new_id);
+ }
+
+ if ( conflict && conflict != object ) {
+ if (!document->isSeeking()) {
+ sp_object_ref(conflict, nullptr);
+ // give the conflicting object a new ID
+ gchar *new_conflict_id = sp_object_get_unique_id(conflict, nullptr);
+ conflict->setAttribute("id", new_conflict_id);
+ g_free(new_conflict_id);
+ sp_object_unref(conflict, nullptr);
+ } else {
+ new_id = nullptr;
+ }
+ }
+
+ if (object->getId()) {
+ document->bindObjectToId(object->getId(), nullptr);
+ SPObjectImpl::setId(object, nullptr);
+ }
+
+ if (new_id) {
+ SPObjectImpl::setId(object, new_id);
+ document->bindObjectToId(object->getId(), object);
+ }
+
+ g_free(object->_default_label);
+ object->_default_label = nullptr;
+ }
+ break;
+
+ case SPAttr::INKSCAPE_LABEL:
+ g_free(object->_label);
+ if (value) {
+ object->_label = g_strdup(value);
+ } else {
+ object->_label = nullptr;
+ }
+ g_free(object->_default_label);
+ object->_default_label = nullptr;
+ break;
+
+ case SPAttr::INKSCAPE_COLLECT:
+ if ( value && !std::strcmp(value, "always") ) {
+ object->setCollectionPolicy(SPObject::ALWAYS_COLLECT);
+ } else {
+ object->setCollectionPolicy(SPObject::COLLECT_WITH_PARENT);
+ }
+ break;
+
+ case SPAttr::XML_SPACE:
+ if (value && !std::strcmp(value, "preserve")) {
+ object->xml_space.value = SP_XML_SPACE_PRESERVE;
+ object->xml_space.set = TRUE;
+ } else if (value && !std::strcmp(value, "default")) {
+ object->xml_space.value = SP_XML_SPACE_DEFAULT;
+ object->xml_space.set = TRUE;
+ } else if (object->parent) {
+ SPObject *parent;
+ parent = object->parent;
+ object->xml_space.value = parent->xml_space.value;
+ }
+ object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ break;
+
+ case SPAttr::LANG:
+ if (value) {
+ lang = value;
+ // To do: sanity check
+ }
+ break;
+
+ case SPAttr::XML_LANG:
+ if (value) {
+ lang = value;
+ // To do: sanity check
+ }
+ break;
+
+ case SPAttr::STYLE:
+ object->style->readFromObject( object );
+ object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ break;
+
+ default:
+ break;
+ }
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::set", false );
+#endif
+}
+
+void SPObject::setKeyValue(SPAttr key, gchar const *value)
+{
+ //g_assert(object != NULL);
+ //g_assert(SP_IS_OBJECT(object));
+
+ this->set(key, value);
+}
+
+void SPObject::readAttr(SPAttr keyid)
+{
+ char const *key = sp_attribute_name(keyid);
+
+ assert(key != nullptr);
+ assert(getRepr() != nullptr);
+
+ char const *value = getRepr()->attribute(key);
+
+ setKeyValue(keyid, value);
+}
+
+void SPObject::readAttr(gchar const *key)
+{
+ //g_assert(object != NULL);
+ //g_assert(SP_IS_OBJECT(object));
+ g_assert(key != nullptr);
+
+ //XML Tree being used here.
+ g_assert(this->getRepr() != nullptr);
+
+ auto keyid = sp_attribute_lookup(key);
+ if (keyid != SPAttr::INVALID) {
+ /* Retrieve the 'key' attribute from the object's XML representation */
+ gchar const *value = getRepr()->attribute(key);
+
+ setKeyValue(keyid, value);
+ }
+}
+
+void SPObject::repr_attr_changed(Inkscape::XML::Node * /*repr*/, gchar const *key, gchar const * /*oldval*/, gchar const * /*newval*/, bool is_interactive, gpointer data)
+{
+ auto object = static_cast<SPObject *>(data);
+
+ object->readAttr(key);
+
+ // manual changes to extension attributes require the normal
+ // attributes, which depend on them, to be updated immediately
+ if (is_interactive) {
+ object->updateRepr(0);
+ }
+}
+
+void SPObject::repr_content_changed(Inkscape::XML::Node * /*repr*/, gchar const * /*oldcontent*/, gchar const * /*newcontent*/, gpointer data)
+{
+ auto object = static_cast<SPObject *>(data);
+
+ object->read_content();
+}
+
+/**
+ * Return string representation of space value.
+ */
+static gchar const *sp_xml_get_space_string(unsigned int space)
+{
+ switch (space) {
+ case SP_XML_SPACE_DEFAULT:
+ return "default";
+ case SP_XML_SPACE_PRESERVE:
+ return "preserve";
+ default:
+ return nullptr;
+ }
+}
+
+Inkscape::XML::Node* SPObject::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) {
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::write" );
+#endif
+
+ if (!repr && (flags & SP_OBJECT_WRITE_BUILD)) {
+ repr = this->getRepr()->duplicate(doc);
+ if (!( flags & SP_OBJECT_WRITE_EXT )) {
+ repr->removeAttribute("inkscape:collect");
+ }
+ } else if (repr) {
+ repr->setAttribute("id", this->getId());
+
+ if (this->xml_space.set) {
+ char const *xml_space;
+ xml_space = sp_xml_get_space_string(this->xml_space.value);
+ repr->setAttribute("xml:space", xml_space);
+ }
+
+ if ( flags & SP_OBJECT_WRITE_EXT &&
+ this->collectionPolicy() == SPObject::ALWAYS_COLLECT )
+ {
+ repr->setAttribute("inkscape:collect", "always");
+ } else {
+ repr->removeAttribute("inkscape:collect");
+ }
+
+ if (style) {
+ // Write if property set by style attribute in this object
+ Glib::ustring style_prop = style->write(SPStyleSrc::STYLE_PROP);
+
+ // Write style attributes (SPStyleSrc::ATTRIBUTE) back to xml object
+ bool any_written = false;
+ auto properties = style->properties();
+ for (auto * prop : properties) {
+ if(prop->shall_write(SP_STYLE_FLAG_IFSET | SP_STYLE_FLAG_IFSRC, SPStyleSrc::ATTRIBUTE)) {
+ // WARNING: We don't know for sure if the css names are the same as the attribute names
+ repr->setAttributeOrRemoveIfEmpty(prop->name(), prop->get_value());
+ any_written = true;
+ }
+ }
+ if(any_written) {
+ // We need to ask the object to update the style and keep things in sync
+ // see `case SPAttr::STYLE` above for how the style attr itself does this.
+ style->readFromObject(this);
+ requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+
+ // Check for valid attributes. This may be time consuming.
+ // It is useful, though, for debugging Inkscape code.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if( prefs->getBool("/options/svgoutput/check_on_editing") ) {
+
+ unsigned int flags = sp_attribute_clean_get_prefs();
+ style_prop = sp_attribute_clean_style(repr, style_prop.c_str(), flags);
+ }
+
+ repr->setAttributeOrRemoveIfEmpty("style", style_prop);
+ } else {
+ /** \todo I'm not sure what to do in this case. Bug #1165868
+ * suggests that it can arise, but the submitter doesn't know
+ * how to do so reliably. The main two options are either
+ * leave repr's style attribute unchanged, or explicitly clear it.
+ * Must also consider what to do with property attributes for
+ * the element; see below.
+ */
+ char const *style_str = repr->attribute("style");
+ if (!style_str) {
+ style_str = "NULL";
+ }
+ g_warning("Item's style is NULL; repr style attribute is %s", style_str);
+ }
+ }
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::write", false );
+#endif
+ return repr;
+}
+
+Inkscape::XML::Node * SPObject::updateRepr(unsigned int flags)
+{
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::updateRepr 1" );
+#endif
+
+ if ( !cloned ) {
+ Inkscape::XML::Node *repr = getRepr();
+ if (repr) {
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::updateRepr 1", false );
+#endif
+ return updateRepr(repr->document(), repr, flags);
+ } else {
+ g_critical("Attempt to update non-existent repr");
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::updateRepr 1", false );
+#endif
+ return nullptr;
+ }
+ } else {
+ /* cloned objects have no repr */
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::updateRepr 1", false );
+#endif
+ return nullptr;
+ }
+}
+
+Inkscape::XML::Node * SPObject::updateRepr(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned int flags)
+{
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::updateRepr 2" );
+#endif
+
+ g_assert(doc != nullptr);
+
+ if (cloned) {
+ /* cloned objects have no repr */
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::updateRepr 2", false );
+#endif
+ return nullptr;
+ }
+
+ if (!(flags & SP_OBJECT_WRITE_BUILD) && !repr) {
+ repr = getRepr();
+ }
+
+#ifdef OBJECT_TRACE
+ Inkscape::XML::Node *node = write(doc, repr, flags);
+ objectTrace( "SPObject::updateRepr 2", false );
+ return node;
+#else
+ return this->write(doc, repr, flags);
+#endif
+
+}
+
+/* Modification */
+
+void SPObject::requestDisplayUpdate(unsigned int flags)
+{
+ g_return_if_fail( this->document != nullptr );
+
+#ifndef NDEBUG
+ // expect no nested update calls
+ if (document->update_in_progress) {
+ // observed with LPE on <rect>
+ g_print("WARNING: Requested update while update in progress, counter = %d\n", document->update_in_progress);
+ }
+#endif
+
+ /* requestModified must be used only to set one of SP_OBJECT_MODIFIED_FLAG or
+ * SP_OBJECT_CHILD_MODIFIED_FLAG */
+ g_return_if_fail(!(flags & SP_OBJECT_PARENT_MODIFIED_FLAG));
+ g_return_if_fail((flags & SP_OBJECT_MODIFIED_FLAG) || (flags & SP_OBJECT_CHILD_MODIFIED_FLAG));
+ g_return_if_fail(!((flags & SP_OBJECT_MODIFIED_FLAG) && (flags & SP_OBJECT_CHILD_MODIFIED_FLAG)));
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::requestDisplayUpdate" );
+#endif
+
+ bool already_propagated = (!(this->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG)));
+ //https://stackoverflow.com/a/7841333
+ if ((this->uflags & flags) != flags ) {
+ this->uflags |= flags;
+ }
+ /* If requestModified has already been called on this object or one of its children, then we
+ * don't need to set CHILD_MODIFIED on our ancestors because it's already been done.
+ */
+ if (already_propagated) {
+ if(this->document) {
+ if (parent) {
+ parent->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG);
+ } else {
+ this->document->requestModified();
+ }
+ }
+ }
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::requestDisplayUpdate", false );
+#endif
+
+}
+
+void SPObject::updateDisplay(SPCtx *ctx, unsigned int flags)
+{
+ g_return_if_fail(!(flags & ~SP_OBJECT_MODIFIED_CASCADE));
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::updateDisplay" );
+#endif
+
+ assert(++(document->update_in_progress));
+
+#ifdef SP_OBJECT_DEBUG_CASCADE
+ g_print("Update %s:%s %x %x %x\n", g_type_name_from_instance((GTypeInstance *) this), getId(), flags, this->uflags, this->mflags);
+#endif
+
+ /* Get this flags */
+ flags |= this->uflags;
+ /* Copy flags to modified cascade for later processing */
+ this->mflags |= this->uflags;
+ /* We have to clear flags here to allow rescheduling update */
+ this->uflags = 0;
+
+ // Merge style if we have good reasons to think that parent style is changed */
+ /** \todo
+ * I am not sure whether we should check only propagated
+ * flag. We are currently assuming that style parsing is
+ * done immediately. I think this is correct (Lauris).
+ */
+ if (style) {
+ if ((flags & SP_OBJECT_STYLESHEET_MODIFIED_FLAG)) {
+ style->readFromObject(this);
+ } else if (parent && (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) && (flags & SP_OBJECT_PARENT_MODIFIED_FLAG)) {
+ style->cascade( this->parent->style );
+ }
+ }
+
+ try
+ {
+ this->update(ctx, flags);
+ }
+ catch(...)
+ {
+ /** \todo
+ * in case of catching an exception we need to inform the user somehow that the document is corrupted
+ * maybe by implementing an document flag documentOk
+ * or by a modal error dialog
+ */
+ g_warning("SPObject::updateDisplay(SPCtx *ctx, unsigned int flags) : throw in ((SPObjectClass *) G_OBJECT_GET_CLASS(this))->update(this, ctx, flags);");
+ }
+
+ assert((document->update_in_progress)--);
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::updateDisplay", false );
+#endif
+}
+
+void SPObject::requestModified(unsigned int flags)
+{
+ g_return_if_fail( this->document != nullptr );
+
+ /* requestModified must be used only to set one of SP_OBJECT_MODIFIED_FLAG or
+ * SP_OBJECT_CHILD_MODIFIED_FLAG */
+ g_return_if_fail(!(flags & SP_OBJECT_PARENT_MODIFIED_FLAG));
+ g_return_if_fail((flags & SP_OBJECT_MODIFIED_FLAG) || (flags & SP_OBJECT_CHILD_MODIFIED_FLAG));
+ g_return_if_fail(!((flags & SP_OBJECT_MODIFIED_FLAG) && (flags & SP_OBJECT_CHILD_MODIFIED_FLAG)));
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::requestModified" );
+#endif
+
+ bool already_propagated = (!(this->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG)));
+
+ this->mflags |= flags;
+
+ /* If requestModified has already been called on this object or one of its children, then we
+ * don't need to set CHILD_MODIFIED on our ancestors because it's already been done.
+ */
+ if (already_propagated) {
+ if (parent) {
+ parent->requestModified(SP_OBJECT_CHILD_MODIFIED_FLAG);
+ } else {
+ document->requestModified();
+ }
+ }
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::requestModified", false );
+#endif
+}
+
+void SPObject::emitModified(unsigned int flags)
+{
+ /* only the MODIFIED_CASCADE flag is legal here */
+ g_return_if_fail(!(flags & ~SP_OBJECT_MODIFIED_CASCADE));
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::emitModified", true, flags );
+#endif
+
+#ifdef SP_OBJECT_DEBUG_CASCADE
+ g_print("Modified %s:%s %x %x %x\n", g_type_name_from_instance((GTypeInstance *) this), getId(), flags, this->uflags, this->mflags);
+#endif
+
+ flags |= this->mflags;
+ /* We have to clear mflags beforehand, as signal handlers may
+ * make changes and therefore queue new modification notifications
+ * themselves. */
+ this->mflags = 0;
+
+ sp_object_ref(this);
+
+ this->modified(flags);
+
+ _modified_signal.emit(this, flags);
+ sp_object_unref(this);
+
+#ifdef OBJECT_TRACE
+ objectTrace( "SPObject::emitModified", false );
+#endif
+}
+
+gchar const *SPObject::getTagName() const
+{
+ g_assert(repr != nullptr);
+
+ /// \todo fixme: Exception if object is NULL? */
+ //XML Tree being used here.
+ return getRepr()->name();
+}
+
+gchar const *SPObject::getAttribute(gchar const *key) const
+{
+ g_assert(this->repr != nullptr);
+
+ /// \todo fixme: Exception if object is NULL? */
+ //XML Tree being used here.
+ return (gchar const *) getRepr()->attribute(key);
+}
+
+void SPObject::setAttribute(Inkscape::Util::const_char_ptr key,
+ Inkscape::Util::const_char_ptr value)
+{
+ g_assert(this->repr != nullptr);
+
+ /// \todo fixme: Exception if object is NULL? */
+ //XML Tree being used here.
+ getRepr()->setAttribute(key, value);
+}
+
+void SPObject::setAttributeDouble(Inkscape::Util::const_char_ptr key, double value) {
+ Inkscape::CSSOStringStream os;
+ os << value;
+ setAttribute(key, os.str());
+}
+
+void SPObject::removeAttribute(gchar const *key)
+{
+ /// \todo fixme: Exception if object is NULL? */
+ //XML Tree being used here.
+ getRepr()->removeAttribute(key);
+}
+
+bool SPObject::storeAsDouble( gchar const *key, double *val ) const
+{
+ g_assert(this->getRepr()!= nullptr);
+ double nan = std::numeric_limits<double>::quiet_NaN();
+ double temp_val = ((Inkscape::XML::Node *)(this->getRepr()))->getAttributeDouble(key, nan);
+ if (std::isnan(temp_val)) {
+ return false;
+ }
+ *val = temp_val;
+ return true;
+}
+
+/** Helper */
+gchar *
+sp_object_get_unique_id(SPObject *object,
+ gchar const *id)
+{
+ static unsigned long count = 0;
+
+ g_assert(SP_IS_OBJECT(object));
+
+ count++;
+
+ //XML Tree being used here.
+ gchar const *name = object->getRepr()->name();
+ g_assert(name != nullptr);
+
+ gchar const *local = std::strchr(name, ':');
+ if (local) {
+ name = local + 1;
+ }
+
+ if (id != nullptr) {
+ if (object->document->getObjectById(id) == nullptr) {
+ return g_strdup(id);
+ }
+ }
+
+ size_t const name_len = std::strlen(name);
+ size_t const buflen = name_len + (sizeof(count) * 10 / 4) + 1;
+ gchar *const buf = (gchar *) g_malloc(buflen);
+ std::memcpy(buf, name, name_len);
+ gchar *const count_buf = buf + name_len;
+ size_t const count_buflen = buflen - name_len;
+ do {
+ ++count;
+ g_snprintf(count_buf, count_buflen, "%lu", count);
+ } while ( object->document->getObjectById(buf) != nullptr );
+ return buf;
+}
+
+void SPObject::_requireSVGVersion(Inkscape::Version version) {
+ for ( SPObject::ParentIterator iter=this ; iter ; ++iter ) {
+ SPObject *object = iter;
+ if (SP_IS_ROOT(object)) {
+ SPRoot *root = SP_ROOT(object);
+ if ( root->version.svg < version ) {
+ root->version.svg = version;
+ }
+ }
+ }
+}
+
+// Titles and descriptions
+
+/* Note:
+ Titles and descriptions are stored in 'title' and 'desc' child elements
+ (see section 5.4 of the SVG 1.0 and 1.1 specifications). The spec allows
+ an element to have more than one 'title' child element, but strongly
+ recommends against this and requires using the first one if a choice must
+ be made. The same applies to 'desc' elements. Therefore, these functions
+ ignore all but the first 'title' child element and first 'desc' child
+ element, except when deleting a title or description.
+
+ This will change in SVG 2, where multiple 'title' and 'desc' elements will
+ be allowed with different localized strings.
+*/
+
+gchar * SPObject::title() const
+{
+ return getTitleOrDesc("svg:title");
+}
+
+bool SPObject::setTitle(gchar const *title, bool verbatim)
+{
+ return setTitleOrDesc(title, "svg:title", verbatim);
+}
+
+gchar * SPObject::desc() const
+{
+ return getTitleOrDesc("svg:desc");
+}
+
+bool SPObject::setDesc(gchar const *desc, bool verbatim)
+{
+ return setTitleOrDesc(desc, "svg:desc", verbatim);
+}
+
+char * SPObject::getTitleOrDesc(gchar const *svg_tagname) const
+{
+ char *result = nullptr;
+ SPObject *elem = findFirstChild(svg_tagname);
+ if ( elem ) {
+ //This string copy could be avoided by changing
+ //the return type of SPObject::getTitleOrDesc
+ //to std::unique_ptr<Glib::ustring>
+ result = g_strdup(elem->textualContent().c_str());
+ }
+ return result;
+}
+
+bool SPObject::setTitleOrDesc(gchar const *value, gchar const *svg_tagname, bool verbatim)
+{
+ if (!verbatim) {
+ // If the new title/description is just whitespace,
+ // treat it as though it were NULL.
+ if (value) {
+ bool just_whitespace = true;
+ for (const gchar *cp = value; *cp; ++cp) {
+ if (!std::strchr("\r\n \t", *cp)) {
+ just_whitespace = false;
+ break;
+ }
+ }
+ if (just_whitespace) {
+ value = nullptr;
+ }
+ }
+ // Don't stomp on mark-up if there is no real change.
+ if (value) {
+ gchar *current_value = getTitleOrDesc(svg_tagname);
+ if (current_value) {
+ bool different = std::strcmp(current_value, value);
+ g_free(current_value);
+ if (!different) {
+ return false;
+ }
+ }
+ }
+ }
+
+ SPObject *elem = findFirstChild(svg_tagname);
+
+ if (value == nullptr) {
+ if (elem == nullptr) {
+ return false;
+ }
+ // delete the title/description(s)
+ while (elem) {
+ elem->deleteObject();
+ elem = findFirstChild(svg_tagname);
+ }
+ return true;
+ }
+
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+
+ if (elem == nullptr) {
+ // create a new 'title' or 'desc' element, putting it at the
+ // beginning (in accordance with the spec's recommendations)
+ Inkscape::XML::Node *xml_elem = xml_doc->createElement(svg_tagname);
+ repr->addChild(xml_elem, nullptr);
+ elem = document->getObjectByRepr(xml_elem);
+ Inkscape::GC::release(xml_elem);
+ }
+ else {
+ // remove the current content of the 'text' or 'desc' element
+ auto tmp = elem->children | boost::adaptors::transformed([](SPObject& obj) { return &obj; });
+ std::vector<SPObject*> vec(tmp.begin(), tmp.end());
+ for (auto &child: vec) {
+ child->deleteObject();
+ }
+ }
+
+ // add the new content
+ elem->appendChildRepr(xml_doc->createTextNode(value));
+ return true;
+}
+
+SPObject* SPObject::findFirstChild(gchar const *tagname) const
+{
+ for (auto& child: const_cast<SPObject*>(this)->children)
+ {
+ if (child.repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE &&
+ !std::strcmp(child.repr->name(), tagname)) {
+ return &child;
+ }
+ }
+ return nullptr;
+}
+
+Glib::ustring SPObject::textualContent() const
+{
+ Glib::ustring text;
+
+ for (auto& child: children)
+ {
+ Inkscape::XML::NodeType child_type = child.repr->type();
+
+ if (child_type == Inkscape::XML::NodeType::ELEMENT_NODE) {
+ text += child.textualContent();
+ }
+ else if (child_type == Inkscape::XML::NodeType::TEXT_NODE) {
+ text += child.repr->content();
+ }
+ }
+ return text;
+}
+
+Glib::ustring SPObject::getExportFilename() const
+{
+ if (auto filename = repr->attribute("inkscape:export-filename")) {
+ return Glib::ustring(filename);
+ }
+ return "";
+}
+
+void SPObject::setExportFilename(Glib::ustring filename)
+{
+ // Is this svg has been saved before.
+ const char *doc_filename = document->getDocumentFilename();
+ std::string base = Glib::path_get_dirname(doc_filename ? doc_filename : filename);
+
+ filename = Inkscape::convertPathToRelative(filename, base);
+ repr->setAttributeOrRemoveIfEmpty("inkscape:export-filename", filename.c_str());
+}
+
+Geom::Point SPObject::getExportDpi() const
+{
+ return Geom::Point(
+ repr->getAttributeDouble("inkscape:export-xdpi", 0.0),
+ repr->getAttributeDouble("inkscape:export-ydpi", 0.0));
+}
+
+void SPObject::setExportDpi(Geom::Point dpi)
+{
+ if (!dpi.x() || !dpi.y()) {
+ repr->removeAttribute("inkscape:export-xdpi");
+ repr->removeAttribute("inkscape:export-ydpi");
+ } else {
+ repr->setAttributeSvgDouble("inkscape:export-xdpi", dpi.x());
+ repr->setAttributeSvgDouble("inkscape:export-ydpi", dpi.y());
+ }
+}
+
+// For debugging: Print SP tree structure.
+void SPObject::recursivePrintTree( unsigned level )
+{
+ if (level == 0) {
+ std::cout << "SP Object Tree" << std::endl;
+ }
+ std::cout << "SP: ";
+ for (unsigned i = 0; i < level; ++i) {
+ std::cout << " ";
+ }
+ std::cout << (getId()?getId():"No object id")
+ << " clone: " << std::boolalpha << (bool)cloned
+ << " hrefcount: " << hrefcount << std::endl;
+ for (auto& child: children) {
+ child.recursivePrintTree(level + 1);
+ }
+}
+
+// Function to allow tracing of program flow through SPObject and derived classes.
+// To trace function, add at entrance ('in' = true) and exit of function ('in' = false).
+void SPObject::objectTrace( std::string const &text, bool in, unsigned flags ) {
+ if( in ) {
+ for (unsigned i = 0; i < indent_level; ++i) {
+ std::cout << " ";
+ }
+ std::cout << text << ":"
+ << " entrance: "
+ << (id?id:"null")
+ // << " uflags: " << uflags
+ // << " mflags: " << mflags
+ // << " flags: " << flags
+ << std::endl;
+ ++indent_level;
+ } else {
+ --indent_level;
+ for (unsigned i = 0; i < indent_level; ++i) {
+ std::cout << " ";
+ }
+ std::cout << text << ":"
+ << " exit: "
+ << (id?id:"null")
+ // << " uflags: " << uflags
+ // << " mflags: " << mflags
+ // << " flags: " << flags
+ << std::endl;
+ }
+}
+
+std::ostream &operator<<(std::ostream &out, const SPObject &o)
+{
+ out << (o.getId()?o.getId():"No ID")
+ << " cloned: " << std::boolalpha << (bool)o.cloned
+ << " ref: " << o.refCount
+ << " href: " << o.hrefcount
+ << " total href: " << o._total_hrefcount;
+ return out;
+}
+/*
+ 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 :