K-3D's design mandates that plugin data provide sophisticated behaviors, including:
- To support the clean separatation of user interface from implementation, data objects must expose metadata including human-readable labels and descriptions.
- For efficient updates, data objects must be able to notify observers when their value has changed.
- To support the Visualization Pipeline, data objects must expose their data type, value, and connections to other properties for lookup.
- To support scripting and serialization, data objects must expose a canonical "name" for access and unique identification.
To address these and other issues, K-3D uses the k3d::data::container template class, which implements a policy-driven approach to specifying the behavior of a data object (see "Modern C++ Design", Alexandrescu). k3d::data::container defines a family of data objects that are parameterized by specific behavioral policies, which can be extended using custom classes. Using k3d::data::container and the supporting k3d_data() macro, plugin developers can take a clear, type-safe, declarative approach to specifying data object behavior.
The following text assumes the use of
using namespace k3d::data;
To declare an instance of k3d::data::container, use the k3d_data() macro, which takes eight arguments:
- Data Type
- Name Policy
- Signal Policy
- Undo Policy
- Storage Policy
- Constraint Policy
- Property Policy
- Serialization Policy
Following the first argument, which specifies the type of data to be stored, are the seven policies which will be combined to determine the container behavior. The policies are classes; you can use the existing policies which are defined in k3dsdk/data.h, or you can create new policy classes to implement custom behavior as-needed.
To begin, look at the simplest-possible combination of policies:
k3d_data(bool, no_name, no_signal, no_undo, local_storage, no_constraint, no_property, no_serialization)
... this declares a container that can store a "bool" value, but doesn't have a name, doesn't notify observers of state changes, does not support undo or redo of state changes, stores the data by-value in memory, is unconstrained, cannot be connected to other boolean data in the Visualization Pipeline, does not show up in scripts, and does not support automatic serialization of its value. This container is thus roughly equivalent to a "plain" bool.
A more realistic example of a bool container for use in a document plugin would include name, modification, undo, property, and serialization support:
k3d_data(bool, immutable_name, change_signal, with_undo, local_storage, no_constraint, writable_property, with_serialization)
Here is a floating-point value that must be constrained (e.g. to be non-negative):
k3d_data(double, immutable_name, change_signal, with_undo, local_storage, with_constraint, writable_property, with_serialization)
And a container that references a K-3D document node, accessing it as a light (via the k3d::ilight interface):
k3d_data(k3d::ilight*, immutable_name, change_signal, with_undo, node_storage, no_constraint, node_property, node_serialization)
The following could be used to store calculated mesh data that is only created when needed, and cannot be modified from the outside (and thus never needs to be serialized):
k3d_data(k3d::mesh*, immutable_name, change_signal, no_undo, demand_storage, no_constraint, read_only_property, no_serialization)
- no_name - Container does not have a name.
- immutable_name - Container has an immutable name specified at construction.
- no_signal - Container does not signal observers when its value changes.
- change_signal - Container signals observers when its value changes.
- no_undo - Container does not record undo/redo information.
- with_undo - Container records undo/redo information so changes to its value can be undone / redone. Requires the change_signal policy.
- local_storage - Container stores data by-value in local memory.
- node_storage - Container stores an interface pointer to a K-3D document node.
- demand_storage - Container stores a pointer to an object, creating the object on-demand.
- computed_storage - Container caches a value that is computed on-demand.
Note: demand_storage and computed_storage have evolved to the point where they are nearly identical, they will likely be merged / replaced in the future.
- no_constraint - Container values are unconstrained.
- with_constraint - Container values are constrained by abstract objects specified at construction time.
- no_property - Container values are not exposed as a property.
- read_only_property - Container values are exposed as a read-only property. (requires change_signal policy and initialization with init_owner, init_label and init_description)
- writable_property - Container values are exposed as a read-write property.
- string_property - Container values are exposed as a read-write property of type "string" - values are automatically converted to-and-from strings using the insertion and extraction operators.
- path_property - Container values are exposed as a read-write filesystem path property - the property will expose metadata describing the purpose of the path (reading or writing) and its category (for use by the UI layer for retaining most-recently-used data).
- script_property - Container values are exposed as a read-write scripting-language source code property.
- enumeration_property - Container values are exposed as a read-write string enumeration property, whose values are limited to a specific set.
- list_property - Container values are exposed as a read-write string list property, which can take on any value, but with a predefined set of values available in the UI.
- node_property - Container values are exposed as a read-write node property (requires the node_storage policy).
- measurement_property - Container values are exposed as a read-write measurement property, which exposes metadata describing the real-world units-of-measure for the property (e.g. angle, distance, mass) and other formatting information.
- no_serialization - Container values are not automatically serialized.
- with_serialization - Container values are automatically serialized to XML (requires a non-pointer value type).
- node_serialization - Container values are automatically serialized to XML (requires a k3d::iunknown-derived value type, and node_storage policy).
k3d::data::container and its policies are designed to require full initialization at construction-time. Initialization is handled using initializer objects that can be concatenated using the addition operator. As an example, the simplest-possible container must still have its value initialized, using the init_value() initializer:
k3d_data(bool, no_name, no_signal, no_undo, local_storage, no_constraint, no_property, no_serialization) my_bool(init_value(true));
As the set of policies changes, so does the set of initializers required to compile:
k3d_data(bool, immutable_name, no_signal, no_undo, local_storage, no_constraint, no_property, no_serialization) my_bool(init_value(true) + init_name("mybool"));
A container with constraints must specify them using initializers:
k3d_data(double, no_name, no_signal, no_undo, local_storage, with_constraint, no_property, no_serialization) my_double(init_value(5.0) + init_constraint(constraint::minimum(0.0)));
Constraints can be chained-together, e.g. to constrain a value to a range:
k3d_data(double, no_name, no_signal, no_undo, local_storage, with_constraint, no_property, no_serialization) my_double(init_value(5.0) + init_constraint(constraint::minimum(0.0, constraint::maximum(10.0))));
Note that when using the minimum contraint, a signed number type must be used to avoid overflow problems when going below zero.
A container's value normally accessed using its internal_value() member function :
bool is_on = my_bool.internal_value(); double value = my_double.internal_value();
Note that internal_value() returns a copy of the container contents. While this doesn't matter when working with simple data types, be careful with more complex types such as containers. The internal_value() function returns a temporary object that must be copied before use :
typedef std::vector<bool> array_t; k3d_data(array_t, ...) my_array(...); array_t array_value = my_array.internal_value();
For containers that are also properties, you normally use the pipeline_value() method to retrieve the container value. This ensures that you retrieve the correct value if the property has been connected to another via the Visualization Pipeline.
// Create a property ... k3d_data(k3d::int32_t, ...) my_property(...); // Set the property's internal value to '5' ... my_property.set_value(5); // This might-or-might-not return '5', depending // on whether this property is connected to another. // If it is, the other property's value will be returned. k3d::int32_t v = my_property.pipeline_value();