Files
create/src/App/core-app.dox
Pieter Hijma 951afad93a Doc: Move Extension documentation to a topic
This commit constitutes an almost verbatim move from the documentation
of Extension and ExtensionContainer to its own topic in core-app.dox.
2025-05-20 13:39:26 +02:00

446 lines
20 KiB
Plaintext

/**
* @defgroup APP App
* @ingroup CORE
* @brief The part of FreeCAD that works without GUI (console or server mode)
*
* @details It contains the App namespace and defines core concepts such as
* @ref DocumentGroup "Document", @ref DocumentObjectGroup "Document Object",
* @ref PropertyFramework "Property Framework", @ref ExpressionFramework
* "Expression Framework", and the @ref ExtensionFramework "Extension
* Framework".
*
* The largest difference between the functionality in @ref BASE "Base"
* compared to %App is that %App introduces the notion of properties, both used
* in @ref App::Document "Document" and @ref App::DocumentObject
* "DocumentObject". In addition, %App has a representation of the running
* @ref App::Application "Application".
*/
/**
* @defgroup DocumentGroup Document
* @ingroup APP
* @brief The class that represents a FreeCAD document
*
* This (besides the App::Application class) is the most important class in FreeCAD.
* It contains all the data of the opened, saved, or newly created FreeCAD Document.
* The App::Document manages the Undo and Redo mechanism and the linking of documents.
*
* Note: the documents are not free objects. They are completely handled by the
* App::Application. Only the Application can Open or destroy a document.
*
* \section Exception Exception handling
* As the document is the main data structure of FreeCAD we have to take a close
* look at how Exceptions affect the integrity of the App::Document.
*
* \section UndoRedo Undo Redo an Transactions
* Undo Redo handling is one of the major mechanism of a document in terms of
* user friendliness and speed (no one will wait for Undo too long).
*
* \section Dependency Graph and dependency handling
* The FreeCAD document handles the dependencies of its DocumentObjects with
* an adjacency list. This gives the opportunity to calculate the shortest
* recompute path. Also, it enables more complicated dependencies beyond trees.
*
* @see App::Application
* @see App::DocumentObject
*/
/**
* @defgroup DocumentObjectGroup Document Object
* @ingroup APP
* @brief %Base class of all objects handled in the Document.
*/
/**
* @defgroup PropertyFramework Property framework
* @ingroup APP
* @brief System to access object properties.
*
* @section propframe_intro Introduction
*
* The property framework introduces an intricate system to access properties
* of objects. It provides the ability to:
* 1. define properties in a class and access them by name,
* 2. add properties to a class at runtime and access them by name, and
* 3. access properties of a class by name without knowing the class type.
*
* The first two points are similar to what the dynamic reflection framework of
* C# or Java offer. The third point allows FreeCAD to have App::Property as a
* generic interface to access properties. This is similar to the way that
* Python allows to access properties of a class by name.
*
* This ability is introduced by the @ref App::PropertyContainer
* "PropertyContainer" class and can be used by all derived classes. In
* particular, there are two classes that inherit from @ref
* App::PropertyContainer "PropertyContainer" which are @ref App::Document
* "Document" and @ref App::DocumentObject "DocumentObject". These two classes
* serve different purposes but are both able to hold properties. @ref
* App::PropertyContainer "PropertyContainer" contains the shared logic to do
* so.
*
* @section propframe_static_dynamic_props Static/Dynamic Properties
*
* In C++, it is possible to define properties as part of the class. These can
* be considered "static" properties but this term is typically not used within
* FreeCAD. Properties created in a class cannot be removed from a @ref
* App::PropertyContainer "PropertyContainer".
*
* However, it is also possible to add properties to a @ref
* App::PropertyContainer "PropertyContainer" at runtime. These properties are
* called "dynamic properties" and these properties can be freely added and
* removed by users. In Python, all properties are dynamic properties.
*
* @section propframe_mechanisms Mechanisms
*
* The macros `PROPERTY_HEADER` and `PROPERTY_SOURCE` (and variants) are used
* to define a private static PropertyData member in the class. This data
* structure contains a multi index that maps 1) the property name to the
* property specifications @ref App::PropertyData::PropertySpec "PropertySpec",
* allowing access of properties by name, and 2) maps the offset of a property
* with respect to its container, allowing access to property specifications.
*
* The static function @ref App::PropertyContainer::getPropertyDataPtr()
* "PropertyContainer::getPropertyDataPtr()" is used to access the class-wide
* static @ref App::PropertyData "PropertyData" structure shared by all
* instances of the class. The virtual function @ref
* App::PropertyContainer::getPropertyData()
* "PropertyContainer::getPropertyData()" allows access to the class-level
* static PropertyData based on the dynamic type of the object, even when
* downcasted. This allows for example a @ref App::PropertyInteger
* "PropertyInteger*" downcasted to @ref App::Property "Property*" to access
* all its properties.
*
* Since the @ref App::PropertyData "PropertyData" structure is static in the
* class and maintains static information of properties, such as the group and
* documentation, we need to be able to access a specific instance given a
* property name. This is achieved by looking up the @ref
* App::PropertyData::PropertySpec "PropertySpec" in the index based on the
* property name. The property specification stores an offset to the property
* address given an @ref App::PropertyData::OffsetBase "OffsetBase" which wraps
* the address of a @ref App::PropertyContainer "PropertyContainer". See @ref
* App::PropertyData::findProperty() "PropertyData::findProperty()" and @ref
* App::PropertyData::OffsetBase::getOffsetTo() "OffsetBase::getOffsetTo()".
* The offset of the property relative to the offset base gives us the address
* of the property in the instance. Note that different values for properties
* across property containers are stored in the @ref App::Property "Property" instances.
*
* The reverse is also needed: Given a property in a property container, it is
* possible to compute the offset relative to the base of the property
* container. The index in @ref App::PropertyData "PropertyData" allows us to
* acquire the property spec and provides us with the static data associated
* with the property.
*
* Dynamic properties are stored in their own @ref App::DynamicProperty
* "DynamicProperty" container. It can be added with the function @ref
* App::PropertyContainer::addDynamicProperty()
* "PropertyContainer::addDynamicProperty()" and removed with @ref
* App::PropertyContainer::removeDynamicProperty()
* "PropertyContainer::removeDynamicProperty()". The rest of the interface of
* dynamic properties is virtually the same. In general, in FreeCAD, it is
* difficult for users to know whether a property is dynamic or static and they
* typically do not need to be concerned with this distinction.
*
* @section Locking Dynamic Properties
*
* Since in Python all properties are dynamic properties, it is difficult to
* understand whether properties are part of a Python class and are necessary
* for the functioning of the class, or whether a user has added these
* properties. Therefore, it is possible to indicate that a property is
* "locked":
*
* @code
* prop->setStatus(Property::LockDynamic, true);
* @endcode
*
* In Python, this can be indicated in the `addProperty()` function:
*
* @code
* addProperty(type: string, name: string, group="", doc="",
* attr=0, read_only=False, hidden=False,
* locked=False, enum_vals=[])
* @endcode
*
* The Property Framework makes it possible in the first place to make an
* automatic mapping to python (e.g. in App::FeaturePy) and abstract editing
* properties in Gui::PropertyEditor.
*
* @section Examples
*
* Here some little examples how to use the property framework:
*
* @code
* // Acquire a property by name and return the value as a Python object.
* Property *prop = feature->getPropertyByName(nameProperty);
* if (prop) {
* return prop->getPyObject();
* }
* @endcode
*
* or:
*
* @code
* // Restore properties from a reader.
* void PropertyContainer::Restore(Base::Reader &reader)
* {
* reader.readElement("Properties");
* int Cnt = reader.getAttributeAsInteger("Count");
*
* for(int i=0 ;i<Cnt ;i++) {
* reader.readElement("Property");
* string PropName = reader.getAttribute("name");
* Property* prop = getPropertyByName(PropName.c_str());
* if(prop) {
* prop->Restore(reader);
* }
*
* reader.readEndElement("Property");
* }
* reader.readEndElement("Properties");
* }
* @endcode
*/
/**
* @defgroup ExpressionFramework Expressions framework
* @ingroup APP
* @brief The expression system allows users to write expressions and formulas that produce values
*/
/**
* @defgroup ExtensionFramework Extension framework
* @ingroup APP
* @brief The extension system provides a way to extend the functionality of Document Objects.
*
* 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.
*
* 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++.
*
* @code
* if (object->hasExtension(GroupExtension::getClassTypeId())) {
* App::GroupExtension* group = object->getExtensionByType<GroupExtension>();
* group->hasObject(...);
* }
* @endcode
*
* To add an 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++.
*
* 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.
*
* Here is a working example:
* @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)
* {
* 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.
*
* @code{.py}
* class Test():
* __init(self)__:
* registerExtension("App::FirstExtensionPython", self)
* 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 queried if the method
* to override exists in this proxy before calling it.
*
* @section SecExtensionFrameworkCreateExtension Create an Extension
*
* An Extension is like every other FreeCAD object and is based on properties.
* All information storage and persistence should be achieved by use of these
* properties. Additionally, any number of methods can be added to provide
* functionality related to the properties. There are three small differences
* to normal objects:
*
* 1. They must be derived from this Extension class,
* 2. Properties must be handled with special extension macros.
* 3. Extensions must be initialised.
*
* The following code illustrates the basic setup of an extension:
*
* @code
* class MyExtension : public Extension
* {
* EXTENSION_PROPERTY_HEADER(MyExtension);
* PropertyInt MyProp;
* virtual bool overridableMethod(DocumentObject* obj) {};
* };
*
* EXTENSION_PROPERTY_SOURCE(App::MyExtension, App::Extension)
* MyExtension::MyExtension()
* {
* EXTENSION_ADD_PROPERTY(MyProp, (0))
* initExtensionType(MyExtension::getExtensionClassTypeId());
* }
*
* using MyExtensionPython = ExtensionPythonT<MyExtension>;
* @endcode
*
* The special Python extension type created above is important, as only those Python extensions
* can be added to an object from Python. It does not work to add the C++ version directly there.
*
* Note that every method of the extension becomes part of the extended object when added from C++.
* This means one should carefully design the API and make only necessary methods public or protected.
* Every internal method should be private.
*
* The automatic availability of methods in the class does not hold for the
* Python interface, only for C++ classes. This is common in the rest of
* FreeCAD as well: there is no automatic creation of Python API from C++
* classes.
*
* Hence, the extension creator must also create a custom Python object of its
* extension, which is the same for the normal FreeCAD Python object workflow.
* There is nothing special at all for Python extension objects: the normal
* `.pyi` and `PyImp.cpp` approach is used but note that it is important that
* the object's father is the correct extension base class. Additionally, make
* sure the extension returns the correct Python object in its
* getExtensionPyObject() call.
*
* Every method that is created in the extension's Python counterpart will be
* later added to the extended object. This happens automatically for both the
* C++ and Python extension if getExtensionPyObject() returns the correct
* Python object. This does not require extra work.
*
* A special case that needs to be handled for extensions is the possibility of
* overridden methods. Often, it is desired to customise extension behaviour
* by allowing the user to override methods provided by the extension. On the
* C++ side, this is trivial: such methods are simply marked as "virtual" and
* can then be overridden in any derived class. This is more involved for the
* Python interface and here special care needs to be taken.
*
* As already shown above, one needs to create a special `ExtensionPythonT<>`
* object for extension from Python. This is done exactly for the purpose of
* allowing to have overridable methods. The ExtensionPythonT wrapper adds a
* proxy property that holds a PyObject which itself will contain the
* implementations for the overridden methods. This design is equal to the
* ObjectPythonT<> design of normal document objects. As this wrapper inherits
* the C++ extension class it, can also override the virtual functions the user
* designed to be overridden. What it should do at a call of the virtual
* method is to check if this method is implemented in the proxy object and if
* so, call it, and if not, call the normal C++ version. It is the extension
* creator's responsibility to implement this check and call behaviour for
* every overridable method. This is done by creating a custom wrapper just
* like ExtensionPythonT<> and overriding all virtual methods.
*
* @code
* template<typename ExtensionT> class MyExtensionPythonT : public ExtensionT {
* public:
*
* MyExtensionPythonT() {}
* virtual ~MyExtensionPythonT() {}
*
* virtual bool overridableMethod(DocumentObject* obj) override {
* Py::Object pyobj = Py::asObject(obj->getPyObject());
* EXTENSION_PROXY_ONEARG(allowObject, pyobj);
*
* if(result.isNone())
* ExtensionT::allowObject(obj);
*
* if(result.isBoolean())
* return result.isTrue();
*
* return false;
* };
* };
* @endcode
*
* @note As seen in the code there are multiple helper macros to ease the
* repetitive work of querying and calling methods of the proxy object. See the
* macro documentation for how to use them.
*
* To ensure that your wrapper is used when a extension is created from Python
* the extension type must be exposed as follows:
*
* @code
* using MyExtensionPython = ExtensionPythonT<MyExtensionPythonT<MyExtension>>;
* @endcode
*
* This boilerplate is absolutely necessary to allow overridable methods in
* Python and it is the extension creator's responsibility to ensure full
* implementation.
*
* @section SecExtensionLimitiationsPython Limitations of Python
*
* Without this extension system, it would be challenging to extend
* 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.
*/
/**
* @namespace App
* @ingroup APP
* @brief The namespace for the part of FreeCAD that works without GUI.
* @details This namespace includes %Application services of FreeCAD that such as:
* - The Application class
* - The Document class
* - The DocumentObject classes
* - The Expression classes
* - The Property classes
*
* For a more high-level discussion see the topic @ref APP "App".
*/