// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2006 Werner Mayer * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include #include "Document.h" #include "GeoFeatureGroupExtension.h" #include "GroupExtensionPy.h" using namespace App; namespace sp = std::placeholders; EXTENSION_PROPERTY_SOURCE(App::GroupExtension, App::DocumentObjectExtension) namespace App { EXTENSION_PROPERTY_SOURCE_TEMPLATE(App::GroupExtensionPython, App::GroupExtension) // explicit template instantiation template class AppExport ExtensionPythonT>; } // namespace App GroupExtension::GroupExtension() { initExtensionType(GroupExtension::getExtensionClassTypeId()); EXTENSION_ADD_PROPERTY_TYPE(Group, (nullptr), "Base", (App::PropertyType)(Prop_None), "List of referenced objects"); EXTENSION_ADD_PROPERTY_TYPE(_GroupTouched, (false), "Base", PropertyType(Prop_Hidden | Prop_Transient), 0); } GroupExtension::~GroupExtension() = default; DocumentObject* GroupExtension::addObject(const char* sType, const char* pObjectName) { DocumentObject* obj = getExtendedObject()->getDocument()->addObject(sType, pObjectName); if (!allowObject(obj)) { getExtendedObject()->getDocument()->removeObject(obj->getNameInDocument()); return nullptr; } addObject(obj); return obj; } std::vector GroupExtension::addObject(DocumentObject* obj) { std::vector vec = {obj}; return addObjects(vec); } std::vector GroupExtension::addObjects(std::vector objs) { std::vector added; std::vector grp = Group.getValues(); for (auto obj : objs) { if (!allowObject(obj)) { continue; } if (hasObject(obj)) { continue; } // only one group per object. Note that it is allowed to be in a group and geofeaturegroup. // However, getGroupOfObject() returns only normal groups, no GeoFeatureGroups. Hence this // works. auto* group = App::GroupExtension::getGroupOfObject(obj); if (group && group != getExtendedObject()) { group->getExtensionByType()->removeObject(obj); } // if we are in a geofeaturegroup we need to ensure the object is too auto geogrp = GeoFeatureGroupExtension::getGroupOfObject(getExtendedObject()); auto objgrp = GeoFeatureGroupExtension::getGroupOfObject(obj); if (geogrp != objgrp) { // what to do depends on if we are in geofeature group or not if (geogrp) { geogrp->getExtensionByType()->addObject(obj); } else { objgrp->getExtensionByType()->removeObject(obj); } } grp.push_back(obj); added.push_back(obj); } Group.setValues(grp); return added; } std::vector GroupExtension::setObjects(std::vector obj) { Group.setValues(std::vector()); return addObjects(obj); } std::vector GroupExtension::removeObject(DocumentObject* obj) { std::vector vec = {obj}; return removeObjects(vec); } std::vector GroupExtension::removeObjects(std::vector objs) { const std::vector& grp = Group.getValues(); std::vector newGrp = grp; std::vector removed; std::vector::iterator end = newGrp.end(); for (auto obj : objs) { auto res = std::remove(newGrp.begin(), end, obj); if (res != end) { end = res; removed.push_back(obj); } } newGrp.erase(end, newGrp.end()); if (grp.size() != newGrp.size()) { Group.setValues(newGrp); } return removed; } void GroupExtension::removeObjectsFromDocument() { while (Group.getSize() > 0) { // Remove the objects step by step because it can happen // that an object is part of several groups and thus a // double destruction could be possible const std::vector& grp = Group.getValues(); removeObjectFromDocument(grp.front()); } } void GroupExtension::removeObjectFromDocument(DocumentObject* obj) { // check that object is not invalid if (!obj || !obj->isAttachedToDocument()) { return; } // remove all children if (obj->hasExtension(GroupExtension::getExtensionClassTypeId())) { GroupExtension* grp = static_cast( obj->getExtension(GroupExtension::getExtensionClassTypeId())); // recursive call to remove all subgroups grp->removeObjectsFromDocument(); } getExtendedObject()->getDocument()->removeObject(obj->getNameInDocument()); } DocumentObject* GroupExtension::getObject(const char* Name) const { DocumentObject* obj = getExtendedObject()->getDocument()->getObject(Name); if (obj && hasObject(obj)) { return obj; } return nullptr; } bool GroupExtension::hasObject(const DocumentObject* obj, bool recursive) const { if (obj == getExtendedObject()) { return false; } try { const std::vector& grp = Group.getValues(); for (auto child : grp) { if (!child) { continue; } if (child == obj) { return true; } else if (child == getExtendedObject()) { throw Base::RuntimeError( "Cyclic dependencies detected: Search cannot be performed"); } else if (recursive && child->hasExtension(GroupExtension::getExtensionClassTypeId())) { App::GroupExtension* subGroup = static_cast( child->getExtension(GroupExtension::getExtensionClassTypeId())); std::vector history; history.push_back(this); if (subGroup->recursiveHasObject(obj, subGroup, history)) { return true; } } } return false; } catch (const Base::RuntimeError& e) { e.reportException(); return false; } } bool GroupExtension::recursiveHasObject(const DocumentObject* obj, const GroupExtension* group, std::vector history) const { // the purpose is to prevent infinite recursion when groups form a cyclic graph. To do this // we store every group we processed on the current leave of the tree, and if we reach an // already processed group we know that it not really is a tree but a cycle. history.push_back(this); // we use hasObject with out recursion to allow override in derived classes if (group->hasObject(obj, false)) { return true; } // we checked for the searched object already with hasObject and did not find it, now we need to // do the same for all subgroups for (auto child : group->Group.getValues()) { if (!child) { continue; } if (child->hasExtension(GroupExtension::getExtensionClassTypeId())) { auto ext = child->getExtensionByType(); if (std::ranges::find(history, ext) != history.end()) { throw Base::RuntimeError( "Cyclic dependencies detected: Search cannot be performed"); } if (recursiveHasObject(obj, ext, history)) { return true; } } } return false; } bool GroupExtension::isChildOf(const GroupExtension* group, bool recursive) const { return group->hasObject(getExtendedObject(), recursive); } const std::vector& GroupExtension::getObjects() const { return Group.getValues(); } std::vector GroupExtension::getObjectsOfType(const Base::Type& typeId) const { std::vector type; const std::vector& grp = Group.getValues(); for (auto it : grp) { if (it->isDerivedFrom(typeId)) { type.push_back(it); } } return type; } int GroupExtension::countObjectsOfType(const Base::Type& typeId) const { int type = 0; const std::vector& grp = Group.getValues(); for (auto it : grp) { if (it->isDerivedFrom(typeId)) { type++; } } return type; } DocumentObject* GroupExtension::getGroupOfObject(const DocumentObject* obj) { // note that we return here only Groups, but nothing derived from it, e.g. no GeoFeatureGroups. // That is important as there are clear differences between groups/geofeature groups (e.g. an // object can be in only one group, and only one geofeaturegroup, however, it can be in both at // the same time) for (auto o : obj->getInList()) { Extension* ext = o->getExtension(GroupExtension::getExtensionClassTypeId(), false, true); if (!ext) { ext = o->getExtension(GroupExtensionPython::getExtensionClassTypeId(), false, true); } if (ext) { auto grp = static_cast(ext)->Group.getValues(); if (std::find(grp.begin(), grp.end(), obj) != grp.end()) { return o; } } } return nullptr; } PyObject* GroupExtension::getExtensionPyObject() { if (ExtensionPythonObject.is(Py::_None())) { // ref counter is set to 1 auto grp = new GroupExtensionPy(this); ExtensionPythonObject = Py::Object(grp, true); } return Py::new_reference_to(ExtensionPythonObject); } void GroupExtension::extensionOnChanged(const Property* p) { // objects are only allowed in a single group. Note that this check must only be done for normal // groups, not any derived classes if ((this->getExtensionTypeId() == GroupExtension::getExtensionClassTypeId()) && p == &Group && !Group.testStatus(Property::User3)) { if (!getExtendedObject()->isRestoring() && !getExtendedObject()->getDocument()->isPerformingTransaction()) { bool error = false; auto corrected = Group.getValues(); for (auto obj : Group.getValues()) { // we have already set the obj into the group, so in a case of multiple groups // getGroupOfObject would return anyone of it and hence it is possible that we miss // an error. We need a custom check auto list = obj->getInList(); for (auto in : list) { auto ext = in->getExtension(GroupExtension::getExtensionClassTypeId(), false, true); if (ext && (in != getExtendedObject())) { auto grp = static_cast(ext)->Group.getValues(); if (std::find(grp.begin(), grp.end(), obj) != grp.end()) { error = true; corrected.erase(std::remove(corrected.begin(), corrected.end(), obj), corrected.end()); } } } } // if an error was found we need to correct the values and inform the user if (error) { Base::ObjectStatusLocker guard(Property::User3, &Group); Group.setValues(corrected); throw Base::RuntimeError("Object can only be in a single Group"); } } } if (p == &Group) { _Conns.clear(); for (auto obj : Group.getValue()) { if (obj && obj->isAttachedToDocument()) { // NOLINTBEGIN _Conns[obj] = obj->signalChanged.connect( std::bind(&GroupExtension::slotChildChanged, this, sp::_1, sp::_2)); // NOLINTEND } } } App::Extension::extensionOnChanged(p); } void GroupExtension::slotChildChanged(const DocumentObject& obj, const Property& prop) { if (&prop == &obj.Visibility) { _GroupTouched.touch(); } } bool GroupExtension::extensionGetSubObject(DocumentObject*& ret, const char* subname, PyObject** pyObj, Base::Matrix4D* mat, bool /*transform*/, int depth) const { const char* dot; if (!subname || *subname == 0) { auto obj = freecad_cast(getExtendedContainer()); ret = const_cast(obj); return true; } dot = strchr(subname, '.'); if (!dot) { return false; } if (subname[0] != '$') { ret = Group.findUsingMap(std::string(subname, dot)); } else { std::string name = std::string(subname + 1, dot); for (auto child : Group.getValues()) { if (name == child->Label.getStrValue()) { ret = child; break; } } } if (!ret) { return false; } return ret->getSubObject(dot + 1, pyObj, mat, true, depth + 1); } bool GroupExtension::extensionGetSubObjects(std::vector& ret, int) const { for (auto obj : Group.getValues()) { if (obj && obj->isAttachedToDocument()) { ret.push_back(std::string(obj->getNameInDocument()) + '.'); } } return true; } App::DocumentObjectExecReturn* GroupExtension::extensionExecute() { // This touch property is for propagating changes to upper group _GroupTouched.touch(); return inherited::extensionExecute(); } std::vector GroupExtension::getAllChildren() const { std::vector res; std::set rset; getAllChildren(res, rset); return res; } void GroupExtension::getAllChildren(std::vector& res, std::set& rset) const { for (auto obj : Group.getValues()) { if (!obj || !obj->isAttachedToDocument()) { continue; } if (!rset.insert(obj).second) { continue; } res.push_back(obj); auto ext = obj->getExtensionByType(true, false); if (ext) { ext->getAllChildren(res, rset); } } }