Graph Component | Modular Node/Edge Editor

This NodeBase is currently the Timer module implementation.

Source/Node/NodeBase.cpp

/*
  ==============================================================================

    NodeBase.cpp
    Created: 16 Aug 2024 3:26:59am
    Author:  Dirk Schiller

  ==============================================================================
*/

#include "NodeBase.h"
#include "Manager/NodeManager.h"

NodeBase::NodeBase(int x, int y, NodeManager* manager)
: isSelected(false), manager(manager)
{
    setBounds(x, y, 275 + 12, 135);

    inputSockets.add(new Socket("Input", true));
    inputSockets.add(new Socket("Input"));
    outputSockets.add(new Socket("Output", true));
    outputSockets.add(new Socket("Output"));

    addAndMakeVisible(inputSockets[0]);
    addAndMakeVisible(inputSockets[1]);
    addAndMakeVisible(outputSockets[0]);
    addAndMakeVisible(outputSockets[1]);
    
    auto socketSizeX = inputSockets[0]->getBounds().getWidth();
    auto socketSizeY = inputSockets[0]->getBounds().getHeight();

    if (inputSockets.size() > 0)
    {
        juce::Rectangle<int> inputSocketBounds (0, 0, socketSizeX, socketSizeY);
        inputSockets[0]->setBounds(inputSocketBounds.withX(45).withY(9));
        inputSockets[1]->setBounds(inputSocketBounds.withX(0).withY(95));
    }

    if (outputSockets.size() > 0)
    {
        juce::Rectangle<int> outputSocketBounds (0, 0, socketSizeX, socketSizeY);
        outputSockets[0]->setBounds(outputSocketBounds.withX(getWidth() - socketSizeX - 15).withY(9));
        outputSockets[1]->setBounds(outputSocketBounds.withX(getWidth() - socketSizeX).withY(95));
    }
    
    intervalProperty = std::make_unique<PropertyTextEditor>("Hello");
    intervalProperty->setBounds(162, 90, intervalProperty->getWidth(), intervalProperty->getHeight());
    addAndMakeVisible(intervalProperty.get());
}

void NodeBase::connectSockets(Socket* source, Socket* destination)
{
    connections.add({source, destination});
    repaint();
}

void NodeBase::deselect()
{
    isSelected = false;
    repaint();
}

void NodeBase::drawConnections(juce::Graphics& g)
{
    g.setColour(juce::Colours::blue);

    for (const auto& connection : connections)
    {
        auto* source = connection.first;
        auto* destination = connection.second;

        if (source && destination)
        {
            juce::Rectangle<int> sourceBounds = source->getBounds();
            juce::Rectangle<int> destinationBounds = destination->getBounds();

            juce::Point<int> sourceCentre = sourceBounds.getCentre();
            juce::Point<int> destinationCentre = destinationBounds.getCentre();

            g.drawLine(sourceCentre.getX(), sourceCentre.getY(), destinationCentre.getX(), destinationCentre.getY(), 2.0f);
        }
    }
}

void NodeBase::handleSocketMouseDown(Socket* socket, const juce::MouseEvent& event)
{
    // Get the mouse position in the NodeBase component's coordinate space
    juce::Point<int> mousePosInComponentSpace = event.getPosition();

    // Get the bounds of the socket in the NodeBase coordinate space
    juce::Rectangle<int> socketBounds = socket->getBounds();

    // Check if the mouse position is within the socket bounds
    bool isMouseWithinBounds = socketBounds.contains(mousePosInComponentSpace);

    if (isMouseWithinBounds)
    {
        draggingSocket = socket;
        return;
    }

    for (auto* inputSocket : inputSockets)
    {
        juce::Rectangle<int> inputSocketBounds = inputSocket->getBounds();

        if (inputSocketBounds.contains(mousePosInComponentSpace))
        {
            draggingSocket = inputSocket;
            return;
        }
    }

    for (auto* outputSocket : outputSockets)
    {
        juce::Rectangle<int> outputSocketBounds = outputSocket->getBounds();

        if (outputSocketBounds.contains(mousePosInComponentSpace))
        {
            draggingSocket = outputSocket;
            return;
        }
    }
}

void NodeBase::handleSocketMouseUp(Socket* socket, const juce::MouseEvent& event)
{
    // Detect if a socket was released over another socket
    if (draggingSocket != nullptr)
    {
        for (auto* socket : inputSockets)
        {
            if (socket != draggingSocket && socket->getBounds().contains(event.getPosition()))
            {
                connectSockets(draggingSocket, socket);
                draggingSocket = nullptr;
                return;
            }
        }

        for (auto* socket : outputSockets)
        {
            if (socket != draggingSocket && socket->getBounds().contains(event.getPosition()))
            {
                connectSockets(draggingSocket, socket);
                draggingSocket = nullptr;
                return;
            }
        }

        draggingSocket = nullptr;
    }
    else
    {
        // End dragging if no socket was involved
    }
}

void NodeBase::mouseDown(const juce::MouseEvent &event)
{
    giveAwayKeyboardFocus();
    intervalProperty->setHighlightedRegion(juce::Range<int>(0,0));
    bool multiSelect = event.mods.isShiftDown() || event.mods.isCommandDown();
    manager->deselectAllNodesExcept(this, multiSelect);
    isSelected = true;
    dragger.startDraggingComponent (this, event);
    repaint();
}

void NodeBase::mouseDrag (const juce::MouseEvent& event)
{
    if (draggingSocket != nullptr)
    {
        // Update the position of the dragging indicator or logic here if needed
    }
    else
    {
        // Drag the Node
        dragger.dragComponent(this, event, nullptr);
    }
}

void NodeBase::mouseEnter(const juce::MouseEvent& event)
{
    isPropertyTextEditorHovered = true;
    repaint();
}

void NodeBase::mouseExit(const juce::MouseEvent& event)
{
    isPropertyTextEditorHovered = false;
    repaint();
}

void NodeBase::mouseMove (const juce::MouseEvent& event) { }

void NodeBase::mouseUp(const juce::MouseEvent& event) { }

void NodeBase::paint(juce::Graphics& g)
{
    // Selection
//    if (isSelected) { g.setColour(juce::Colour(0xffffff11)); }
//    else if (isHovered) { g.setColour(juce::Colour(0xffffffff)); }
//    else { g.setColour(juce::Colour(0x88111111)); }
//    g.fillRoundedRectangle(getLocalBounds().toFloat(), 14.0f);
    
    // Body
    g.setColour(juce::Colour(0xff111111));
    g.fillRoundedRectangle(getLocalBounds().withX(0 + 6).withY(0).withWidth(275).withHeight(135).toFloat(), 12.f);
    
    // Base
    g.setColour(juce::Colour(0xff555555));
    g.fillRoundedRectangle(getLocalBounds().withX(3 + 6).withY(39).withWidth(269).withHeight(93).toFloat(), 9.f);
    
    // BG Title
    g.setColour(juce::Colour(0xff444444));
    juce::Path path;
    auto bounds = getLocalBounds().withX(3 + 6).withY(39).withWidth(269).withHeight(39);
    path.addRoundedRectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), 9.f, 9.f, true, true, false, false);
    g.fillPath(path);
    
    g.setColour(juce::Colour(0xff333333));
    g.drawLine(3 + 6, 77, 3 + 6 + 269, 77);
    
    // Title
    auto typeface = juce::Typeface::createSystemTypefaceFor(Assets::RopaSansRegular_ttf, Assets::RopaSansRegular_ttfSize);
    juce::FontOptions options(typeface);
    juce::Font font(options);
    font.setHeight(23.f);
    g.setFont(font);
    g.setColour(juce::Colour(0xff919cff));
    g.drawText("Timer", 3 + 6 + 13, 39 + 9, font.getStringWidth("Timer"), font.getHeight(), juce::Justification::left);
    
    // Icon
    std::unique_ptr<juce::XmlElement> svgIconTimer(juce::XmlDocument::parse(Assets::timer_svg));
    changeColor(svgIconTimer.get(), "#919cff");
    std::unique_ptr<juce::Drawable> svgDrawableIcon = juce::Drawable::createFromSVG(*svgIconTimer);
    svgDrawableIcon->setTransformToFit(juce::Rectangle<float>(getWidth() - 41, 47, 24, 24), juce::RectanglePlacement::centred);
    svgDrawableIcon->draw(g, 1.0f);

    // Trigger Creator Icon
    std::unique_ptr<juce::XmlElement> svgIconCreator(juce::XmlDocument::parse(Assets::creator_svg));
    changeColor(svgIconCreator.get(), "#333333");
    svgDrawableIcon = juce::Drawable::createFromSVG(*svgIconCreator);
    svgDrawableIcon->setTransformToFit(juce::Rectangle<float>(14, 7, 24, 24), juce::RectanglePlacement::centred);
    svgDrawableIcon->draw(g, 1.0f);
    
    // Property Text
    font.setHeight(19.f);
    g.setFont(font);
    g.setColour(juce::Colour(0xff111111));
    g.drawText("Interval", 3 + 6 + 16, 95, font.getStringWidth("Interval"), font.getHeight(), juce::Justification::left);
    
    // Property Unit
    g.setColour(juce::Colour(0xffaaaaaa));
    g.drawText("ms", 3 + 6 + 128, 95, font.getStringWidth("ms"), font.getHeight(), juce::Justification::left);

    drawConnections(g);
}

void NodeBase::resized() { }

void NodeBase::changeColor(juce::XmlElement* xml, juce::String color_hex)
{
    if (xml->hasTagName("svg"))
    {
        juce::String fill = xml->getStringAttribute("fill");
        if (fill.isNotEmpty())
        {
            fill = color_hex;
            xml->setAttribute("fill", fill);
        }
    }
}

void NodeBase::unFocusPropertyTextEditors() {
    giveAwayKeyboardFocus();
    intervalProperty->setHighlightedRegion(juce::Range<int>(0,0));
}

void NodeBase::unHoverPropertyTextEditors() {
    isPropertyTextEditorHovered = false;
    repaint();
}

I understand. The node base is for making nodes?

do you know how to make an inverted output for adsr 1 , you can look at picture of korg ms-20 also it has eg1 trig in

Hi Dirk, the node editor is a very nice start for versatile API. Are there plans to publish the code on github ? I am searching for node editor for my ongoing project and possibly would like to contribute to it.