Using GraphCanvas

⚠️ Heads Up!

This is incomplete and a work in progress. Think of it as a "getting started" collection of notes rather than proper docs

Credits

  • Shauna: For her notes to get started on Graph Canvas.

Getting started

You'll need to create a gem with a template that gives you a QWidget in your editor tools lke CppToolGem.

We need to add a couple of deps to your CMakeLists.txt in the Code directory of your gem.

This wont list out everything so dont copy and paste this, just add what isn't there.

ly_add_target(
        NAME ${gem_name}.Editor.Private.Object STATIC
        #...
        BUILD_DEPENDENCIES
        PRIVATE
            Gem::GraphModel.Editor.Static
        PUBLIC
            Gem::GraphCanvasWidgets
)

ly_add_target(
        NAME ${gem_name}.Editor GEM_MODULE
        #...
        BUILD_DEPENDENCIES
            PUBLIC
                Gem::GraphCanvas.Editor
            PRIVATE
                Gem::GraphModel.Editor.Static
            RUNTIME_DEPENDENCIES
                Gem::GraphCanvas.Editor
                Gem::GraphModel.Editor
)

Header file

A couple of things you need in your widget header

#if !defined(Q_MOC_RUN)
#include <GraphModel/Integration/EditorMainWindow.h>
#endif

namespace Ui
{
    class GraphCanvasEditorDockWidget;
}

namespace MyGem
{
    inline const GraphCanvas::EditorId THINGCANVAS_EDITOR_ID = AZ_CRC_CE("ThingCanvas");

    class MyGemWidget : public QWidget
    {
        Q_OBJECT
        //....
    private:
        GraphCanvas::StyleManager m_styleManager;
        GraphCanvas::EditorId m_editorId;
        GraphCanvas::GraphId m_graphId;
        AZ::Entity* m_sceneEntity = nullptr;

        AZStd::unique_ptr<GraphCanvas::GraphCanvasGraphicsView> m_graphicsView;
    }
}

Source file

#include "AzQtComponents/Components/StyleManager.h"
#include "GraphCanvas/GraphCanvasBus.h"
#include "GraphCanvas/Components/Nodes/NodeBus.h"
#include "GraphCanvas/Components/Nodes/NodeTitleBus.h"
#include "GraphCanvas/Components/Nodes/Comment/CommentBus.h"
#include "GraphCanvas/Components/Slots/Extender/ExtenderSlotBus.h"
#include "GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.h"
#include "UI/PropertyEditor/EntityIdQLineEdit.h"

namespace MyGem
{
    MyGemWidget::MyGemWidget(QWidget* parent)
        : QWidget(parent)
    , m_graphicsView(new GraphCanvas::GraphCanvasGraphicsView())
    , m_editorId(THINGCANVAS_EDITOR_ID)
    , m_styleManager(THINGCANVAS_EDITOR_ID, "MaterialCanvas/StyleSheet/materialcanvas_style.json")
    {
        m_graphicsView->SetEditorId(m_editorId);

        GraphCanvas::GraphCanvasRequestBus::BroadcastResult(m_sceneEntity, &GraphCanvas::GraphCanvasRequests::CreateSceneAndActivate);
        m_graphId = m_sceneEntity->GetId();
        GraphCanvas::SceneRequestBus::Event(m_graphId, &GraphCanvas::SceneRequests::SetEditorId, m_editorId);

        m_graphicsView->SetScene(m_graphId);

        AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("style:Editor.qss"));
        AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral(":/GraphView/GraphView.qss"));

        QVBoxLayout* mainLayout = new QVBoxLayout(this);
        mainLayout->addWidget(m_graphicsView.get());
        setLayout(mainLayout);
    }
}

Now you should have a basic grid that you can zoom and move around in 🙂

Nodes

Let's add a node to your graph, this is actually pretty straightforward.

//Call this in your constructor after you've set your layout.
void CreateNode()
{
    const char* nodeStyle = "";
    AZ::Entity* nodeEntity = nullptr;
    GraphCanvas::GraphCanvasRequestBus::BroadcastResult(nodeEntity, &GraphCanvas::GraphCanvasRequests::CreateGeneralNodeAndActivate, nodeStyle);

    const AZ::EntityId nodeId = nodeEntity->GetId();

    GraphCanvas::NodeTitleRequestBus::Event(nodeId, &GraphCanvas::NodeTitleRequests::SetTitle, "My Node");
    GraphCanvas::NodeTitleRequestBus::Event(nodeId, &GraphCanvas::NodeTitleRequests::SetSubTitle, "Does stuff");

    //Now lets give it a slot!
    {
        AZ::Entity* slotEntity = nullptr;
        GraphCanvas::ExtenderSlotConfiguration extenderConfig;
        extenderConfig.m_extenderId  = AZ_CRC_CE("TLS_SID"); //?
        extenderConfig.m_name = "Exec";
        extenderConfig.m_tooltip = "Exec line";
        extenderConfig.m_connectionType = GraphCanvas::ConnectionType::CT_Input;
        extenderConfig.m_slotGroup = GraphCanvas::SlotGroups::ExecutionGroup;

        GraphCanvas::GraphCanvasRequestBus::BroadcastResult(slotEntity, &GraphCanvas::GraphCanvasRequests::CreateSlot, nodeId, extenderConfig);
        slotEntity->Init();
        slotEntity->Activate();

        GraphCanvas::NodeRequestBus::Event(nodeId, &GraphCanvas::NodeRequests::AddSlot, slotEntity->GetId());
    }

    //Add the node to our graph
    GraphCanvas::SceneRequestBus::Event(m_graphId, &GraphCanvas::SceneRequests::AddNode, nodeId, AZ::Vector2{}, false);
}

Overview

Now that you have the code, let's go over it.

We don't have anything handling the GraphCanvas::GraphModelRequestBus so connections won't be possible and same with things like undo/redo, the only class that implements this is the higher level stuff in GraphModelIntegration::GraphController or you can implement the GraphModelRequestBus yourself.

Notes

  • m_styleManager Seems to be a helper class that helps with loading JSON style files with hot reload support, it talks over EBus to apply the style using your editor id.
  • GraphCanvas uses the entity system of the engine a lot, in fact you can iterate the components on your entities even connections are entities.
  • If you use the higher level API that uses this it pretty much just manages these entities, creates the slots automatically and handles asset stuff.

Higher level Sorcery!

If you want the ease of use of the higher-level system without the asset editing part, then here's some code for ya.

If you're using the CreateNode function from the Getting Started section, you'll need to stop using it.

You'll need some new variables in your header

#include <GraphModel/Integration/EditorMainWindow.h>
#include "GraphModel/Integration/GraphController.h"
enum MyDataTypeEnum : GraphModel::DataType::Enum
{
    MyDT_Float,
};

class MyGraphContext : public GraphModel::GraphContext
{
public:
    MyGraphContext();
}


class MyGemWidget{
///....
private:
    GraphModelIntegration::GraphController* m_controller;
    GraphModel::GraphPtr m_graph;

    AZStd::shared_ptr<MyGraphContext> m_graphCtx;
}

And here's how you use it

#include "AzQtComponents/Components/StyleManager.h"
#include "GraphCanvas/GraphCanvasBus.h"
#include "GraphCanvas/Components/Nodes/NodeBus.h"
#include "GraphCanvas/Components/Nodes/NodeTitleBus.h"
#include "GraphCanvas/Components/Nodes/Comment/CommentBus.h"
#include "GraphCanvas/Components/Slots/Extender/ExtenderSlotBus.h"
#include "GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.h"
#include "UI/PropertyEditor/EntityIdQLineEdit.h"

namespace MyGem
{
    MyGraphContext::MyGraphContext() : 
        GraphContext("MyGem", ".mygemext", {})
    {
        const auto floatType = azrtti_typeid<float>();
        m_dataTypes.push_back(
            AZStd::make_shared<GraphModel::DataType>(MyDT_Float, floatType, AZStd::any{}, "Float", "float"));
    }

    //Just putting the node code here for simplicity 
    class SimpleNode : public GraphModel::Node
    {
    public:
        static const char* SN_THING_INPUT = "simpleNode_thing_in";
        AZ_RTTI(SimpleNode, "{UUID}", Node);

        static void Reflect(AZ::ReflectContext* context)
        {
            auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
            if (serializeContext)
            {
                serializeContext->Class<SimpleNode, GraphModel::Node>()
                                ->Version(0);
            }
        }

        SimpleNode() = default;

        SimpleNode(GraphModel::GraphPtr graph, MyGraphContext* ctx) : Node(graph),
            m_ctx(ctx)
        {
            RegisterSlots();
            CreateSlotData();
        }

        const char* GetTitle() const override
        {
            return "SimpleNode";
        }

        void RegisterSlots() override
        {
            GraphModel::DataTypePtr floatDataType = ctx->GetDataType(MyDT_Float);
            //Look at SlotDefinition's constructor for what these do :)
            RegisterSlot(AZStd::make_shared<GraphModel::SlotDefinition>(
                GraphModel::SlotDirection::Output,
                GraphModel::SlotType::Data,
                SN_THING_INPUT,
                "Thing",
                "A thingy",
                GraphModel::DataTypeList{floatDataType},
                AZStd::any(0.5f),
                0,
                0,
                AZStd::string{},
                AZStd::string{},
                AZStd::vector<AZStd::string>{},
                true,
                true
            ));
        }

        MyGraphContext* ctx;
    }

    MyGemWidget::MyGemWidget(QWidget* parent)
        : QWidget(parent)
    , m_graphicsView(new GraphCanvas::GraphCanvasGraphicsView())
    , m_editorId(THINGCANVAS_EDITOR_ID)
    , m_styleManager(THINGCANVAS_EDITOR_ID, "MaterialCanvas/StyleSheet/materialcanvas_style.json")
    {
        m_graphicsView->SetEditorId(m_editorId);

        GraphCanvas::GraphCanvasRequestBus::BroadcastResult(m_sceneEntity, &GraphCanvas::GraphCanvasRequests::CreateSceneAndActivate);
        m_graphId = m_sceneEntity->GetId();
        GraphCanvas::SceneRequestBus::Event(m_graphId, &GraphCanvas::SceneRequests::SetEditorId, m_editorId);

        m_graphicsView->SetScene(m_graphId);

        AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("style:Editor.qss"));
        AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral(":/GraphView/GraphView.qss"));

        QVBoxLayout* mainLayout = new QVBoxLayout(this);
        mainLayout->addWidget(m_graphicsView.get());
        setLayout(mainLayout);

        m_graphCtx = AZStd::make_shared<MyGraphContext>();

        m_graph = AZStd::make_shared<GraphModel::Graph>(m_graphCtx);
        m_controller = new GraphModelIntegration::GraphController(m_graph, m_graphId);

        //Now lets add some nodes!
        m_controller->AddNode(AZStd::make_shared<SimpleNode>(m_graph, m_graphCtx));
    }
}

Handling things myself!

If you don't want to use the higher level stuff heres a thing you can copy and paste, so you have all the pure virtual functions exposed.

void RequestUndoPoint() override;
void RequestPushPreventUndoStateUpdate() override;
void RequestPopPreventUndoStateUpdate() override;
void TriggerUndo() override;
void TriggerRedo() override;
void DisconnectConnection(const GraphCanvas::ConnectionId& connectionId) override;
bool CreateConnection(const GraphCanvas::ConnectionId& connectionId, const GraphCanvas::Endpoint& sourcePoint, const GraphCanvas::Endpoint& targetPoint) override;
bool IsValidConnection(const GraphCanvas::Endpoint& sourcePoint, const GraphCanvas::Endpoint& targetPoint) const override;
AZStd::string GetDataTypeString(const AZ::Uuid& typeId) override;
void OnSaveDataDirtied(const AZ::EntityId& savedElement) override;
void OnRemoveUnusedNodes() override;
void OnRemoveUnusedElements() override;
void ResetSlotToDefaultValue(const GraphCanvas::Endpoint& endpoint) override;