diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/object/sp-object.cpp | |
parent | Initial commit. (diff) | |
download | inkscape-upstream.tar.xz inkscape-upstream.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/object/sp-object.cpp | 1848 |
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 : |