In this tutorial we will see how to add a custom visualization for a node.
We will create a visualization for if statement nodes. We showed how to create this node type in Adding a new node type to Envision's model . You should be familiar with that tutorial before reading on. In the present tutorial we will use OOVisualization::VIfStatement as example.
Introduction
Visual objects in Envision are called items. The name comes from the super type of all visualizations which is Visualization::Item (which in turn inherits from Qt's QGraphicsItem).
To draw something on the scene (Envision's visual canvas), an item can do any combination of the following:
- Draw directly on the scene, by using Qt's QPainter drawing methods.
- Contain child items which themselves may draw something.
Envision already provides many useful items that directly draw on the scene. Typically these are used as leaves in a more complicated rendering tree. Most newly created visualizations therefore are only concerned with defining what child items they contain and how they should be arranged on the screen. Our example, OOVisualization::VIfStatement, also does not draw anything itself. Instead it creates a number of child items and arranges them using a layout.
The easiest way to arrange child items is to use the services of Visualization::DeclarativeItem. Items inheriting from it have at their disposal a powerful and fast layout manager inspired by GUI frameworks. For detailed information on this declarative framework please see Andrea Helfenstein's Bachelor Thesis Report. Below we will only cover parts of it that are relevant for our example.
VIfStatement.h
Here is VIfStatement.h
:
#pragma once
#include "../oovisualization_api.h"
#include "VIfStatementStyle.h"
#include "VStatementItem.h"
#include "OOModel/src/statements/IfStatement.h"
#include "VisualizationBase/src/items/ItemWithNode.h"
#include "VisualizationBase/src/declarative/DeclarativeItem.h"
class SequentialLayout;
class Static;
class NodeWrapper;
class Line;
}
class VStatementItemList;
class OOVISUALIZATION_API VIfStatement
:
public Super<VStatementItem<VIfStatement, Visualization::DeclarativeItem<VIfStatement>,
OOModel::IfStatement>>
{
ITEM_COMMON(VIfStatement)
public:
VIfStatement(Item* parent, NodeType* node, const StyleType* style = itemStyles().get());
VStatementItemList* thenBranch() const;
VStatementItemList* elseBranch() const;
static void initializeForms();
virtual int determineForm() override;
private:
VStatementItemList* thenBranch_{};
VStatementItemList* elseBranch_{};
bool isInsideAnotherIf() const;
};
}
Visualization::NodeWrapper * condition() const
Definition: VIfStatement.h:78
VStatementItemList * elseBranch_
Definition: VIfStatement.h:70
VStatementItemList * thenBranch_
Definition: VIfStatement.h:69
Visualization::Static * icon_
Definition: VIfStatement.h:71
Visualization::Static * elseIcon_
Definition: VIfStatement.h:72
VStatementItemList * elseBranch() const
Definition: VIfStatement.h:80
VStatementItemList * thenBranch() const
Definition: VIfStatement.h:79
Visualization::Static * elseIcon() const
Definition: VIfStatement.h:82
Visualization::NodeWrapper * condition_
Definition: VIfStatement.h:68
Visualization::Static * icon() const
Definition: VIfStatement.h:81
This class can be used to display nodes with their default style, but wrapped in an item,...
Definition: NodeWrapper.h:46
Definition: ContractFilter.h:31
The VisualizationBase plug-in sets the foundations of the visualization framework in Envision.
Definition: CppExportPlugin.h:34
Let's examine it starting from the top. The first interesting bit after the includes is:
class OOVISUALIZATION_API VIfStatement
:
public Super<VStatementItem<VIfStatement, Visualization::DeclarativeItem<VIfStatement>,
OOModel::IfStatement>>
{
When creating visualizations a lot of functionality is configured by inheriting from appropriate template classes.
Like for nodes, we use the Super
template to specify the base class.
In this case the 'real' base class is OOVisualization::VStatementItem. OOVisualization::VStatementItem is really just a very thin wrapper on top of the Visualization::ItemWithNode. The only reason we use OOVisualization::VStatementItem is so that we can later set a default handler for all visualizations of statements. This is not important for this tutorial, so we'll just use the fact that we inherit from Visualization::ItemWithNode. What is this template?
An item either stands on its own or represents the contents of a node. Examples of the former are labels, decorative lines, and icons, while examples of the latter are visualizations of classes, methods, expressions, etc. Items, which do not represent a node, inherit directly from Visualization::Item, whereas items that visualize a particular node type inherit from Visualization::ItemWithNode (which ultimately also inherits from Visualization::Item). This class provides the necessary association between the visualization and an instance of a node that our visualization represents. We can gain access to this instance by using the node()
method within our visualization. The Visualization::ItemWithNode template requires 3 parameters:
The first and last bit are straight forward, but let's look at the second argument. In essence Visualization::ItemWithNode will inherit from whatever class we provide here. This needs to be a sub type of Visualization::Item. By specifying Visualization::DeclarativeItem we make it one of the super types of OOVisualization::VIfStatement and can make use of its services which greatly simplify the layouting of child items.
Here is the complete type hierarchy of the OOVisualization::VIfStatement visualization:
OOVisualization::VIfStatement <– Super <– OOVisualization::VStatementItem <– Visualization::ItemWithNode <– Visualization::DeclarativeItem <– Visualization::DeclarativeItemBase <– Visualization::Item <– QGraphicsItem
A note on the name of the visualization class: when you create a new 'stand-alone' item, you are free to call it anything. By convention, items that visualize nodes from the tree should have the same name as the node type, with a 'V' prefix (for visualization). In our example we are visualizing nodes of type OOModel::IfStatement so the corresponding item class is called OOVisualization::VIfStatement.
Next comes this line:
ITEM_COMMON(VIfStatement)
All items must include this macro as the first line in their declaration. This is similar to what we saw when creating new nodes - we use this macro to save us writing a ton of boilerplate code manually (unfortunately, it's not possible to replace the macro with a template solution). Among other things, this macro defines the style for this item to be a class called just like the item class with an added "Style" suffix. In this case the style class for OOVisualization::VIfStatement is OOVisualization::VIfStatementStyle. Sometimes it is useful to manually specify what the style class for an item should be called (e.g. when you want to use the same style for multiple items). In that case you can use the macro ITEM_COMMON_CUSTOM_STYLENAME
instead which also takes an additional style class name as an argument.
For more info about styles see Item styles at the end of this tutorial.
Next we have the constructor:
VIfStatement(Item* parent, NodeType* node, const StyleType* style = itemStyles().get());
All items representing a node (inheriting from Visualization::ItemWithNode) must have a constructor of this form. The style argument should always be initialized by default to itemStyles()
.get() - equivalent to itemStyles()
.get("default") - which returns the default style. itemStyles()
is a static method introduced by the ITEM_COMMON
macro. You can use itemStyles()
.get(styleName) to get the style defined by the file called styleName in the item's style directory.
Items that do not visualize nodes, must declare a similar constructor but omit the node
parameter.
After the constructor we have just a few getter methods for the correspondingly named fields:
VStatementItemList* thenBranch() const;
VStatementItemList* elseBranch() const;
Nothing special, let's go on to the first important method definition:
static void initializeForms();
This is a static method required by Visualization::DeclarativeItem. This method defines the layout for all children of the class. We will see how this is done when discussing the
.cpp file.
Next comes another related method:
virtual int determineForm() override;
This is another method used by Visualization::DeclarativeItem. A declarative item may declare multiple forms (layouts). If this is the case, then the determineForm()
method should be overridden in order to tell the declarative framework which form to use. Many items only declare a single layout, in which case there is no need to redefine this method.
Some visualizations have an additional important method here and OOVisualization::VIfStatement used to have it:
virtual void updateGeometry(int availableWidth, int availableHeight) override;
To understand this method we will first explain Envision's visualization mechanism in more detail.
To improve performance the visualization work flow is split into two major parts:
- Painting on the scene
- Updating the visualizations
Painting is mostly controlled by Qt. Various events trigger a repaint of the whole scene or parts of it. These are for example scrolling the view, zooming in and out, interacting with the view, etc. Parts of the scene are repainted often and Qt decides which items should be repainted. When an item needs to be repainted its paint()
method is called directly by Qt. Leaf items should reimplement this method in order to draw something. More often however, items do not paint anything - they just include child items which do the drawing. In that case there is no need to redefine the method.
The default implementation of paint()
provided by Visualization::Item takes care of drawing a background shape if any has been specified in the style.
Updating is part of the overall visualization mechanism defined by Envision. In order to make sure we do not always recompute the entire scene, we only update items that need to be updated. For instance, if you just rename a class, only that class and all its (visual) parents need to be updated - you don't need to recompute the layout for an unrelated class or package. Envision automatically detects some situations that require an update to an item, and you can manually request that a particular item be updated. Whenever an item is marked for updating, all of its parent items are also marked for updating and the update begins at the root item. The update procedure is recursive and is embodied by the Visualization::Item::updateSubtree() method. This is what happens when an item is being updated:
- The update proceeds only if the item has been marked for update, otherwise nothing happens.
- All child items are determined. Visualization::Item declares a pure virtual method
determineChildren()
whose sole purpose is to make sure that any child items have been created and set as children. Derived items should reimplement this method and maintain any child items in it. The method can also be empty if there are no child items.
- All child items that need updating are updated. This is a recursive call to
updateSubtree()
.
- The item's geometry is calculated. After all of the item's children have been fully updated and their sizes are known, the item must calculate its own geometry. Visualization::Item declares another pure virtual method called
updateGeometry()
. Derived classes must reimplement it and do at least the following two:
- Set the position of all child elements. This is the layouting step, where we determine how our child elements should be positioned on screen. Keep in mind that all children should be drawn at non-negative
x
and y
positions.
- Set the item's own size by calling
setSize()
. It is important that the size we set completely engulfs all child elements. Child items should never 'stick' out of their parent element.
Some items have a variable size depending on where they are put. For example, horizontal lines expand in order to fill all the available space. To achieve this behavior an items must reimplement sizeDependsOnParent()
to return true
. This causes updateGeometry()
to be called twice for this item:
- When it is first updated, the method is called with arguments 0, 0. These arguments indicate the
availableWidth
and availableHeight
. Whenever they are both zero, this means that the item can take as much space as it needs and the parent item will make sure to accommodate it. All items always receive such a call to updateGeometry()
.
- After the parent has positioned all its children and set its size to engulf all child items, it should call the
updateGeometry()
method again for any children that indicate their size depends on the parent's size. This time at least one argument of updateGeometry()
will be positive, indicating how much space the item can take in total in that direction, without changing the layout of the parent. The item can stretch to fill all the available space. If one of the arguments to updateGeometry()
is 0, this means that the item should retain its current size in that direction. Any positive arguments provided in this second call to updateGeometry()
can only indicate that at least as much space is available as the item originally requested, that is an item will never have to shrink in this second call, only expand.
The way this works for the line item is that it sets up a default line that is let's say 20 pixels long on the first call to updateGeometry()
. Then during the second call the line will expand to take any extra space (say 270 pixels). Notice that there is no drawing during these calls, we merely setup the parameters that will be used for drawing later.
Now, why did OOVisualization::VIfStatement have an updateGeometry()
method and why isn't it needed any more. The simple answer is that the visualization of if statements used to be a little more complex, but isn't anymore. Previously an if statement would display its then an else branches either vertically (else under then) or horizontally (else to the right of then). This was a nice little trick but we found that people get confused and so removed this behavior. To able to achieve this it was necessary to reimplement the updateGeometry()
method. A frequent reason for reimplementing this method is to get more information. This was also the case for OOVisualization::VIfStatement. We know that the updateGeometry()
method will be called after all children have been drawn. The reimplementation here just needed to know the size of these child items, that's all. It doesn't actually do anything and calls the overriden implementation to do all the heavy lifting. Based on the size of the children we had to make a choice of a horizontal or vertical arrangement and trigger a RepeatUpdate
if the arrangement changed.
Afterwards we have the fileds that will hold subitems:
VStatementItemList* thenBranch_{};
VStatementItemList* elseBranch_{};
There are some fields that will hold child items and another one called horizontal
that will control which form will be used to arrange the children.
When choosing which form to render we need to know if this if is inside another if:
bool isInsideAnotherIf() const;
Let's see how it all works in the .cpp file.
VIfStatement.cpp
Here is VIfStatement.cpp
:
#include "VIfStatement.h"
#include "../elements/VStatementItemList.h"
#include "VisualizationBase/src/items/Static.h"
#include "VisualizationBase/src/items/Line.h"
#include "VisualizationBase/src/declarative/DeclarativeItem.hpp"
#include "VisualizationBase/src/items/NodeWrapper.h"
#include "VisualizationBase/src/declarative/BorderFormElement.h"
DEFINE_ITEM_COMMON(VIfStatement, "item")
VIfStatement::VIfStatement(Item* parent, NodeType* node, const StyleType* style) :
Super{parent, node, style}
{}
int VIfStatement::determineForm()
{
int formId = 0;
if (node()->elseBranch() && node()->elseBranch()->size() > 0)
{
if (node()->elseBranch()->size() == 1 && DCast<IfStatement>(node()->elseBranch()->first()))
formId = 2;
else
formId = 1;
}
if (isInsideAnotherIf())
formId += 3;
return formId;
}
void VIfStatement::initializeForms()
{
auto icon = item<Static>(&I::icon_, [](I* v){
if (v->node()->parent() && v->node()->parent()->parent() )
{
if (auto ifs = DCast<IfStatement>(v->node()->parent()->parent()))
if ( ifs->elseBranch() == v->node()->parent())
return &v->style()->elificon();
}
return &v->style()->icon();
});
auto condition = item<NodeWrapper>(&I::condition_, [](I* v){return v->node()->condition();},
[](I* v){return &v->style()->condition();});
auto header = grid({{(new AnchorLayoutFormElement{})
->put(TheVCenterOf, icon, AtVCenterOf, condition)
->put(TheHCenterOf, icon, AtLeftOf, condition)}})
->setColumnStretchFactor(1, 1);
auto elseIcon = grid({{item<Static>(&I::elseIcon_, &StyleType::elseIcon)}})
->setColumnStretchFactor(1, 1)
->setNoBoundaryCursors([](Item*){return true;})->setNoInnerCursors([](Item*){return true;});
auto thenBranch = grid({{item<VStatementItemList>(&I::thenBranch_, [](I* v){return v->node()->thenBranch();},
[](I* v){return &v->style()->thenBranch();})}})
->setColumnStretchFactor(1, 1)->setRowStretchFactor(0, 1)->setHorizontalSpacing(5)
->setNoBoundaryCursors([](Item*){return true;})->setNoInnerCursors([](Item*){return true;});
auto elseBranch = grid({{item<VStatementItemList>(&I::elseBranch_, [](I* v){return v->node()->elseBranch();},
[](I* v){return &v->style()->elseBranch();})}})
->setColumnStretchFactor(1, 1)->setRowStretchFactor(0, 1)->setHorizontalSpacing(5)
->setNoBoundaryCursors([](Item*){return true;})->setNoInnerCursors([](Item*){return true;});
auto shapeElement = new ShapeFormElement{};
auto topLevelIfWithoutElse = (new AnchorLayoutFormElement{})
->put(TheTopOf, thenBranch, 3, FromBottomOf, header)
->put(TheTopOf, shapeElement, AtCenterOf, header)
->put(TheLeftOf, shapeElement, -10, FromLeftOf, header)
->put(TheLeftOf, shapeElement, 5, FromLeftOf, thenBranch)
->put(TheRightOf, header, AtRightOf, thenBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, header)
->put(TheBottomOf, shapeElement, 3, FromBottomOf, thenBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, thenBranch);
addForm(topLevelIfWithoutElse);
auto elseHorizontalLineElement = item<Line>(&I::elseLine_, [](I* v){return &v->style()->elseHorizontalLine();});
auto topLevelIfAndStandardElse = (new AnchorLayoutFormElement{})
->put(TheTopOf, thenBranch, 3, FromBottomOf, header)
->put(TheLeftOf, elseIcon, AtLeftOf, header)
->put(TheLeftOf, elseBranch, AtLeftOf, thenBranch)
->put(TheTopOf, shapeElement, AtCenterOf, header)
->put(TheLeftOf, shapeElement, -10, FromLeftOf, header)
->put(TheLeftOf, shapeElement, 5, FromLeftOf, thenBranch)
->put(TheTopOf, elseIcon, 3, FromBottomOf, thenBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, thenBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, elseBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, header)
->put(TheTopOf, elseBranch, 3, FromBottomOf, elseIcon)
->put(TheBottomOf, shapeElement, 3, FromBottomOf, elseBranch)
->put(TheVCenterOf, elseHorizontalLineElement, AtVCenterOf, elseIcon)
->put(TheLeftOf, elseHorizontalLineElement, AtHCenterOf, elseIcon)
->put(TheRightOf, elseHorizontalLineElement, AtRightOf, shapeElement);
addForm(topLevelIfAndStandardElse);
auto elseIfBranch = item<VStatementItemList>(&I::elseBranch_, [](I* v){return v->node()->elseBranch();},
[](I* v){return &v->style()->elseIfBranch();});
auto topLevelIfAndElseIf = (new AnchorLayoutFormElement{})
->put(TheTopOf, thenBranch, 3, FromBottomOf, header)
->put(TheTopOf, shapeElement, AtCenterOf, header)
->put(TheLeftOf, shapeElement, -10, FromLeftOf, header)
->put(TheLeftOf, shapeElement, 5, FromLeftOf, thenBranch)
->put(TheRightOf, header, AtRightOf, thenBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, header)
->put(TheBottomOf, shapeElement, AtBottomOf, elseIfBranch)
->put(TheTopOf, elseIfBranch, 2, FromBottomOf, thenBranch)
->put(TheLeftOf, elseIfBranch, AtLeftOf, header)
->put(TheRightOf, elseIfBranch, AtRightOf, shapeElement);
addForm(topLevelIfAndElseIf);
auto borderElement = new BorderFormElement{};
addForm(topLevelIfWithoutElse->clone()
->put(TheRightOf, shapeElement, AtRightOf, borderElement)
->put(TheLeftOf, header, AtLeftOf, borderElement));
addForm(topLevelIfAndStandardElse->clone()
->put(TheRightOf, shapeElement, AtRightOf, borderElement)
->put(TheLeftOf, header, AtLeftOf, borderElement));
addForm(topLevelIfAndElseIf->clone()
->put(TheRightOf, shapeElement, AtRightOf, borderElement)
->put(TheLeftOf, header, AtLeftOf, borderElement));
topLevelIfWithoutElse->setBottomMargin(10);
topLevelIfAndStandardElse->setBottomMargin(10);
topLevelIfAndElseIf->setBottomMargin(10);
}
bool VIfStatement::isInsideAnotherIf() const
{
return parent() && parent()->parent() && parent()->parent()->typeId() == typeIdStatic();
}
}
Definition: ContractFilter.h:35
The first thing to notice is this inlcude:
#include "VisualizationBase/src/declarative/DeclarativeItem.hpp"
We need to include this additional definitions header pertaining to the Visualization::DeclarativeItem framework.
Next is:
DEFINE_ITEM_COMMON(VIfStatement, "item")
which complements the ITEM_COMMON
declaration in the header file. The second argument indicates which subdirectory of /styles
we should look in for the /VIfStatement
directory that contains all styles for this class. Possible choices here are "item", "icon" and "layout" for now.
The constructor:
VIfStatement::VIfStatement(Item* parent, NodeType* node,
const StyleType* style) :
Super{parent, node, style}
is typical for items deriving from Visualization::DeclarativeItem, we just forward the arguments.
In the determineForm()
method we choose which form we use to render the if statement. There are several choices there depending on whether the if is inside another if or not. These are mainly there to make the rendering of longer if
else
if
else
if
else
chains nicer.
int VIfStatement::determineForm()
{
int formId = 0;
if (node()->elseBranch() && node()->elseBranch()->size() > 0)
{
if (node()->elseBranch()->size() == 1 && DCast<IfStatement>(node()->elseBranch()->first()))
formId = 2;
else
formId = 1;
}
if (isInsideAnotherIf())
formId += 3;
return formId;
}
Finally let's see the real work that we need to do in order to use Visualization::DeclarativeItem - defining the initializeForms()
method:
void VIfStatement::initializeForms()
{
auto icon = item<Static>(&I::icon_, [](I* v){
if (v->node()->parent() && v->node()->parent()->parent() )
{
if (auto ifs = DCast<IfStatement>(v->node()->parent()->parent()))
if ( ifs->elseBranch() == v->node()->parent())
return &v->style()->elificon();
}
return &v->style()->icon();
});
auto condition = item<NodeWrapper>(&I::condition_, [](I* v){return v->node()->condition();},
[](I* v){return &v->style()->condition();});
auto header = grid({{(new AnchorLayoutFormElement{})
->put(TheVCenterOf, icon, AtVCenterOf, condition)
->put(TheHCenterOf, icon, AtLeftOf, condition)}})
->setColumnStretchFactor(1, 1);
auto elseIcon = grid({{item<Static>(&I::elseIcon_, &StyleType::elseIcon)}})
->setColumnStretchFactor(1, 1)
->setNoBoundaryCursors([](Item*){return true;})->setNoInnerCursors([](Item*){return true;});
auto thenBranch = grid({{item<VStatementItemList>(&I::thenBranch_, [](I* v){return v->node()->thenBranch();},
[](I* v){return &v->style()->thenBranch();})}})
->setColumnStretchFactor(1, 1)->setRowStretchFactor(0, 1)->setHorizontalSpacing(5)
->setNoBoundaryCursors([](Item*){return true;})->setNoInnerCursors([](Item*){return true;});
auto elseBranch = grid({{item<VStatementItemList>(&I::elseBranch_, [](I* v){return v->node()->elseBranch();},
[](I* v){return &v->style()->elseBranch();})}})
->setColumnStretchFactor(1, 1)->setRowStretchFactor(0, 1)->setHorizontalSpacing(5)
->setNoBoundaryCursors([](Item*){return true;})->setNoInnerCursors([](Item*){return true;});
auto shapeElement = new ShapeFormElement{};
auto topLevelIfWithoutElse = (new AnchorLayoutFormElement{})
->put(TheTopOf, thenBranch, 3, FromBottomOf, header)
->put(TheTopOf, shapeElement, AtCenterOf, header)
->put(TheLeftOf, shapeElement, -10, FromLeftOf, header)
->put(TheLeftOf, shapeElement, 5, FromLeftOf, thenBranch)
->put(TheRightOf, header, AtRightOf, thenBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, header)
->put(TheBottomOf, shapeElement, 3, FromBottomOf, thenBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, thenBranch);
addForm(topLevelIfWithoutElse);
auto elseHorizontalLineElement = item<Line>(&I::elseLine_, [](I* v){return &v->style()->elseHorizontalLine();});
auto topLevelIfAndStandardElse = (new AnchorLayoutFormElement{})
->put(TheTopOf, thenBranch, 3, FromBottomOf, header)
->put(TheLeftOf, elseIcon, AtLeftOf, header)
->put(TheLeftOf, elseBranch, AtLeftOf, thenBranch)
->put(TheTopOf, shapeElement, AtCenterOf, header)
->put(TheLeftOf, shapeElement, -10, FromLeftOf, header)
->put(TheLeftOf, shapeElement, 5, FromLeftOf, thenBranch)
->put(TheTopOf, elseIcon, 3, FromBottomOf, thenBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, thenBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, elseBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, header)
->put(TheTopOf, elseBranch, 3, FromBottomOf, elseIcon)
->put(TheBottomOf, shapeElement, 3, FromBottomOf, elseBranch)
->put(TheVCenterOf, elseHorizontalLineElement, AtVCenterOf, elseIcon)
->put(TheLeftOf, elseHorizontalLineElement, AtHCenterOf, elseIcon)
->put(TheRightOf, elseHorizontalLineElement, AtRightOf, shapeElement);
addForm(topLevelIfAndStandardElse);
auto elseIfBranch = item<VStatementItemList>(&I::elseBranch_, [](I* v){return v->node()->elseBranch();},
[](I* v){return &v->style()->elseIfBranch();});
auto topLevelIfAndElseIf = (new AnchorLayoutFormElement{})
->put(TheTopOf, thenBranch, 3, FromBottomOf, header)
->put(TheTopOf, shapeElement, AtCenterOf, header)
->put(TheLeftOf, shapeElement, -10, FromLeftOf, header)
->put(TheLeftOf, shapeElement, 5, FromLeftOf, thenBranch)
->put(TheRightOf, header, AtRightOf, thenBranch)
->put(TheRightOf, shapeElement, 3, FromRightOf, header)
->put(TheBottomOf, shapeElement, AtBottomOf, elseIfBranch)
->put(TheTopOf, elseIfBranch, 2, FromBottomOf, thenBranch)
->put(TheLeftOf, elseIfBranch, AtLeftOf, header)
->put(TheRightOf, elseIfBranch, AtRightOf, shapeElement);
addForm(topLevelIfAndElseIf);
auto borderElement = new BorderFormElement{};
addForm(topLevelIfWithoutElse->clone()
->put(TheRightOf, shapeElement, AtRightOf, borderElement)
->put(TheLeftOf, header, AtLeftOf, borderElement));
addForm(topLevelIfAndStandardElse->clone()
->put(TheRightOf, shapeElement, AtRightOf, borderElement)
->put(TheLeftOf, header, AtLeftOf, borderElement));
addForm(topLevelIfAndElseIf->clone()
->put(TheRightOf, shapeElement, AtRightOf, borderElement)
->put(TheLeftOf, header, AtLeftOf, borderElement));
topLevelIfWithoutElse->setBottomMargin(10);
topLevelIfAndStandardElse->setBottomMargin(10);
topLevelIfAndElseIf->setBottomMargin(10);
We won't go into detail here, please refer to Andrea Helfenstein's Bachelor Thesis Report for the various ways to use Visualization::DeclarativeItem. In short:
- We create several elements that are reused in multiple forms:
icon
, condition
, header
- We wrap some elements in a grid, just so that the elements can be stretched by their parent if needed. This will not result in any visual stretching since we declare only the last (empty) cell of the grid as stretchable. This is merely needed to satisfy the constraints of the anchor layout.
- Then we define three forms that match different situations for an if-statement
- Based on these three forms we make three additional forms which are similar but allow the if statement to visually stretch to fill all available space on the right.
- Finally we add some margins to the original three forms (not their clones)
This is all there is to it - now we have a visualization for if statements that will be automatically used when needed. In simpler cases, all you need to do is define the initializeForms()
method and you can create a visual item in no time. It is recommended to look at other examples when creating your own items. An example of a very short item is OOVisualization::VTypeAlias, and an example of a more complicated one is OOVisualization::VClass.
Item styles
Each item type has an associated style type. A style type is a class that contains properties used to determine how exactly the item should be rendered. For example an item rendering text may have a style that tells it what font family and size to use, what text the color should be, should there be a background, etc. The properties defined for a style can be other styles or any number of 'basic' values: numbers, booleans, lists, fonts, brushes, colors, etc. An important feature of styles is that they are dynamically loaded at run-time by reading them from an XML file. Thus all the properties that you specify in a style can be used to alter the appearance of an item, without the need of recompiling any code.
Each item must have at least a default style that specifies all properties listed in the item's style type. Within your plug-in you must put the default style in the /styles/item/ItemName/
directory and it should be called default
. For the OOVisualization::VIfStatement this file is /OOVisualization/styles/item/VIfStatement/default
.
Creating a style class is straight forward, all you really need to do is list the properties of the style. Here is the header of the style class for if statements
(VIfStatementStyle.h):
#pragma once
#include "../oovisualization_api.h"
#include "VisualizationBase/src/layouts/SequentialLayoutStyle.h"
#include "VisualizationBase/src/items/VListStyle.h"
#include "VisualizationBase/src/items/StaticStyle.h"
#include "VisualizationBase/src/items/LineStyle.h"
class OOVISUALIZATION_API VIfStatementStyle :
public Super<Visualization::DeclarativeItemBaseStyle>
{
public:
virtual ~VIfStatementStyle() override;
Property<Visualization::SequentialLayoutStyle> header{this, "header"};
Property<Visualization::StaticStyle> icon{this, "icon"};
Property<Visualization::StaticStyle> elseIcon{this, "elseIcon"};
Property<Visualization::StaticStyle> elificon{this, "elificon"};
Property<Visualization::ItemStyle> condition{this, "condition"};
Property<Visualization::VListStyle> thenBranch{this, "thenBranch"};
Property<Visualization::VListStyle> elseBranch{this, "elseBranch"};
Property<Visualization::VListStyle> elseIfBranch{this, "elseIfBranch"};
Property<Visualization::LineStyle> elseHorizontalLine{this, "elseHorizontalLine"};
Property<Visualization::LineStyle> elseVerticalLine{this, "elseVerticalLine"};
};
}
There are three things to notice here:
- The style must ultimately inherit from Visualization::ItemStyle. If your item inherits from Visualization::DeclarativeItem or another visual item that requires a more specific style, you must inherit from that item's style.
- We always need to explicitly define the destructor. It must not be defined inline in the header file and should be defined only in the corresponding
.cpp file. Almost always the destructor will be empty and it will be the only thing we put in the
.cpp file. Nevertheless, this should still be done. The reason is that this is a hint to gcc, where to generate the virtual table for the class and other related data structures. If we don't do this, then this information will be generated in multiple plug-ins which will cause problems at run-time on Windows.
- There is a list of properties that we want our style to include. Each property is defined by providing the type of information it should store. Additionally we need to provide this and a string to the property's constructor. The string is name of the tag in the XML file that will be used to read this property. By convention this should be the same as the name of the property.
The corresponding
.cpp file is trivial and the same for most styles:
#include "VIfStatementStyle.h"
}
virtual ~VIfStatementStyle() override
Definition: VIfStatementStyle.cpp:31
And here is the corresponding default XML file
(OOVisualization/styles/item/VIfStatement/default). Things here can get a bit clunky, but the important thing to see is that each top-level tag corresponds to a property of the style or to a property of a base class. Simple properties like cornerRadius
are represented by values. Properties which are themselves styles, follow an XML structure as dictated in their style class. To learn more about how styles work see the documentation of the Visualization namespace.
<!DOCTYPE EnvisionVisualizationStyle>
<style prototypes="item/DeclarativeItemBase/default">
<shape prototypes="shape/Box/default">
Box
<outline>
<width>1</width>
<color>#e0e0c8</color>
</outline>
<backgroundBrush>
<style>1</style>
<color>#ffffe0</color>
</backgroundBrush>
<cornerRadius>2</cornerRadius>
</shape>
<header prototypes="layout/SequentialLayout/default">
<spaceBetweenElements>5</spaceBetweenElements>
</header>
<icon prototypes="icon/SVGIcon/default">
<filename>styles/icon/if.svg</filename>
<width>22</width>
<height>22</height>
<zValue>1</zValue>
</icon>
<elseIcon prototypes="icon/SVGIcon/default">
<filename>styles/icon/else.svg</filename>
<width>22</width>
<height>22</height>
<zValue>1</zValue>
</elseIcon>
<elificon prototypes="icon/SVGIcon/default">
<filename>styles/icon/if.svg</filename>
<width>22</width>
<height>22</height>
<zValue>1</zValue>
</elificon>
<condition prototypes="item/Item/default">
<shape prototypes="shape/Frame/default">
Frame
<outline>
<width>1</width>
<color>#e0e0c8</color>
</outline>
<contentBrush>
<color>#ffffe0</color>
</contentBrush>
<topBrush>
<style>1</style>
<color>#e0e0c8</color>
</topBrush>
<topWidth>1</topWidth>
<topMargin>1</topMargin>
<bottomBrush>
<style>1</style>
<color>#e0e0c8</color>
</bottomBrush>
<bottomWidth>1</bottomWidth>
<bottomMargin>1</bottomMargin>
<leftBrush>
<style>1</style>
<color>#e0e0c8</color>
</leftBrush>
<leftWidth>1</leftWidth>
<leftMargin>15</leftMargin>
<rightBrush>
<style>1</style>
<color>#e0e0c8</color>
</rightBrush>
<rightWidth>1</rightWidth>
<rightMargin>1</rightMargin>
</shape>
</condition>
<thenBranch prototypes="item/VStatementItemList/default" />
<elseBranch prototypes="item/VStatementItemList/default" />
<elseIfBranch prototypes="item/VStatementItemList/default" />
<elseHorizontalLine prototypes="item/Line/default">
<pen>
<color>#e0e0c8</color>
</pen>
</elseHorizontalLine>
<elseVerticalLine prototypes="item/Line/default">
<width>1</width>
<height>0</height>
<pen>
<color>#e0e0c8</color>
</pen>
</elseVerticalLine>
</style>