Property Framework

Met.3D provides user-editable properties that expose settings for various parts of the application, including actor and scene-specific settings. These properties are made possible by a property framework developed to meet the needs of Met.3D. This section of the developer documentation explains the framework’s implementation and usage.

Concept

The core concept of the property framework is that properties can be organized in a tree structure. Each property represents a value, allowing it to be used directly in place of the value it holds. Additionally, a property can save itself to a QSettings instance and load that value. If the root of the property tree is saved, all sub-properties will also be saved. An important aspect is that the property and its UI representation are completely separate, meaning properties can function without a UI and without dependencies on it. If a UI is necessary, each property constructs its own UI representation, leveraging the Qt signal-slot framework.

Implementation

The MProperty Class

The MProperty class forms the base of the property framework, providing the essential interface that all properties should implement and functioning as an empty property for grouping. It offers several constructors:

  • MProperty(QString name) — The default constructor that creates a property with a specified name and save key. The save key is used to save the property to a configuration file for later retrieval.

  • MProperty(const MProperty &other) — Copy constructor that deep-copies the property, excluding its tree relations. This means that the copy loses its parent and sub-properties.

  • MProperty(MProperty::Data *data) — Protected constructor used in subclasses to instantiate a property with a given data pointer.

The class utilizes the Qt signal and slot mechanism to notify other classes and the frontend about property changes. The most important signal for development is the valueChanged signal, emitted when the property’s value changes. The registerValueCallback method simplifies connecting to this signal. Other signals primarily enable frontends to react to changes in property settings.

Properties are always their own values, necessitating a copy constructor and an assignment operator override. As the base class for all other properties, it does not hold a value and is therefore empty, only serving to group other properties in the property tree.

The property-specific settings are contained within a data pointer, minimizing the direct memory footprint of this class. For the frontend, a property can create an editor widget via createEditorWidget().

The Data Pointer

The nested MProperty::Data struct holds all data associated with various settings for the property. This separate struct holds infrequently accessed settings that occupy significantly more space than the property value itself. An instance of this struct is created by the property and saved as a pointer. Subclasses of MProperty that require additional settings need to also subclass MProperty::Data and utilize the specific constructor of MProperty to provide it.

It comprises the following settings:

  • parent — A pointer to the parent property, if one exists.

  • subProperties — A list of sub-properties associated with this property.

  • name — The property’s name for user identification.

  • tooltip — An optional tooltip for frontend implementations to explain the property to the user.

  • enabled — Indicates whether the property is enabled or disabled. A disabled property should not be editable by the user in the frontend, which also implicitly disables all sub-properties.

  • isHidden — Indicates whether the property is hidden from the user in the frontend.

  • configKey — A unique key in the property tree identifying the property in configuration and save files. If the key is empty, the property will not be saved to the QSettings instance.

  • configGroup — An optional group surrounding the properties’ config key in the config or save file, also including sub-properties. This can help avoid duplication in property sub-trees.

  • editors — A list of editor widgets displayed in the frontend, allowing user edits to the property.

  • suppressValueChangedSignal — If set to true, suppresses the value changed signal, preventing emission of said signal.

  • bgColor — An optional background color for the property, if supported by frontend implementations.

  • contextMenu — A list of actions available when the user right-clicks the property in supported frontend implementations.

The property editor

The MProperty::Editor class provides a widget for editing properties from the UI frontend. It is typically subclassed for different property types that provide various widgets, such as spinboxes or comboboxes. Generally, the editor is a horizontally laid-out widget that can expand or maintain its preferred horizontal size. Two essential methods to be implemented in subclasses are:

  • updateSettings — Called when property settings change, potentially altering editor settings.

  • updateValue — Called when the property value changes in the backend, necessitating an update in the editor.

Context Menu

Each property can contain several QAction items that provide a right-click context menu. However, frontend implementations for the property must support this functionality.

The MValueProperty Template Class

This template subclass of MProperty serves as the base for properties with specific values, accommodating types like booleans, floats, or pointers. The value property has a new field, the propertyValue, which is not part of the data pointer. This allows the property value to be local to the property, allowing for better optimizations. Additionally, a default value can be specified, used when loading the property from a config file or to initialize the property value.

Since the value property should also represent its own value, it supports implicit conversion to and from its value type. The assignment operator override, MValueProperty<T> &operator=(const T &other), enables direct value assignments, e.g., aBoolProperty = true.

In addition to the assignment operator, this class provides two setter methods:

  • setValue — Assigns the property value (like the assignment operator) and updates all frontend editors.

  • setUndoableValue — To be used when modifying the value from the UI frontend, making the change undoable and pushing the action onto Met.3D’s global undo stack.

All properties containing values, such as bool, float, or QString, should inherit from this class along with its data pointer.

Frontends

Properties themselves consist of data and require a UI for user editing. One approach is to directly use the property’s editor widget, which may not represent the property tree accurately or work with properties requiring sub-properties. Developers can implement custom frontend widgets that utilize these editors to display a property tree. One such implementation is the MPropertyTree widget, which displays properties in a two-column tree format, with property labels on the left and editors on the right. It is implemented as a tree of MPropertyTree::Item, where each item represents a single property and its sub-properties. For every property in the tree, there is a corresponding item. The top-level items, or root items, are organized into a special layout that manages alignment. Properties can be expanded or collapsed, showing or hiding their sub-properties while respecting background colors. The widget also features a search bar to filter properties. This represents one possible implementation, with opportunities for other custom widgets in the future.

Currently Implemented Property Types

This section lists all the currently implemented types of properties.

  • MArrayProperty — A property that accepts other properties and lays them out horizontally, useful for creating button or min/max groups.

  • MBoolProperty — A boolean represented by a checkbox for frontend editing.

  • MButtonProperty — A valueless property providing a button in the frontend for executing the registered value callback.

  • MColorProperty — A QColor value with a color picker and preview in the frontend.

  • MEnumProperty — Technically an int, but presents a list of strings for user selection and the selected index of in that list.

  • MNumberProperty — An abstract numeric type, providing the base for all number-related properties.

  • MFloatProperty — A float represented by a spinbox with various frontend settings.

  • MIntProperty — An integer represented by a spinbox with various frontend settings.

  • MDoubleProperty — A double represented by a spinbox with various frontend settings.

  • MSciFloatProperty — A float represented by a spinbox in scientific notation.

  • MSciDoubleProperty — A double represented by a spinbox in scientific notation.

  • MNWPActorVarProperty — Holds a pointer to an actor variable, allowing user selection from a combobox list of actor variables.

  • MPointFProperty — A QPointF value with two spinboxes for editing x and y coordinates.

  • MRectProperty — A QRectF value with predefined sub-properties for editing x and y coordinates along with width and height.

  • MStringProperty — A QString value with a line edit for string editing.

  • MTransferFunctionProperty — A pointer to an MTransferFunction, providing a combobox with available transfer functions from the scene.

  • MVector3DProperty — A QVector3D value with three sub-properties for editing x, y, and z coordinates.

Usage

This section addresses the property framework’s usage, which should be straightforward.

Creation

The following code example illustrates the usage of a float property as a class member.

Listing 3 Class header (.h)
 1 class MyClass
 2 {
 3     MyClass();
 4     ...
 5     // The root of the property tree.
 6     MProperty treeRoot;
 7     // A float property as a member variable.
 8     MFloatProperty aFloatMember;
 9     ...
10 }

In the class’s source file, the property needs to be initialized in the constructor. This can occur via two methods: initializing in the initializer list or creating it in the constructor’s body. Additionally, the property can be added to a property tree as a sub-property.

Listing 4 Class source (.cpp)
 1 MyClass::MyClass()
 2 : treeRoot("Name"),
 3   aFloatMember("Name", 0)
 4 {
 5     ...
 6     // Alternative initialization of the property.
 7     aFloatMember = MFloatProperty("Name", 0);
 8     // Add the float member into the tree as a sub property of the root.
 9     treeRoot.addSubProperty(aFloatMember);
10     ...
11 }

Value changes

To respond to value changes, register callback methods to a property.

Listing 5 Registering callbacks
1 ...
2 // Registering a callback function defined as a Qt slot in the header.
3 aFloatMember.registerValueCallback(this, &MyClass::aCallbackSlot);
4 // OR alternatively register a lambda as a callback.
5 aFloatMember.registerValueCallback([=]()
6 {
7     // Callback body.
8 });
9 ...

Using a lambda is recommended, as it avoids the necessity of defining a slot method in the class header for each property. Additionally, since the lambda captures the this pointer, it enables access to the property and other class members.

Settings

Some properties may expose multiple settings to restrict acceptable property values or customize the property’s appearance. For instance, a float property can have minimum and maximum values, decimal places, and an increment step for user input.

Listing 6 Setting settings of the property
1 ...
2 // Set min max values.
3 aFloatMember.setMinMax(-100.0f, 100.0f);
4 // Set decimal places.
5 aFloatMember.setDecimals(3);
6 // Set increment step to 1.
7 aFloatMember.setStep(1);
8 ...

Creating an Editor

If properties need to be used independently from an MPropertyTree widget, you can create custom editor widgets. This functionality is rarely required, as properties are typically used within a tree widget.

Listing 7 Creating an editor for the property
1 ...
2 QWidget *editor = aFloatProperty.createEditorWidget(parentWidget);
3 ...

Saving Property Values to File.

Properties facilitate automatic saving and loading from a QSettings configuration file. To save the entire tree, you only need to save the root node, as all sub-properties are saved implicitly.

Listing 8 Saving and loading a property tree.
1 ...
2 QSettings *configFile = ...;
3 ...
4 // Save property tree to config file.
5 treeRoot.saveAllToConfiguration(configFile);
6 ...
7 // Load property tree from config file.
8 treeRoot.loadAllFromConfiguration(configFile);
9 ...