Skip to main content
Skip to content

Flake shape creation

Write a Flake shape plugin that can be loaded by any Calligra application

This tutorial will guide you step by step through the creation of a Flake shape. At the end you will be able to write a shape that is loadable by any Calligra application.

For a technical introduction to how plugins in general are structured in Calligra, see Generic Calligra plugin creation — this tutorial reuses that plugin/factory pattern for the shape-specific parts.

Do the groundwork: create a shape

First of all you need a class derived from the KoShape class. This will be the actual shape class, so you have to ensure that all the data you need for painting is accessible from this KoShape-derived class.

The only method you have to reimplement is paint(), which is responsible for painting your shape. loadOdf() and saveOdf() are also pure virtual, so your shape needs to implement ODF loading and saving as well, even if that just means storing and restoring your shape's data in a private namespace.

You might also be interested in the setSize() and size() methods, which control the size available to the shape. Some shapes know the size they need on their own, so they reimplement size() to return that fixed size instead of the size set on them — KoFormulaShape does this, because a formula's size is dictated by its contents.

If your shape has a special outline, reimplement outline() to return your shape's outline correctly. Rotation, scaling and skewing are handled through a transformation matrix, so you don't need to worry about them yourself.

Here is roughly how your shape's header might look:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#pragma once

#include <KoShape.h>

class SomeDataClass; // whatever data backs your shape

class KoFooShape : public KoShape
{
public:
    KoFooShape();
    ~KoFooShape() override;

    // absolutely necessary:
    void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) override;
    bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) override;
    void saveOdf(KoShapeSavingContext &context) const override;

private:
    SomeDataClass *m_dataClass;
};

Note that paint() now takes a KoShapePaintingContext in addition to the QPainter and KoViewConverter — this carries extra state (such as whether the shape is being painted for on-screen display or for a thumbnail) that used to be threaded through in other ways in older versions of Calligra.

Make your shape loadable: create a factory and a plugin

Once you have created your shape class and implemented everything necessary to make it at least compile, you can think about how it gets loaded.

Calligra apps use KoShapeFactoryBase to obtain instances of shapes in a generic way — an application of the abstract factory pattern. You should implement a KoShapeFactoryBase-derived class that makes creating new instances of your shape possible. There's one method you must override, since it's pure virtual:

  • bool supports(const KoXmlElement &element, KoShapeLoadingContext &context) const — return true if the given ODF/SVG element is one your shape knows how to load. This is how Calligra picks the right shape factory while loading a document.

And two more you will usually want to override too:

  • KoShape *createDefaultShape(KoDocumentResourceManager *documentResources = nullptr) const
  • KoShape *createShape(const KoProperties *params, KoDocumentResourceManager *documentResources = nullptr) const

Both take an optional KoDocumentResourceManager, which gives you access to resources shared by the whole document (such as an image collection), and both have sensible default implementations in KoShapeFactoryBasecreateShape() by default just ignores params and calls createDefaultShape() — so you only need to reimplement the ones your shape actually needs.

An example factory header:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#pragma once

#include <KoShapeFactoryBase.h>

class FooShapeFactory : public KoShapeFactoryBase
{
public:
    FooShapeFactory();
    ~FooShapeFactory() override = default;

    KoShape *createDefaultShape(KoDocumentResourceManager *documentResources = nullptr) const override;
    KoShape *createShape(const KoProperties *params, KoDocumentResourceManager *documentResources = nullptr) const override;
    bool supports(const KoXmlElement &element, KoShapeLoadingContext &context) const override;
};

And the corresponding implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "FooShapeFactory.h"
#include "KoFooShape.h"

#include <KLocalizedString>
#include <KoXmlNS.h>
#include <KoXmlReader.h>

FooShapeFactory::FooShapeFactory()
    : KoShapeFactoryBase("FooShape", i18n("Foo Shape"))
{
    setToolTip(i18n("A foo shape"));
}

KoShape *FooShapeFactory::createDefaultShape(KoDocumentResourceManager *documentResources) const
{
    KoFooShape *fooShape = new KoFooShape();
    // set defaults
    return fooShape;
}

KoShape *FooShapeFactory::createShape(const KoProperties *params, KoDocumentResourceManager *documentResources) const
{
    KoFooShape *fooShape = new KoFooShape();
    // use the params
    return fooShape;
}

bool FooShapeFactory::supports(const KoXmlElement &element, KoShapeLoadingContext &context) const
{
    Q_UNUSED(context);
    // return true for whichever ODF element(s) this shape knows how to load,
    // e.g. a custom draw:frame with a draw:foo child
    return element.localName() == "foo" && element.namespaceURI() == KoXmlNS::draw;
}

With the factory, there is now a generic way to obtain an instance of your shape. But somehow you still have to publish your shape as a plugin, so a Calligra application knows there is something to load. The flake library provides KoShapeRegistry for this: each application has access to the registry, and to let the application know about your shape you register it there:

KoShapeRegistry::instance()->add(new FooShapeFactory());

This call has to happen somewhere, and that somewhere is the plugin class described in Generic Calligra plugin creation — a small QObject-derived class that is instantiated once by the plugin loader purely so its constructor can perform this registration.

Example plugin header:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#pragma once

#include <QObject>
#include <QVariantList>

class FooShapePlugin : public QObject
{
    Q_OBJECT
public:
    FooShapePlugin(QObject *parent, const QVariantList &);
};

Example plugin implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include "FooShapePlugin.h"
#include "FooShapeFactory.h"

#include <KPluginFactory>

#include <KoShapeRegistry.h>

K_PLUGIN_FACTORY_WITH_JSON(FooShapePluginFactory, "calligra_shape_foo.json", registerPlugin<FooShapePlugin>();)

FooShapePlugin::FooShapePlugin(QObject *parent, const QVariantList &)
    : QObject(parent)
{
    // register the shape's factory
    KoShapeRegistry::instance()->add(new FooShapeFactory());
    // we could register more things here in this same plugin.
}

#include "FooShapePlugin.moc"

The K_PLUGIN_FACTORY_WITH_JSON macro (from <KPluginFactory>) is what makes FooShapePlugin loadable as a KDE plugin, and ties it to the JSON metadata file described next.

The last piece is the JSON metadata file that describes your plugin and makes it findable by Calligra. Example calligra_shape_foo.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "KPlugin": {
        "Name": "Foo Shape",
        "Description": "A foo shape",
        "ServiceTypes": [
            "Calligra/Shape"
        ]
    },
    "X-Flake-MinVersion": "28",
    "X-Flake-PluginVersion": "28"
}

Finally, here is a CMakeLists.txt that builds and installs the plugin. Note that it installs into calligra/shapes under the Qt plugin directory; that is what makes KoShapeRegistry find it, since shape plugins are located by directory rather than by their ServiceTypes entry:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cmake_minimum_required(VERSION 3.16)
project(fooshape)

set(CMAKE_AUTOMOC ON)

find_package(ECM 6 REQUIRED NO_MODULE)
list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})

include(KDEInstallDirs)
include(KDECMakeSettings)

find_package(CalligraQt6 REQUIRED COMPONENTS flake)

add_definitions(-DTRANSLATION_DOMAIN=\"calligra_shape_foo\")

add_library(calligra_shape_foo MODULE
    KoFooShape.cpp
    FooShapeFactory.cpp
    FooShapePlugin.cpp
)

target_link_libraries(calligra_shape_foo CalligraQt6::flake)

install(TARGETS calligra_shape_foo DESTINATION ${KDE_INSTALL_PLUGINDIR}/calligra/shapes)

Once this is configured and built, your plugin is known system-wide and can be loaded by any Calligra application. See Generic Calligra plugin creation for more detail on each of these pieces.

Make your shape editable: create a tool

To edit your shape in the GUI, the user needs a tool to select and manipulate it. For this you provide a KoToolBase-derived class, together with a KoToolFactoryBase that Calligra uses to instantiate your tool for each canvas — much like KoShapeFactoryBase is used to instantiate shapes. A tool class implements all the edit actions that can be performed on your shape, and it is possible to register more than one tool for the same shape (for example, a dedicated tool that only edits your shape's text, in addition to a general-purpose one).

The factory is registered the same way a shape factory is, from your plugin's constructor:

KoToolRegistry::instance()->add(new FooToolFactory());

For a complete, real-world example of a shape and its accompanying tool, see the picture shape plugin in the Calligra source tree, which pairs PictureShape/PictureShapeFactory with PictureTool/PictureToolFactory.