Write your first plugin

An Easy Writer plugin is a native shared library the app discovers at startup and loads. On load it declares its ABI level and identity and, through a host-supplied PluginContext, registers what it adds: colour themes, AI commands, and — from ABI 2 — whole new dockable panels and menu commands that read and edit the open project. A plugin links ew::core + ew::app, so a panel can use the same model and engines the app itself uses. Everything reachable that way is documented in this reference; this guide builds a panel that reads the codex and the relationship graph.

The shape of a plugin

The concrete plugin is a QObject that also implements the pure-vtable ew::app::Plugin interface. The Q_PLUGIN_METADATA / Q_INTERFACES macros wire it up so QPluginLoader can find and cast it, while the interface itself stays a plain vtable that is safe across the DLL/ABI boundary.

#include <ew/app/Plugin.h>
#include <ew/app/PluginAppContext.h>
#include <ew/app/PluginContext.h>
#include <ew/app/PluginContributions.h>
#include <QObject>
#include <QWidget>

class MyPlugin : public QObject, public ew::app::Plugin {
    Q_OBJECT
    Q_PLUGIN_METADATA(IID EW_PLUGIN_IID)
    Q_INTERFACES(ew::app::Plugin)

public:
    int abiVersion() const override { return ew::app::kPluginAbiVersion; }

    ew::app::PluginMetadata metadata() const override {
        return {.id = QStringLiteral("games.acme.graph"),
                .name = QStringLiteral("Graph Explorer"),
                .version = QStringLiteral("1.0.0"),
                .author = QStringLiteral("Acme"),
                .description = QStringLiteral("A panel that explores the relationship graph.")};
    }

    void registerContributions(ew::app::PluginContext& context) override {
        context.addPanel({.id = QStringLiteral("games.acme.graph.panel"),
                          .title = QStringLiteral("Graph Explorer"),
                          .makeWidget = [](ew::app::PluginAppContext& app) -> QWidget* {
                              return makeGraphPanel(app);
                          }});
    }
};

#include "MyPlugin.moc"

Reaching the data: PluginAppContext

A PluginPanel's makeWidget factory is called once, on the GUI thread, with a live PluginAppContext. That is the handle to the running app:

Read the project, build a graph

makeGraphPanel reads the codex and turns the relationship graph into a laid-out set of points, using the same engine the built-in Relationship dock uses.

#include <ew/app/PluginAppContext.h>
#include <ew/app/RelationshipGraph.h>
#include <ew/app/RelationshipLayout.h>
#include <ew/core/Entity.h>
#include <ew/core/Project.h>
#include <QLabel>
#include <QVBoxLayout>
#include <QWidget>

QWidget* makeGraphPanel(ew::app::PluginAppContext& app) {
    ew::core::Project& project = app.project();

    // Read: count the codex entities (project.ids() enumerates every content object).
    int entities = 0;
    for (const ew::core::ContentId id : project.ids())
        if (dynamic_cast<const ew::core::Entity*>(project.find(id)) != nullptr)
            ++entities;

    // Build + lay out the relationship graph (nodes = entities, edges = Reference fields).
    const ew::app::RelationshipGraph graph = ew::app::relationshipGraph(project);
    const std::map<ew::core::ContentId, QPointF> positions =
        ew::app::forceDirectedLayout(graph);
    // graph.nodes: {id, name, categoryId, category}   graph.edges: {from, to, label}
    // Also available: neighborhood(), filterByCategories(), shortestRelationshipPath(), radialLayout().

    auto* widget = new QWidget;
    auto* layout = new QVBoxLayout(widget);
    layout->addWidget(new QLabel(
        QObject::tr("%1 entities, %2 links").arg(entities).arg(graph.edges.size()), widget));
    // Draw the graph yourself: e.g. a QGraphicsView with a node at each positions[node.id].
    return widget;
}

You render the graph with whatever Qt widgets you like (a QGraphicsView reading positions is the usual choice) — the data, layout, and queries all come from project() and the ew::app engines.

Edit the project, undoably

Push a Command onto commands() and it joins the app's Ctrl+Z history like any edit made in the UI:

#include <ew/core/AddObjectCommand.h>
#include <ew/core/Entity.h>

app.commands().push(std::make_unique<ew::core::AddObjectCommand>(
    app.project(),
    std::make_unique<ew::core::Entity>(ew::core::ContentId::generate(), ew::core::CategoryId(),
                                       QObject::tr("New entity"))));

There is a command for every kind of edit (see the ~100 *Command types under ew::core); reach for SetFieldValueCommand, SetEntityAliasesCommand, AddObjectCommand, and so on rather than mutating the model directly, so every change stays undoable.

Menu commands

For an action that does not need a panel, addMenuCommand contributes a PluginMenuCommand whose invoke(PluginAppContext&) runs on the GUI thread with the same live access:

context.addMenuCommand(
    {.id = QStringLiteral("games.acme.graph.count"),
     .title = QStringLiteral("Graph Explorer: Count Entities"),
     .invoke = [](ew::app::PluginAppContext& app) {
         QMessageBox::information(app.mainWindow(), QObject::tr("Graph Explorer"),
                                  QObject::tr("The project has %1 objects.")
                                      .arg(app.project().objectCount()));
     }});

Try it

A working reference plugin ships in the repository at plugins/example/ — it contributes a theme, an AI command, a panel that reads and edits the codex, and a menu command. Build it (next: Building & installing), drop the resulting library in the app's plugins/ folder, and restart: the panel appears as a dock, and Preferences ▸ Plugins lists the plugin and any that failed to load (with the reason), never crashing the app.

From here, the rest of the C++ developer API is every public type and function in ew::core and ew::app — all of it a documented, stable contract you can build on.