Workspace 6.21.5
Writing a Workspace Datatype - looking at the code

In this section, we're going to understand the code that was automatically generated by Workspace so that we know how to customise our datatype.


Using our new datatype (continue on with tutorial Writing a Workspace Datatype )


rectangle.h

As should any C++ header, our file contains a header guard followed by any other header files this one requires. The datafactorytraits.h header defines the DECLARE_WORKSPACE_DATA_FACTORY macro which we will explain shortly.

#ifndef CSIRO_RECTANGLE_H
#define CSIRO_RECTANGLE_H
#include <memory>
#include <QCoreApplication>
#include "simpleplugin.h"

Next comes our implementation class's definition, wrapped in our namespace.

namespace CSIRO
{
class RectangleImpl;
Top level namespace for all Workspace code.
Definition: applicationsupportplugin.cpp:32

Next comes our class's definition. As you would expect, it is exported using our plugin's API symbol.

class SIMPLEPLUGIN_API Rectangle : public CSIRO::DataExecution::ObjectGroup
{
// Allow string translation to work properly
Q_DECLARE_TR_FUNCTIONS(CSIRO::Rectangle)
std::unique_ptr<RectangleImpl> pImpl_;
public:
Rectangle();
Rectangle(const Rectangle& other);
~Rectangle() override;
// Clones our data type
Rectangle* clone() const override;
// Optional, but handy operators
bool operator==(const Rectangle& rhs) const;
Rectangle& operator=(const Rectangle& rhs);
// Get / set functions for modifying data members in code
double getWidth() const;
void setWidth(double width);
double getHeight() const;
void setHeight(double height);
};
Class for relating a group of DataObject instances together.
Definition: objectgroup.h:65
ObjectGroup & operator=(const ObjectGroup &rhs)
Definition: objectgroup.cpp:543
bool operator==(const ObjectGroup &rhs) const
Definition: objectgroup.cpp:576
ObjectGroup * clone() const override=0
Definition: objectgroup.cpp:615

The first thing to note is that the only data member we see here is a pointer to type RectangleImpl. This is an "implementation" class defined in the .cpp file, which contains the real data members owned by our data type. This isn't essential when creating a new Workspace data type (or any class in C++ for that matter), but we do it here in order to to keep our data type's implementation separate from its interface.

The other interesting thing about this class is that it extends CSIRO::DataExecution::ObjectGroup. An ObjectGroup is a special type of class that Workspace is able to easily interact with. Workspace knows the names and data types of all an object group's members, which means it can automatically create operations for Composing (ComposeGroup) and Decomposing (DecomposeGroup) them. See More about object groups for extra information on ObjectGroups and how they work.

The last and most important thing we need to look at is the set of DECLARE macros at the bottom of the file:

DECLARE_WORKSPACE_DATA_FACTORY(CSIRO::Rectangle, SIMPLEPLUGIN_API)
#endif
#define DECLARE_WORKSPACE_DATA_FACTORY(T, WORKSPACE_EXPORT_SYMBOL)
Definition: datafactorytraits.h:759
#define DECLARE_WORKSPACE_DERIVEDTOBASEADAPTOR(D, B, WORKSPACE_EXPORT_SYMBOL)
Definition: derivedtobaseadaptor.h:196

The first line of of code (and one more bit of code in the .cpp file which is explained later in the tutorial) is what takes our class and turns it into a Workspace data type. The DECLARE_WORKSPACE_DATA_FACTORY macro takes two parameters. The first parameter must be the fully scoped class name and the second parameter should be the export symbol used between the class keyword and the class name in the class definition.

Note
It is critical that these macros contain the fully-scoped names of the data type. Workspace uses the scoping information to distinguish between identically named types from different namespaces and plugins.

The second line is another special macro, reserved for when the data type inherits another Workspace data type. It allows Workspace to handle derived-to-base class conversion and other special scenarios. It takes three parameters; the scoped derived class name, the scoped base class name and the export symbol.

Not all Workspace data type need to be ObjectGroups; ObjectGroups just simplify the process by saving the user from creating custom "builder" operations. In fact, you can use any C++ class you define as a custom data type. The magic that turns a class into Workspace data type is the DECLARE_WORKSPACE_DATA_FACTORY and DEFINE_WORKSPACE_DATA_FACTORY macros.



Using our new datatype (continue on with tutorial Writing a Workspace Datatype )


rectangle.cpp

Again, we include the headers needed. The Workspace/DataExecution/DataObjects/typeddatafactory.h header provides the DEFINE_WORKSPACE_DATA_FACTORY macro used at the end of this file to define your custom data type's factory. This macro is the matching pair to the DECLARE_WORKSPACE_DATA_FACTORY macro used in the header. We also need to include the plugin's header (because we need to know about its getInstance() function) and the header for the class itself.

#include "rectangle.h"

Now let's have a look at the implementation class, RectangleImpl.

namespace CSIRO
{
using namespace CSIRO;
using namespace CSIRO::DataExecution;
class RectangleImpl
{
// Allow string translation to work properly
Q_DECLARE_TR_FUNCTIONS(CSIRO::RectangleImpl)
void setObjects();
public:
Rectangle& owner_;
// Data objects
RectangleImpl(Rectangle& owner);
RectangleImpl(Rectangle& owner, const RectangleImpl& other);
};
Template for instantiating a smart pointer to a particular data type.
Definition: typedobject.h:474
Base workspace functionality not requiring a user interface.
Definition: serverwidget.cpp:79
WSGLModelUpdateInfo::Impl & owner
Definition: wsglmodelupdateinfo.cpp:56

This class contains our member variables, as well as a back reference to our datatype itself (owner_). Note that the members are TypedObjects, similar to the data types used previously in operation tutorial. For object groups, any members we wish to expose to Workspace must be TypedObjects. For non-object-group data types, this is not necessary.

Next, let's have a look at our implementation classes functions:

RectangleImpl::RectangleImpl(Rectangle& owner) :
owner_(owner),
width_(),
height_()
{
setObjects();
}
RectangleImpl::RectangleImpl(Rectangle& owner, const RectangleImpl& other) :
owner_(owner),
width_(other.width_),
height_(other.height_)
{
setObjects();
}
void RectangleImpl::setObjects()
{
owner_.add("Width", width_);
owner_.add("Height", height_);
}

We see that it has a simple constructor which initialises our members, a copy constructor which does the same and a function called setObjects(), which is invoked by both the constructor and copy constructor. This function is responsible for assigning our data members Visible Names, which Workspace uses when editing our datatype with the ComposeGroup and DecomposeGroup operations.

Let's keep looking at the functions in the file

Rectangle::Rectangle() :
CSIRO::DataExecution::ObjectGroup(),
pImpl_(std::make_unique<RectangleImpl>(*this))
{
ensureGroupHasData();
}
Rectangle::Rectangle(const Rectangle& other) :
CSIRO::DataExecution::ObjectGroup()
{
pImpl_ = std::make_unique<RectangleImpl>(*this, *other.pImpl_);
ensureGroupHasData();
}
Rectangle::~Rectangle() = default;

Our constructor and destructor serve only to initialise our implementation class. Nothing tricky here.

Rectangle* Rectangle::clone() const
{
return new Rectangle(*this);
}

The clone function is used by Workspace to duplicate our data type when necessary. We simply return a copied instance of our Rectangle.

bool Rectangle::operator==(const Rectangle& rhs) const
{
if (&rhs == this)
{
return true;
}
return ObjectGroup::operator==(rhs);
}
Rectangle& Rectangle::operator=(const Rectangle& rhs)
{
// Check for self assignment
if (&rhs == this)
{
return *this;
}
// Clear the current contents; we're about to delete the impl, so we
// don't want any dangling pointers to our dataobjects.
clear();
pImpl_ = std::make_unique<RectangleImpl>(*this, *rhs.pImpl_);
ensureGroupHasData();
return *this;
}
double Rectangle::getWidth() const
{
return *pImpl_->width_;
}
void Rectangle::setWidth(double width)
{
*pImpl_->width_ = width;
}
double Rectangle::getHeight() const
{
return *pImpl_->height_;
}
void Rectangle::setHeight(double height)
{
*pImpl_->height_ = height;
}
} // namespace CSIRO
DEFINE_WORKSPACE_DATA_FACTORY(CSIRO::Rectangle, CSIRO::SimplePlugin::getInstance())
DEFINE_WORKSPACE_DERIVEDTOBASEADAPTOR(CSIRO::Rectangle, CSIRO::DataExecution::ObjectGroup, CSIRO::SimplePlugin::getInstance())
#define DEFINE_WORKSPACE_DERIVEDTOBASEADAPTOR(D, B, P)
Definition: derivedtobaseadaptor.h:156
const QString width("width")
const QString height("height")
#define DEFINE_WORKSPACE_DATA_FACTORY(T, P)
Definition: typeddatafactory.h:1426

Here we see the assignment and comparison operators, our get / set functions and the very important DEFINE_WORKSPACE_DATA_FACTORY macro. Depending upon what you want to do with your data type, you may want to remove some or all of the get / set functions.

The DEFINE_WORKSPACE_DATA_FACTORY macro should just about always be at the end of the file and it must sit outside any namespaces. Most importantly, it must never appear in a header file. The macro associates your custom class with your plugin and adds a data factory for it to Workspace. It even takes care of handling whether or not your class supports serialization. It should be used as follows:

  • The first parameter should be the name of the class and will be used to identify the class for which a Workspace data type is being defined. Do not try to use any namespace "using" directives to shorten this name, as it must be unique among all data types that Workspace will see from all plugins. This name is also used as the name of the class when shown to the user in the case of any such clashes.
  • The second parameter for the macro is the plugin instance the data type belongs to. This second parameter will just be the getInstance() function of your plugin.

Updating the plugin.cpp and CMakeLists files

The wizard will also automatically add our data factory to the simpleplugin.cpp file, and add the headers and source files to the CMakeLists.txt files in exactly the same way as we did with our operation. You can have a look at those files below:


Using our new datatype (continue on with tutorial Writing a Workspace Datatype )