From e9d298a94fb25f86140634db0551803d2d9f6eec Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Wed, 7 May 2025 20:57:23 +0200 Subject: [PATCH] Doc: Improve App::ExtensionContainer documentation --- src/App/ExtensionContainer.cpp | 6 +- src/App/ExtensionContainer.h | 353 +++++++++++++++++++++++++-------- 2 files changed, 277 insertions(+), 82 deletions(-) diff --git a/src/App/ExtensionContainer.cpp b/src/App/ExtensionContainer.cpp index 0068b16aed..c41c2b3167 100644 --- a/src/App/ExtensionContainer.cpp +++ b/src/App/ExtensionContainer.cpp @@ -74,15 +74,15 @@ void ExtensionContainer::registerExtension(Base::Type extension, Extension* ext) _extensions[extension] = ext; } -bool ExtensionContainer::hasExtension(Base::Type t, bool derived) const +bool ExtensionContainer::hasExtension(Base::Type type, bool derived) const { // check for the exact type - bool found = _extensions.find(t) != _extensions.end(); + bool found = _extensions.find(type) != _extensions.end(); if (!found && derived) { // and for types derived from it, as they can be cast to the extension for (const auto& entry : _extensions) { - if (entry.first.isDerivedFrom(t)) { + if (entry.first.isDerivedFrom(type)) { return true; } } diff --git a/src/App/ExtensionContainer.h b/src/App/ExtensionContainer.h index 646ef77ab6..0b56e4c781 100644 --- a/src/App/ExtensionContainer.h +++ b/src/App/ExtensionContainer.h @@ -35,33 +35,41 @@ namespace App { class Extension; + /** - * @brief Container which can hold extensions + * @brief A container that can hold extensions. * - * In FreeCAD normally inheritance is a chain, it is not possible to use multiple inheritance. - * The reason for this is that all objects need to be exposed to python, and it is basically - * impossible to handle multiple inheritance in the C-API for python extensions. Also using multiple - * parent classes in python is currently not possible with the default object approach. + * The concept of extensions provides a way to extend the functionality of + * objects in FreeCAD despite Python's limitations with multiple inheritance + * (see below for an explanation). Extensions are FreeCAD objects that act + * like regular objects in the sense that they have properties and class + * methods to define their functionality. However, extensions are not exposed + * as individual usable entities but are used to extend other objects. An + * extended object obtains all the properties and methods of the extension. * - * The concept of extensions allows one to circumvent those problems. Extensions are FreeCAD objects - * which work like normal objects in the sense that they use properties and class methods to define - * their functionality. However, they are not exposed as individual usable entities but are used to - * extend other objects. A extended object gets all the properties and methods of the extension. - * Therefore it is like c++ multiple inheritance, which is indeed used to achieve this on c++ side, - * but provides a few important additional functionalities: - * - Property persistence is handled, save and restore work out of the box - * - The objects python API gets extended too with the extension python API - * - Extensions can be added from c++ and python, even from both together + * As such, it is like C++ multiple inheritance, which is indeed used to + * achieve this on C++ side, but it provides a few important additional + * functionalities as well: + * + * - Property persistence is handled: save and restore work out of the box. + * - The objects Python API gets extended too with the extension Python API. + * - Extensions can be added from C++ and Python, even from both together. + * + * The interoperability with Python is highly important since in FreeCAD, all + * functionality should be easily accessible from both Python and C++. To + * ensure this -- as already noted -- extensions can be added to an object from + * Python. + * + * However, this means that it is not clear from the C++ object type whether an + * extension was added or not: If added from C++, it becomes clear in the type + * due to the use of multiple inheritance. If added from Python, it is a + * runtime extension and not visible from the type. Hence, querying existing + * extensions of an object and accessing its methods works not by type casting + * but by the interface provided in ExtensionContainer. The default workflow + * is to query whether an extension exists and then to get the extension + * object. This interface always works the same, no matter if added from + * Python or C++. * - * The interoperability with python is highly important, as in FreeCAD all functionality should be - * as easily accessible from python as from c++. To ensure this, and as already noted, extensions - * can be added to a object from python. However, this means that it is not clear from the c++ - * object type if an extension was added or not. If added from c++ it becomes clear in the type due - * to the use of multiple inheritance. If added from python it is a runtime extension and not - * visible from type. Hence querying existing extensions of an object and accessing its methods - * works not by type casting but by the interface provided in ExtensionContainer. The default - * workflow is to query if an extension exists and then get the extension object. No matter if added - * from python or c++ this interface works always the same. * @code * if (object->hasExtension(GroupExtension::getClassTypeId())) { * App::GroupExtension* group = object->getExtensionByType(); @@ -69,33 +77,43 @@ class Extension; * } * @endcode * - * To add a extension to an object, it must comply to a single restriction: it must be derived - * from ExtensionContainer. This is important to allow adding extensions from python and also to - * access the universal extension API. As DocumentObject itself derives from ExtensionContainer this - * should be the case automatically in most circumstances. + * To add a extension to an object, it must comply to a single restriction: it + * must be derived from ExtensionContainer. This is important to allow adding + * extensions from Python and also to access the universal extension API. As + * DocumentObject itself derives from ExtensionContainer, this should be the + * case automatically in most circumstances. + * + * Note that two small boilerplate changes are needed in addition to the + * multiple inheritance when adding extensions from C++. * - * Note that two small boilerplate changes are needed next to the multiple inheritance when adding - * extensions from c++. * 1. It must be ensured that the property and type registration is aware of the extensions by using * special macros. - * 2. The extensions need to be initialised in the constructor + * 2. The extensions need to be initialised in the constructor. * * Here is a working example: - * @code - * class AppExport Part : public App::DocumentObject, public App::FirstExtension, public - * App::SecondExtension { PROPERTY_HEADER_WITH_EXTENSIONS(App::Part); + * @code{.cpp} + * class AppExport Part : public App::DocumentObject + * , public App::FirstExtension + * , public App::SecondExtension + * { + * PROPERTY_HEADER_WITH_EXTENSIONS(App::Part); * }; + * * PROPERTY_SOURCE_WITH_EXTENSIONS(App::Part, App::DocumentObject) - * Part::Part(void) { + * + * Part::Part(void) + * { * FirstExtension::initExtension(this); * SecondExtension::initExtension(this); * } * @endcode * - * From python adding an extension is easier, it must be simply registered to a document object - * at object initialisation like done with properties. Note that the special python extension - * objects need to be added, not the c++ objects. Normally the only difference in name is the - * additional "Python" at the end of the extension name. + * From Python, adding an extension is easier: It must be simply registered to + * a document object at object initialisation like done with properties. Note + * that the special Python extension objects need to be added, not the C++ + * objects. Normally the only difference in name is the additional "Python" at + * the end of the extension name. + * * @code{.py} * class Test(): * __init(self)__: @@ -103,15 +121,28 @@ class Extension; * registerExtension("App::SecondExtensionPython", self) * @endcode * - * Extensions can provide methods that should be overridden by the extended object for customisation - * of the extension behaviour. In c++ this is as simple as overriding the provided virtual - * functions. In python a class method must be provided which has the same name as the method to - * override. This method must not necessarily be in the object that is extended, it must be in the - * object which is provided to the "registerExtension" call as second argument. This second argument - * is used as a proxy and enqueired if the method to override exists in this proxy before calling - * it. + * Extensions can provide methods that should be overridden by the extended + * object for customisation of the extension behaviour. In C++ this is as + * simple as overriding the provided virtual functions. In Python a class + * method must be provided which has the same name as the method to override. + * This method must not necessarily be in the object that is extended, it must + * be in the object which is provided to the "registerExtension" call as second + * argument. This second argument is used as a proxy and queried if the method + * to override exists in this proxy before calling it. * - * For information on howto create extension see the documentation of Extension + * For information on how to create extension see the documentation of + * Extension. + * + * @section{sec_limitations_python_multiple_inheritance Limitations of Python} + * + * Without this extension system, it would be challenging to use extending + * functionality in FreeCAD. Although C++ supports multiple inheritance, it is + * not possible to use it in FreeCAD because it should be possible to expose + * all objects to Python. + * + * However, using multiple parent classes in Python is currently not possible + * with the default object approach. Moreover, it is basically impossible to + * handle multiple inheritance in the C-API for Python extensions. */ class AppExport ExtensionContainer: public App::PropertyContainer { @@ -119,21 +150,96 @@ class AppExport ExtensionContainer: public App::PropertyContainer TYPESYSTEM_HEADER_WITH_OVERRIDE(); public: + /// A type alias for iterating extensions. using ExtensionIterator = std::map::iterator; + /// Construct an extension container. ExtensionContainer(); + /// Destruct an extension container. ~ExtensionContainer() override; + /** + * @brief Register an extension. + * + * @param[in] extension The type of the extension to register. + * @param[in] ext The extension to register. + */ void registerExtension(Base::Type extension, App::Extension* ext); - // returns first of type (or derived from if set to true) and throws otherwise - bool hasExtension(Base::Type, bool derived = true) const; - // this version does not check derived classes + + /** + * @brief Whether this container has an extension of the given type. + * + * This version checks for derived types. It returns the first extension + * of the given type or the first extension that is derived from the type + * if @p derived is true. + * + * @param[in] type The type of the extension to check for. + * @param[in] derived (Optionally) Whether to check for derived types, true by default. + * + * @return True if the container has the (derived) extension, false otherwise. + */ + bool hasExtension(Base::Type type, bool derived = true) const; + + /** + * @brief Whether this container has an extension of the given type. + * + * This version checks for derived types. It returns the first extension + * of the given type. Note that this function does not check for derived + * types. + * + * @param[in] name The name of the extension to check for. + * @return True if the container has the extension, false otherwise. + */ bool hasExtension(const std::string& name) const; + + /** + * @brief Whether this container has extensions. + * + * @return True if the container has extensions, false otherwise. + */ bool hasExtensions() const; + + /** + * @brief Get the extension of the given type. + * + * This version checks for derived types. It returns the first extension + * of the given type or the first extension that is derived from the type + * if @p derived is true. If @p no_except is true, it returns nullptr, + *otherwise it throws an exception if no extension of the given type is + * found. + * + * @param[in] type The type of the extension to get. + * @param[in] derived (Optionally) Whether to check for derived types, true by default. + * @param[in] no_except (Optionally) Whether to throw an exception if no extension is found, + * false by default. + * + * @return The extension of the given type or `nullptr` if not found. + * @throws Base::TypeError if no extension of the given type is found and + * @p no_except is false. + */ App::Extension* getExtension(Base::Type, bool derived = true, bool no_except = false) const; - // this version does not check derived classes + + /** + * @brief Get the extension with the given name. + * + * This version does not check for derived types. + * + * @param[in] name The name of the extension to get. + * + * @return The extension with the given name or `nullptr` if not found. + */ App::Extension* getExtension(const std::string& name) const; - // this version checks for derived types and doesn't throw + + /** + * @brief Get the extension of the given type. + * + * This version checks for derived types. It returns the first extension + * of the given type or the first extension that is derived from the type. + * It doesn't throw an exception but returns `nullptr` if not found. + * + * @tparam ExtensionT The type of the extension to get. + * @return The extension of the given type or `nullptr` if not found. + */ template ExtensionT* getExtension() const { @@ -141,7 +247,24 @@ public: getExtension(ExtensionT::getExtensionClassTypeId(), true, true)); } - // returns first of type (or derived from) and throws otherwise + /** + * @brief Get the extension with the given type. + * + * This version checks for derived types if @p derived is true. It returns the first + * extension of the given type or the first extension that is derived from the type + * if @p derived is true. If not found, it returns `nullptr` if @p no_except is true, + * otherwise it throws an exception. + * + * @tparam ExtensionT The type of the extension to get. + * + * @param[in] no_except (Optionally) Whether to throw an exception if no extension is found, + * false by default. + * @param[in] derived (Optionally) Whether to check for derived types, true by default. + * + * @return The extension of the given type or `nullptr` if not found if @p no_except is true. + * @throws Base::TypeError if no extension of the given type is found and + * @p no_except is false. + */ template ExtensionT* getExtensionByType(bool no_except = false, bool derived = true) const { @@ -149,8 +272,20 @@ public: getExtension(ExtensionT::getExtensionClassTypeId(), derived, no_except)); } - // get all extensions which have the given base class + /** + * @brief Get all extensions with the given type as base class. + * + * @param[in] type The type of the extension to get. + * @return A vector of extensions of the given type. + */ std::vector getExtensionsDerivedFrom(Base::Type type) const; + + /** + * @brief Get all extensions with the given type as base class. + * + * @tparam ExtensionT The type of the extension to get. + * @return A vector of extensions of the given type. + */ template std::vector getExtensionsDerivedFromType() const { @@ -163,78 +298,138 @@ public: return typevec; } + /** + * @brief Get the begin iterator for the extensions. + * + * @return The begin iterator for the extensions. + */ ExtensionIterator extensionBegin() { return _extensions.begin(); } + + /** + * @brief Get the end iterator for the extensions. + * + * @return The end iterator for the extensions. + */ ExtensionIterator extensionEnd() { return _extensions.end(); } - /** @name Access properties */ - //@{ - /// find a property by its name + /** @name Access properties + * + * @{ + */ + Property* getPropertyByName(const char* name) const override; - /// find a property by its name, dynamic cased to specified type + + /** + * @brief Find a property by its name and cast it to the specified type. + * + * This method finds a property by its name and casts it to the specified + * type with a `freecad_cast`. + * + * @tparam T The property type to which the property is cast. + * @param[in] name The name of the property to find. + * @return The property if found cast to the specified type, or `nullptr` if not found. + */ template T* getPropertyByName(const char* name) const { return freecad_cast(this->getPropertyByName(name)); } - /// get the name of a property + const char* getPropertyName(const Property* prop) const override; - /// get all properties of the class (including properties of the parent) + void getPropertyMap(std::map& Map) const override; - /// See PropertyContainer::visitProperties for semantics + void visitProperties(const std::function& visitor) const override; - /// get all properties of the class (including properties of the parent) + void getPropertyList(std::vector& List) const override; - /// get the Type of a Property short getPropertyType(const Property* prop) const override; - /// get the Type of a named Property + short getPropertyType(const char* name) const override; - /// get the Group of a Property + const char* getPropertyGroup(const Property* prop) const override; - /// get the Group of a named Property + const char* getPropertyGroup(const char* name) const override; - /// get the Group of a Property + const char* getPropertyDocumentation(const Property* prop) const override; - /// get the Group of a named Property + const char* getPropertyDocumentation(const char* name) const override; - //@} + ///@} void onChanged(const Property*) override; void Save(Base::Writer& writer) const override; + void Restore(Base::XMLReader& reader) override; // those methods save/restore the dynamic extensions without handling properties, which is // something done by the default Save/Restore methods. + /** + * @brief Save the extensions to the given writer. + * + * This method saves the dynamic extensions to the given writer without + * handling properties, which is something done by the default Save/Restore + * methods. + * + * @param[in,out] writer The writer to save the extensions to. + */ void saveExtensions(Base::Writer& writer) const; + + /** + * @brief Restore the extensions from the given reader. + * + * This method restores the dynamic extensions from the given reader + * without handling properties, which is something done by the default + * Save/Restore methods. + * + * @param[in,out] reader The reader to restore the extensions from. + */ void restoreExtensions(Base::XMLReader& reader); - /** Extends the rules for handling property name changed, so that extensions are given an - * opportunity to handle it. If an extension handles a change, neither the rest of the - * extensions, nor the container itself get to handle it. + /** + * @brief Handle a changed property name during restore. * - * Extensions get their extensionHandleChangedPropertyName() called. + * This method extends the rules for handling property name changed, so + * that extensions are given an opportunity to handle it. If an extension + * handles a change, neither the rest of the extensions, nor the container + * itself get to handle it. * - * If no extension handles the request, then the containers handleChangedPropertyName() is + * Extensions get their Extension::extensionHandleChangedPropertyName() * called. + * + * If no extension handles the request, then + * PropertyContainer::handleChangedPropertyName() is called. + * + * @param[in,out] reader The reader to restore the extensions from. + * @param[in] TypeName The name of the type of the property to handle. + * @param[in] PropName The name of the property to handle. */ void handleChangedPropertyName(Base::XMLReader& reader, const char* TypeName, const char* PropName) override; - /** Extends the rules for handling property type changed, so that extensions are given an - * opportunity to handle it. If an extension handles a change, neither the rest of the - * extensions, nor the container itself get to handle it. + + /** + * @brief Handle a changed property type during restore. * - * Extensions get their extensionHandleChangedPropertyType() called. + * This method extends the rules for handling property type changed, so + * that extensions are given an opportunity to handle it. If an extension + * handles a change, neither the rest of the extensions, nor the container + * itself get to handle it. * - * If no extension handles the request, then the containers handleChangedPropertyType() is - * called. + * Extensions get their Extension::extensionHandleChangedPropertyType() called. + * + * If no extension handles the request, then + * PropertyContainer::handleChangedPropertyType() is called. + * + * @param[in,out] reader The reader to restore the extensions from. + * @param[in] TypeName The name of the type of the property to handle. + * @param[in] prop The property that needs to be restored. Its type differs from `TypeName`. */ void handleChangedPropertyType(Base::XMLReader& reader, const char* TypeName,