2026-09-23 15:14:16 +02:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
//
|
|
|
|
|
// Copyright (C) 2026- Equinor ASA
|
|
|
|
|
//
|
|
|
|
|
// ResInsight is free software: you can redistribute it and/or modify
|
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
//
|
|
|
|
|
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
|
|
|
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
|
|
|
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
|
|
|
// at <http://www.gnu.org/licenses/gpl.html> for more details.
|
|
|
|
|
//
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
#include "RiuWorkflowGraphView.h"
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
#include "RiuWorkflowGraphLayout.h"
|
|
|
|
|
|
|
|
|
|
#include <QContextMenuEvent>
|
|
|
|
|
#include <QDragEnterEvent>
|
2026-09-23 15:14:16 +02:00
|
|
|
#include <QFontMetrics>
|
|
|
|
|
#include <QGraphicsEllipseItem>
|
|
|
|
|
#include <QGraphicsPathItem>
|
2026-10-02 15:43:09 +02:00
|
|
|
#include <QGraphicsPixmapItem>
|
2026-09-23 15:14:16 +02:00
|
|
|
#include <QGraphicsPolygonItem>
|
|
|
|
|
#include <QGraphicsRectItem>
|
|
|
|
|
#include <QGraphicsScene>
|
|
|
|
|
#include <QGraphicsTextItem>
|
2026-10-02 15:43:09 +02:00
|
|
|
#include <QIcon>
|
2026-09-23 15:14:16 +02:00
|
|
|
#include <QJsonValue>
|
2026-10-02 13:13:27 +02:00
|
|
|
#include <QKeyEvent>
|
|
|
|
|
#include <QMimeData>
|
2026-09-23 15:14:16 +02:00
|
|
|
#include <QMouseEvent>
|
|
|
|
|
#include <QPainter>
|
|
|
|
|
#include <QPainterPath>
|
2026-10-02 13:13:27 +02:00
|
|
|
#include <QPainterPathStroker>
|
2026-09-23 15:14:16 +02:00
|
|
|
#include <QPen>
|
|
|
|
|
#include <QResizeEvent>
|
2026-10-02 13:13:27 +02:00
|
|
|
#include <QToolTip>
|
2026-09-23 15:14:16 +02:00
|
|
|
#include <QWheelEvent>
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
constexpr qreal nodeWidth = 240.0;
|
|
|
|
|
constexpr qreal configWidth = 190.0;
|
|
|
|
|
constexpr qreal portStep = 40.0;
|
|
|
|
|
constexpr qreal portTop = 51.0;
|
2026-10-03 09:59:29 +02:00
|
|
|
constexpr qreal subtitleGap = 16.0;
|
|
|
|
|
constexpr qreal stackOffset = 5.0;
|
2026-09-23 15:14:16 +02:00
|
|
|
constexpr qreal columnStep = 650.0;
|
|
|
|
|
constexpr qreal configGap = 110.0;
|
|
|
|
|
constexpr qreal rowGap = 55.0;
|
2026-10-02 13:13:27 +02:00
|
|
|
constexpr qreal portRadius = 5.0;
|
|
|
|
|
constexpr qreal portHitDist = 14.0;
|
2026-10-02 15:43:09 +02:00
|
|
|
constexpr int iconSize = 12;
|
2026-10-02 13:13:27 +02:00
|
|
|
|
|
|
|
|
const QColor taskBorder( 60, 85, 115 );
|
|
|
|
|
const QColor taskBackground( 226, 238, 250 );
|
|
|
|
|
const QColor unknownBackground( 232, 232, 232 );
|
|
|
|
|
const QColor configBorder( 140, 105, 45 );
|
|
|
|
|
const QColor configBackground( 253, 244, 218 );
|
|
|
|
|
const QColor configPortColor( 170, 110, 35 );
|
|
|
|
|
const QColor inputPortColor( 75, 145, 95 );
|
|
|
|
|
const QColor outputPortColor( 50, 115, 165 );
|
|
|
|
|
const QColor missingPortColor( 190, 60, 60 );
|
|
|
|
|
const QColor validTargetColor( 40, 170, 70 );
|
|
|
|
|
const QColor invalidTargetColor( 175, 175, 175 );
|
|
|
|
|
const QColor errorColor( 190, 45, 45 );
|
|
|
|
|
const QColor warningColor( 200, 130, 20 );
|
|
|
|
|
const QColor edgeColor( 75, 95, 115 );
|
|
|
|
|
const QColor selectedEdgeColor( 30, 120, 220 );
|
2026-10-03 09:59:29 +02:00
|
|
|
const QColor mapColor( 95, 80, 160 );
|
2026-10-02 13:13:27 +02:00
|
|
|
|
|
|
|
|
QString keyForField( const QString& field )
|
|
|
|
|
{
|
|
|
|
|
return field.isEmpty() ? "model" : "field:" + field;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString fieldForKey( const QString& key )
|
|
|
|
|
{
|
|
|
|
|
return key == "model" ? QString() : key.mid( 6 );
|
|
|
|
|
}
|
2026-09-23 15:14:16 +02:00
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
QStringList fieldPorts( const QJsonArray& fields )
|
|
|
|
|
{
|
|
|
|
|
QStringList keys;
|
|
|
|
|
for ( const QJsonValue& field : fields )
|
|
|
|
|
{
|
|
|
|
|
if ( field.isString() && !field.toString().isEmpty() ) keys.append( "field:" + field.toString() );
|
|
|
|
|
}
|
|
|
|
|
keys.removeDuplicates();
|
|
|
|
|
keys.sort();
|
|
|
|
|
return keys;
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
} // namespace
|
2026-09-23 15:14:16 +02:00
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 13:13:27 +02:00
|
|
|
namespace RiuWorkflowGraphViewItems
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
class GraphEdge;
|
2026-09-23 15:14:16 +02:00
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 13:13:27 +02:00
|
|
|
//==================================================================================================
|
|
|
|
|
/// How a port is drawn. Hollow: optional and not used. Missing: required and not provided.
|
|
|
|
|
//==================================================================================================
|
|
|
|
|
struct PortInfo
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
enum class Style
|
|
|
|
|
{
|
|
|
|
|
Normal,
|
|
|
|
|
Hollow,
|
|
|
|
|
Missing
|
|
|
|
|
};
|
2026-09-23 15:14:16 +02:00
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
QString key;
|
|
|
|
|
QString label;
|
|
|
|
|
QString toolTip;
|
2026-10-02 14:38:42 +02:00
|
|
|
QString typeName{};
|
2026-10-02 15:43:09 +02:00
|
|
|
QString iconResource{};
|
2026-10-03 09:59:29 +02:00
|
|
|
Style style = Style::Normal;
|
|
|
|
|
bool collect = false;
|
2026-10-02 13:13:27 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
|
|
|
///
|
|
|
|
|
//==================================================================================================
|
|
|
|
|
class PortItem : public QGraphicsEllipseItem
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
enum class Highlight
|
|
|
|
|
{
|
|
|
|
|
None,
|
|
|
|
|
Valid,
|
|
|
|
|
Invalid
|
|
|
|
|
};
|
2026-09-23 15:14:16 +02:00
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
PortItem( GraphNode* node, const PortInfo& info, bool output, bool config, const QPointF& center );
|
|
|
|
|
|
|
|
|
|
GraphNode* node() const { return m_node; }
|
|
|
|
|
QString key() const { return m_key; }
|
|
|
|
|
QString field() const { return fieldForKey( m_key ); }
|
|
|
|
|
bool isOutput() const { return m_output; }
|
|
|
|
|
bool isConfig() const { return m_config; }
|
|
|
|
|
void setHighlight( Highlight highlight, const QString& reason = {} );
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
GraphNode* m_node;
|
|
|
|
|
QString m_key;
|
|
|
|
|
bool m_output;
|
|
|
|
|
bool m_config;
|
|
|
|
|
QPointF m_center;
|
|
|
|
|
QBrush m_brush;
|
|
|
|
|
QPen m_pen;
|
|
|
|
|
QString m_toolTip;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
|
|
|
///
|
|
|
|
|
//==================================================================================================
|
2026-09-23 15:14:16 +02:00
|
|
|
class GraphNode : public QGraphicsRectItem
|
|
|
|
|
{
|
|
|
|
|
public:
|
2026-10-03 09:59:29 +02:00
|
|
|
GraphNode( const QString& name,
|
|
|
|
|
const std::vector<PortInfo>& inputs,
|
|
|
|
|
const std::vector<PortInfo>& outputs,
|
|
|
|
|
bool isConfig = false,
|
|
|
|
|
const QString& subtitle = {} );
|
2026-09-23 15:14:16 +02:00
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
qreal height() const;
|
|
|
|
|
QPointF portPosition( const QString& key, bool output ) const;
|
|
|
|
|
void addEdge( GraphEdge* edge );
|
|
|
|
|
QString name() const;
|
|
|
|
|
bool isConfig() const;
|
|
|
|
|
std::vector<PortItem*> ports() const;
|
|
|
|
|
void setConfigValue( const QString& fieldName, const QString& value );
|
|
|
|
|
void setTaskState( const QString& state, const QString& error = {} );
|
2026-10-03 09:59:29 +02:00
|
|
|
void setItemState( const QString& item, const QString& state, const QString& error = {} );
|
|
|
|
|
void setSubtitleToolTip( const QString& toolTip );
|
2026-10-02 13:13:27 +02:00
|
|
|
void setTitle( const QString& title, const QString& toolTip );
|
|
|
|
|
void setIssues( const QJsonArray& issues );
|
|
|
|
|
void setUnknownType( bool unknown );
|
2026-09-23 15:14:16 +02:00
|
|
|
|
|
|
|
|
protected:
|
|
|
|
|
QVariant itemChange( GraphicsItemChange change, const QVariant& value ) override;
|
|
|
|
|
|
|
|
|
|
private:
|
2026-10-02 13:13:27 +02:00
|
|
|
void addPorts( const std::vector<PortInfo>& ports, bool output );
|
|
|
|
|
void applyStyle();
|
|
|
|
|
|
|
|
|
|
QString m_name;
|
|
|
|
|
bool m_isConfig;
|
|
|
|
|
qreal m_width;
|
|
|
|
|
QMap<QString, PortItem*> m_inputs;
|
|
|
|
|
QMap<QString, PortItem*> m_outputs;
|
|
|
|
|
QMap<QString, QGraphicsTextItem*> m_configValues;
|
|
|
|
|
std::vector<GraphEdge*> m_edges;
|
|
|
|
|
QGraphicsTextItem* m_title = nullptr;
|
|
|
|
|
QGraphicsTextItem* m_stateLabel = nullptr;
|
|
|
|
|
QGraphicsTextItem* m_issueLabel = nullptr;
|
2026-10-03 09:59:29 +02:00
|
|
|
QGraphicsTextItem* m_subtitle = nullptr;
|
|
|
|
|
std::vector<QGraphicsRectItem*> m_stack;
|
|
|
|
|
qreal m_portTop = portTop;
|
|
|
|
|
QMap<QString, QString> m_itemStates;
|
|
|
|
|
QMap<QString, QString> m_itemErrors;
|
2026-10-02 13:13:27 +02:00
|
|
|
QString m_state;
|
|
|
|
|
QString m_stateError;
|
|
|
|
|
QString m_baseToolTip;
|
|
|
|
|
QString m_issueText;
|
|
|
|
|
int m_errorCount = 0;
|
|
|
|
|
int m_warningCount = 0;
|
|
|
|
|
bool m_unknownType = false;
|
2026-09-23 15:14:16 +02:00
|
|
|
};
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
//==================================================================================================
|
|
|
|
|
///
|
|
|
|
|
//==================================================================================================
|
2026-09-23 15:14:16 +02:00
|
|
|
class GraphEdge : public QGraphicsPathItem
|
|
|
|
|
{
|
|
|
|
|
public:
|
2026-10-02 13:13:27 +02:00
|
|
|
GraphEdge( GraphNode* from, QString outputKey, GraphNode* to, QString inputKey, bool isConfig, bool selectable );
|
|
|
|
|
|
|
|
|
|
void updatePath();
|
|
|
|
|
QPainterPath shape() const override;
|
|
|
|
|
bool isConfig() const { return m_isConfig; }
|
|
|
|
|
GraphNode* fromNode() const { return m_from; }
|
|
|
|
|
GraphNode* toNode() const { return m_to; }
|
|
|
|
|
QString outputField() const { return fieldForKey( m_outputKey ); }
|
|
|
|
|
QString inputField() const { return fieldForKey( m_inputKey ); }
|
2026-10-03 09:59:29 +02:00
|
|
|
void setLabel( const QString& label, const QString& toolTip );
|
2026-09-23 15:14:16 +02:00
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
protected:
|
|
|
|
|
QVariant itemChange( GraphicsItemChange change, const QVariant& value ) override;
|
2026-09-23 15:14:16 +02:00
|
|
|
|
|
|
|
|
private:
|
2026-10-02 13:13:27 +02:00
|
|
|
void applyPen();
|
|
|
|
|
|
2026-09-23 15:14:16 +02:00
|
|
|
GraphNode* m_from;
|
|
|
|
|
GraphNode* m_to;
|
|
|
|
|
QString m_outputKey;
|
|
|
|
|
QString m_inputKey;
|
2026-10-02 13:13:27 +02:00
|
|
|
bool m_isConfig;
|
2026-09-23 15:14:16 +02:00
|
|
|
QGraphicsPolygonItem* m_arrow;
|
2026-10-03 09:59:29 +02:00
|
|
|
QGraphicsTextItem* m_label = nullptr;
|
2026-09-23 15:14:16 +02:00
|
|
|
};
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 13:13:27 +02:00
|
|
|
PortItem::PortItem( GraphNode* node, const PortInfo& info, bool output, bool config, const QPointF& center )
|
|
|
|
|
: QGraphicsEllipseItem( center.x() - portRadius, center.y() - portRadius, 2 * portRadius, 2 * portRadius, node )
|
|
|
|
|
, m_node( node )
|
|
|
|
|
, m_key( info.key )
|
|
|
|
|
, m_output( output )
|
|
|
|
|
, m_config( config )
|
|
|
|
|
, m_center( center )
|
|
|
|
|
, m_toolTip( info.toolTip.isEmpty() ? info.label : info.toolTip )
|
|
|
|
|
{
|
|
|
|
|
const QColor color = config ? configPortColor : ( output ? outputPortColor : inputPortColor );
|
|
|
|
|
switch ( info.style )
|
|
|
|
|
{
|
|
|
|
|
case PortInfo::Style::Normal:
|
|
|
|
|
m_brush = color;
|
|
|
|
|
m_pen = Qt::NoPen;
|
|
|
|
|
break;
|
|
|
|
|
case PortInfo::Style::Hollow:
|
|
|
|
|
m_brush = QColor( 255, 255, 255 );
|
|
|
|
|
m_pen = QPen( color, 1.5 );
|
|
|
|
|
break;
|
|
|
|
|
case PortInfo::Style::Missing:
|
|
|
|
|
m_brush = QColor( 255, 255, 255 );
|
|
|
|
|
m_pen = QPen( missingPortColor, 2.0 );
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
setHighlight( Highlight::None );
|
|
|
|
|
setAcceptedMouseButtons( Qt::NoButton );
|
|
|
|
|
setZValue( 2 );
|
2026-10-03 09:59:29 +02:00
|
|
|
|
|
|
|
|
// An outer ring marks inputs that collect several connections
|
|
|
|
|
if ( info.collect )
|
|
|
|
|
{
|
|
|
|
|
const qreal ring = portRadius + 3.0;
|
|
|
|
|
auto* outer = new QGraphicsEllipseItem( center.x() - ring, center.y() - ring, 2 * ring, 2 * ring, node );
|
|
|
|
|
outer->setPen( QPen( color, 1.2 ) );
|
|
|
|
|
outer->setBrush( Qt::NoBrush );
|
|
|
|
|
outer->setAcceptedMouseButtons( Qt::NoButton );
|
|
|
|
|
outer->setZValue( 1.5 );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void PortItem::setHighlight( Highlight highlight, const QString& reason )
|
|
|
|
|
{
|
|
|
|
|
const qreal radius = highlight == Highlight::Valid ? portRadius * 1.8 : portRadius;
|
|
|
|
|
setRect( m_center.x() - radius, m_center.y() - radius, 2 * radius, 2 * radius );
|
|
|
|
|
switch ( highlight )
|
|
|
|
|
{
|
|
|
|
|
case Highlight::None:
|
|
|
|
|
setBrush( m_brush );
|
|
|
|
|
setPen( m_pen );
|
|
|
|
|
setToolTip( m_toolTip );
|
|
|
|
|
break;
|
|
|
|
|
case Highlight::Valid:
|
|
|
|
|
setBrush( validTargetColor );
|
|
|
|
|
setPen( QPen( validTargetColor.darker( 130 ), 1.5 ) );
|
|
|
|
|
setToolTip( m_toolTip );
|
|
|
|
|
break;
|
|
|
|
|
case Highlight::Invalid:
|
|
|
|
|
setBrush( invalidTargetColor );
|
|
|
|
|
setPen( Qt::NoPen );
|
|
|
|
|
setToolTip( reason.isEmpty() ? m_toolTip : m_toolTip + "\n\nCannot connect: " + reason );
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-03 09:59:29 +02:00
|
|
|
GraphNode::GraphNode( const QString& name,
|
|
|
|
|
const std::vector<PortInfo>& inputs,
|
|
|
|
|
const std::vector<PortInfo>& outputs,
|
|
|
|
|
bool isConfig,
|
|
|
|
|
const QString& subtitle )
|
2026-09-23 15:14:16 +02:00
|
|
|
: m_name( name )
|
|
|
|
|
, m_isConfig( isConfig )
|
|
|
|
|
, m_width( isConfig ? configWidth : nodeWidth )
|
2026-10-03 09:59:29 +02:00
|
|
|
, m_portTop( subtitle.isEmpty() ? portTop : portTop + subtitleGap )
|
2026-10-02 13:13:27 +02:00
|
|
|
, m_baseToolTip( name )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
const qsizetype rows = std::max( { qsizetype( 1 ), qsizetype( inputs.size() ), qsizetype( outputs.size() ) } );
|
2026-10-03 09:59:29 +02:00
|
|
|
setRect( 0, 0, m_width, m_portTop + portStep * rows + 12 );
|
2026-09-23 15:14:16 +02:00
|
|
|
setFlags( ItemIsMovable | ItemIsSelectable | ItemSendsGeometryChanges );
|
|
|
|
|
setZValue( 1 );
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
m_title = new QGraphicsTextItem( this );
|
2026-09-23 15:14:16 +02:00
|
|
|
QFont titleFont;
|
|
|
|
|
titleFont.setBold( true );
|
2026-10-02 13:13:27 +02:00
|
|
|
m_title->setFont( titleFont );
|
|
|
|
|
m_title->setPos( 10, 4 );
|
|
|
|
|
m_title->setAcceptedMouseButtons( Qt::NoButton );
|
2026-09-23 15:14:16 +02:00
|
|
|
|
|
|
|
|
if ( !isConfig )
|
|
|
|
|
{
|
|
|
|
|
m_stateLabel = new QGraphicsTextItem( this );
|
|
|
|
|
QFont font;
|
|
|
|
|
font.setPointSize( 8 );
|
|
|
|
|
font.setBold( true );
|
|
|
|
|
m_stateLabel->setFont( font );
|
|
|
|
|
m_stateLabel->setPos( m_width - 91, 5 );
|
|
|
|
|
m_stateLabel->setAcceptedMouseButtons( Qt::NoButton );
|
2026-10-02 13:13:27 +02:00
|
|
|
|
|
|
|
|
m_issueLabel = new QGraphicsTextItem( this );
|
|
|
|
|
m_issueLabel->setFont( font );
|
|
|
|
|
m_issueLabel->setPos( 8, rect().height() - 2 );
|
|
|
|
|
m_issueLabel->setAcceptedMouseButtons( Qt::NoButton );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-03 09:59:29 +02:00
|
|
|
// A mapped task runs once per item: drawn as a stack of boxes with a line saying what it maps over
|
|
|
|
|
if ( !subtitle.isEmpty() )
|
|
|
|
|
{
|
|
|
|
|
for ( int depth = 2; depth >= 1; --depth )
|
|
|
|
|
{
|
|
|
|
|
auto* card = new QGraphicsRectItem( rect().translated( depth * stackOffset, depth * stackOffset ), this );
|
|
|
|
|
card->setFlag( ItemStacksBehindParent );
|
|
|
|
|
card->setAcceptedMouseButtons( Qt::NoButton );
|
|
|
|
|
m_stack.push_back( card );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_subtitle = new QGraphicsTextItem( this );
|
|
|
|
|
QFont font;
|
|
|
|
|
font.setPointSize( 8 );
|
|
|
|
|
font.setItalic( true );
|
|
|
|
|
m_subtitle->setFont( font );
|
|
|
|
|
m_subtitle->setDefaultTextColor( mapColor );
|
|
|
|
|
m_subtitle->setPlainText( QFontMetrics( font ).elidedText( subtitle, Qt::ElideRight, m_width - 20 ) );
|
|
|
|
|
m_subtitle->setPos( 10, 24 );
|
|
|
|
|
m_subtitle->setAcceptedMouseButtons( Qt::NoButton );
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 15:14:16 +02:00
|
|
|
addPorts( inputs, false );
|
|
|
|
|
addPorts( outputs, true );
|
2026-10-02 13:13:27 +02:00
|
|
|
setTitle( isConfig ? "Config: " + name : name, {} );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
qreal GraphNode::height() const
|
|
|
|
|
{
|
|
|
|
|
return rect().height();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 13:13:27 +02:00
|
|
|
void GraphNode::setTitle( const QString& title, const QString& toolTip )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
m_title->setPlainText( QFontMetrics( m_title->font() ).elidedText( title, Qt::ElideRight, m_width - ( m_isConfig ? 20 : 100 ) ) );
|
|
|
|
|
m_title->setToolTip( title );
|
|
|
|
|
if ( !toolTip.isEmpty() ) m_baseToolTip = toolTip;
|
|
|
|
|
applyStyle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void GraphNode::addPorts( const std::vector<PortInfo>& ports, bool output )
|
|
|
|
|
{
|
|
|
|
|
auto& portMap = output ? m_outputs : m_inputs;
|
|
|
|
|
for ( size_t row = 0; row < ports.size(); ++row )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
const PortInfo& info = ports[row];
|
|
|
|
|
const qreal x = output ? m_width : 0.0;
|
2026-10-03 09:59:29 +02:00
|
|
|
const qreal y = m_portTop + row * portStep;
|
2026-10-02 13:13:27 +02:00
|
|
|
auto* port = new PortItem( this, info, output, m_isConfig, QPointF( x, y ) );
|
|
|
|
|
portMap.insert( info.key, port );
|
2026-09-23 15:14:16 +02:00
|
|
|
|
2026-10-02 14:38:42 +02:00
|
|
|
const QString toolTip = info.toolTip.isEmpty() ? info.label : info.toolTip;
|
|
|
|
|
const qreal textWidth = m_isConfig ? m_width - 24 : m_width / 2 - 18;
|
|
|
|
|
const bool showType = !info.typeName.isEmpty();
|
2026-10-02 15:43:09 +02:00
|
|
|
auto addText = [&]( const QString& content, const QFont& font, const QColor& color, qreal top, qreal indent = 0.0 )
|
2026-10-02 14:38:42 +02:00
|
|
|
{
|
2026-10-02 15:43:09 +02:00
|
|
|
auto* text = new QGraphicsTextItem( QFontMetrics( font ).elidedText( content, Qt::ElideRight, textWidth - indent ), this );
|
2026-10-02 14:38:42 +02:00
|
|
|
text->setFont( font );
|
|
|
|
|
text->setToolTip( toolTip );
|
|
|
|
|
text->setDefaultTextColor( color );
|
2026-10-02 15:43:09 +02:00
|
|
|
text->setPos( output ? m_width - text->boundingRect().width() - 12 : 10 + indent, top );
|
2026-10-02 14:38:42 +02:00
|
|
|
text->setAcceptedMouseButtons( Qt::NoButton );
|
2026-10-02 15:43:09 +02:00
|
|
|
return text;
|
2026-10-02 14:38:42 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
QFont labelFont;
|
|
|
|
|
labelFont.setPointSize( 9 );
|
|
|
|
|
const QColor labelColor = info.style == PortInfo::Style::Missing ? missingPortColor : QColor( 45, 65, 80 );
|
|
|
|
|
addText( info.label, labelFont, labelColor, y - ( m_isConfig || !output || showType ? 20 : 13 ) );
|
|
|
|
|
|
|
|
|
|
// The type goes on a second, smaller line below the label
|
|
|
|
|
if ( showType )
|
|
|
|
|
{
|
|
|
|
|
QFont typeFont;
|
|
|
|
|
typeFont.setPointSize( 7 );
|
|
|
|
|
typeFont.setItalic( true );
|
2026-10-02 15:43:09 +02:00
|
|
|
const QIcon icon( info.iconResource );
|
|
|
|
|
const bool showIcon = !info.iconResource.isEmpty() && !icon.isNull();
|
|
|
|
|
auto* typeText = addText( info.typeName, typeFont, QColor( 110, 120, 135 ), y - 3, showIcon ? iconSize + 2 : 0.0 );
|
|
|
|
|
|
|
|
|
|
// The icon of the ResInsight object goes in front of the type
|
|
|
|
|
if ( showIcon )
|
|
|
|
|
{
|
|
|
|
|
const QRectF textRect = typeText->mapRectToParent( typeText->boundingRect() );
|
|
|
|
|
const qreal iconX = output ? textRect.left() - iconSize - 1 : 10 + 3;
|
|
|
|
|
auto* pixmap = new QGraphicsPixmapItem( icon.pixmap( iconSize, iconSize ), this );
|
|
|
|
|
pixmap->setPos( iconX, textRect.center().y() - iconSize / 2.0 );
|
|
|
|
|
pixmap->setTransformationMode( Qt::SmoothTransformation );
|
|
|
|
|
pixmap->setToolTip( toolTip );
|
|
|
|
|
pixmap->setAcceptedMouseButtons( Qt::NoButton );
|
|
|
|
|
}
|
2026-10-02 14:38:42 +02:00
|
|
|
}
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
QPointF GraphNode::portPosition( const QString& key, bool output ) const
|
|
|
|
|
{
|
|
|
|
|
const auto& ports = output ? m_outputs : m_inputs;
|
|
|
|
|
auto* port = ports.value( key, nullptr );
|
|
|
|
|
return port ? port->mapToScene( port->rect().center() ) : scenePos();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
void GraphNode::addEdge( GraphEdge* edge )
|
|
|
|
|
{
|
|
|
|
|
m_edges.push_back( edge );
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
QString GraphNode::name() const
|
|
|
|
|
{
|
|
|
|
|
return m_name;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
bool GraphNode::isConfig() const
|
|
|
|
|
{
|
|
|
|
|
return m_isConfig;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
std::vector<PortItem*> GraphNode::ports() const
|
|
|
|
|
{
|
|
|
|
|
std::vector<PortItem*> result;
|
|
|
|
|
for ( PortItem* port : m_inputs )
|
|
|
|
|
result.push_back( port );
|
|
|
|
|
for ( PortItem* port : m_outputs )
|
|
|
|
|
result.push_back( port );
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
void GraphNode::setTaskState( const QString& state, const QString& error )
|
|
|
|
|
{
|
|
|
|
|
if ( m_isConfig ) return;
|
2026-10-02 13:13:27 +02:00
|
|
|
m_state = state;
|
|
|
|
|
m_stateError = error;
|
2026-10-03 09:59:29 +02:00
|
|
|
if ( state.isEmpty() )
|
|
|
|
|
{
|
|
|
|
|
m_itemStates.clear();
|
|
|
|
|
m_itemErrors.clear();
|
|
|
|
|
}
|
|
|
|
|
applyStyle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// The state of one item of a mapped task
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void GraphNode::setItemState( const QString& item, const QString& state, const QString& error )
|
|
|
|
|
{
|
|
|
|
|
if ( m_isConfig ) return;
|
|
|
|
|
m_itemStates[item] = state;
|
|
|
|
|
if ( error.isEmpty() )
|
|
|
|
|
m_itemErrors.remove( item );
|
|
|
|
|
else
|
|
|
|
|
m_itemErrors[item] = error;
|
2026-10-02 13:13:27 +02:00
|
|
|
applyStyle();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 09:59:29 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void GraphNode::setSubtitleToolTip( const QString& toolTip )
|
|
|
|
|
{
|
|
|
|
|
if ( m_subtitle ) m_subtitle->setToolTip( toolTip );
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void GraphNode::setIssues( const QJsonArray& issues )
|
|
|
|
|
{
|
|
|
|
|
if ( m_isConfig ) return;
|
|
|
|
|
|
|
|
|
|
m_errorCount = 0;
|
|
|
|
|
m_warningCount = 0;
|
|
|
|
|
QStringList lines;
|
|
|
|
|
for ( const QJsonValue& value : issues )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject issue = value.toObject();
|
|
|
|
|
const bool isError = issue.value( "severity" ).toString() != "warning";
|
|
|
|
|
( isError ? m_errorCount : m_warningCount )++;
|
|
|
|
|
lines.append( QString( "%1 %2" ).arg( isError ? "Error:" : "Warning:", issue.value( "message" ).toString() ) );
|
|
|
|
|
}
|
|
|
|
|
m_issueText = lines.join( "\n" );
|
|
|
|
|
applyStyle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void GraphNode::setUnknownType( bool unknown )
|
|
|
|
|
{
|
|
|
|
|
m_unknownType = unknown;
|
|
|
|
|
applyStyle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void GraphNode::applyStyle()
|
|
|
|
|
{
|
|
|
|
|
if ( m_isConfig )
|
|
|
|
|
{
|
|
|
|
|
setPen( QPen( configBorder ) );
|
|
|
|
|
setBrush( configBackground );
|
|
|
|
|
setToolTip( m_baseToolTip );
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-09-23 15:14:16 +02:00
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
QColor border = taskBorder;
|
|
|
|
|
QColor background = m_unknownType ? unknownBackground : taskBackground;
|
|
|
|
|
qreal width = 1.0;
|
|
|
|
|
auto style = Qt::SolidLine;
|
|
|
|
|
if ( m_state == "running" )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
|
|
|
|
border = QColor( 158, 112, 16 );
|
|
|
|
|
background = QColor( 255, 241, 189 );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
else if ( m_state == "completed" )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
|
|
|
|
border = QColor( 42, 120, 65 );
|
|
|
|
|
background = QColor( 215, 242, 222 );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
else if ( m_state == "failed" )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
|
|
|
|
border = QColor( 165, 50, 50 );
|
|
|
|
|
background = QColor( 253, 225, 225 );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
else if ( m_state == "interrupted" )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
|
|
|
|
border = QColor( 140, 85, 50 );
|
|
|
|
|
background = QColor( 245, 228, 210 );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
else if ( m_errorCount > 0 || m_warningCount > 0 )
|
|
|
|
|
{
|
|
|
|
|
border = m_errorCount > 0 ? errorColor : warningColor;
|
|
|
|
|
style = Qt::DashLine;
|
|
|
|
|
}
|
|
|
|
|
if ( !m_state.isEmpty() || m_errorCount > 0 || m_warningCount > 0 ) width = 2.0;
|
|
|
|
|
if ( isSelected() ) width += 1.0;
|
|
|
|
|
|
|
|
|
|
QPen pen( border, width );
|
|
|
|
|
pen.setStyle( style );
|
|
|
|
|
setPen( pen );
|
2026-09-23 15:14:16 +02:00
|
|
|
setBrush( background );
|
2026-10-03 09:59:29 +02:00
|
|
|
for ( QGraphicsRectItem* card : m_stack )
|
|
|
|
|
{
|
|
|
|
|
card->setPen( QPen( border, 1.0 ) );
|
|
|
|
|
card->setBrush( background.darker( 104 ) );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
|
2026-10-03 09:59:29 +02:00
|
|
|
// Item progress of a mapped task, such as "Running 3✓ 1✗"
|
|
|
|
|
int done = 0;
|
|
|
|
|
int failed = 0;
|
|
|
|
|
for ( const QString& itemState : m_itemStates )
|
|
|
|
|
{
|
|
|
|
|
if ( itemState == "completed" ) ++done;
|
|
|
|
|
if ( itemState == "failed" ) ++failed;
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( m_stateLabel )
|
|
|
|
|
{
|
2026-10-03 09:59:29 +02:00
|
|
|
QString text = m_state.isEmpty() ? "" : m_state.at( 0 ).toUpper() + m_state.mid( 1 );
|
|
|
|
|
if ( !m_itemStates.isEmpty() )
|
|
|
|
|
{
|
|
|
|
|
text += QString( " %1/%2" ).arg( done ).arg( m_itemStates.size() );
|
|
|
|
|
if ( failed > 0 ) text += QString::fromUtf8( " ✗%1" ).arg( failed );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
m_stateLabel->setDefaultTextColor( border );
|
2026-10-03 09:59:29 +02:00
|
|
|
m_stateLabel->setPlainText( text );
|
|
|
|
|
m_stateLabel->setPos( m_width - m_stateLabel->boundingRect().width() - 6, 5 );
|
2026-10-02 13:13:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( m_issueLabel )
|
|
|
|
|
{
|
|
|
|
|
QStringList badge;
|
|
|
|
|
if ( m_errorCount > 0 ) badge << QString( "%1 error%2" ).arg( m_errorCount ).arg( m_errorCount > 1 ? "s" : "" );
|
|
|
|
|
if ( m_warningCount > 0 ) badge << QString( "%1 warning%2" ).arg( m_warningCount ).arg( m_warningCount > 1 ? "s" : "" );
|
|
|
|
|
m_issueLabel->setDefaultTextColor( m_errorCount > 0 ? errorColor : warningColor );
|
|
|
|
|
m_issueLabel->setPlainText( badge.isEmpty() ? "" : QString::fromUtf8( "\u26a0 " ) + badge.join( ", " ) );
|
|
|
|
|
m_issueLabel->setToolTip( m_issueText );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QStringList toolTip{ m_baseToolTip };
|
|
|
|
|
if ( !m_stateError.isEmpty() ) toolTip << m_stateError;
|
2026-10-03 09:59:29 +02:00
|
|
|
if ( !m_itemStates.isEmpty() )
|
|
|
|
|
{
|
|
|
|
|
QStringList items;
|
|
|
|
|
for ( auto it = m_itemStates.cbegin(); it != m_itemStates.cend(); ++it )
|
|
|
|
|
{
|
|
|
|
|
QString line = QString( "%1: %2" ).arg( it.key(), it.value() );
|
|
|
|
|
if ( m_itemErrors.contains( it.key() ) ) line += " - " + m_itemErrors.value( it.key() );
|
|
|
|
|
items << line;
|
|
|
|
|
}
|
|
|
|
|
toolTip << "Items:\n" + items.join( "\n" );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( !m_issueText.isEmpty() ) toolTip << m_issueText;
|
|
|
|
|
setToolTip( toolTip.join( "\n\n" ) );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
void GraphNode::setConfigValue( const QString& fieldName, const QString& value )
|
|
|
|
|
{
|
|
|
|
|
if ( !m_isConfig ) return;
|
|
|
|
|
auto* port = m_outputs.value( "field:" + fieldName, nullptr );
|
|
|
|
|
if ( !port ) return;
|
|
|
|
|
|
|
|
|
|
auto* text = m_configValues.value( fieldName, nullptr );
|
|
|
|
|
if ( !text )
|
|
|
|
|
{
|
|
|
|
|
text = new QGraphicsTextItem( this );
|
|
|
|
|
QFont font;
|
|
|
|
|
font.setPointSize( 8 );
|
|
|
|
|
text->setFont( font );
|
|
|
|
|
text->setDefaultTextColor( QColor( 125, 80, 30 ) );
|
|
|
|
|
text->setPos( 10, port->rect().center().y() - 2 );
|
|
|
|
|
text->setAcceptedMouseButtons( Qt::NoButton );
|
|
|
|
|
m_configValues.insert( fieldName, text );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
text->setPlainText( QFontMetrics( text->font() ).elidedText( value, Qt::ElideRight, m_width - 24 ) );
|
|
|
|
|
text->setToolTip( fieldName + ": " + value );
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
QVariant GraphNode::itemChange( GraphicsItemChange change, const QVariant& value )
|
|
|
|
|
{
|
|
|
|
|
if ( change == ItemPositionHasChanged )
|
|
|
|
|
{
|
|
|
|
|
for ( GraphEdge* edge : m_edges )
|
|
|
|
|
edge->updatePath();
|
|
|
|
|
if ( scene() ) scene()->setSceneRect( scene()->sceneRect().united( scene()->itemsBoundingRect().adjusted( -50, -50, 50, 50 ) ) );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
else if ( change == ItemSelectedHasChanged && m_title )
|
|
|
|
|
{
|
|
|
|
|
applyStyle();
|
|
|
|
|
}
|
2026-09-23 15:14:16 +02:00
|
|
|
return QGraphicsRectItem::itemChange( change, value );
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 13:13:27 +02:00
|
|
|
GraphEdge::GraphEdge( GraphNode* from, QString outputKey, GraphNode* to, QString inputKey, bool isConfig, bool selectable )
|
2026-09-23 15:14:16 +02:00
|
|
|
: m_from( from )
|
|
|
|
|
, m_to( to )
|
|
|
|
|
, m_outputKey( std::move( outputKey ) )
|
|
|
|
|
, m_inputKey( std::move( inputKey ) )
|
2026-10-02 13:13:27 +02:00
|
|
|
, m_isConfig( isConfig )
|
2026-09-23 15:14:16 +02:00
|
|
|
, m_arrow( new QGraphicsPolygonItem( this ) )
|
|
|
|
|
{
|
|
|
|
|
setZValue( -1 );
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( selectable )
|
|
|
|
|
{
|
|
|
|
|
setFlag( ItemIsSelectable );
|
|
|
|
|
setToolTip( QString( "%1.%2 \u2192 %3.%4" )
|
|
|
|
|
.arg( from->name(),
|
|
|
|
|
outputField().isEmpty() ? "output" : outputField(),
|
|
|
|
|
to->name(),
|
|
|
|
|
inputField().isEmpty() ? "input" : inputField() ) );
|
|
|
|
|
}
|
2026-09-23 15:14:16 +02:00
|
|
|
m_arrow->setPen( Qt::NoPen );
|
2026-10-02 13:13:27 +02:00
|
|
|
applyPen();
|
2026-09-23 15:14:16 +02:00
|
|
|
m_from->addEdge( this );
|
|
|
|
|
m_to->addEdge( this );
|
|
|
|
|
updatePath();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 09:59:29 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// A label near the target, such as the index or key of a collected member
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void GraphEdge::setLabel( const QString& label, const QString& toolTip )
|
|
|
|
|
{
|
|
|
|
|
if ( label.isEmpty() ) return;
|
|
|
|
|
if ( !m_label )
|
|
|
|
|
{
|
|
|
|
|
m_label = new QGraphicsTextItem( this );
|
|
|
|
|
QFont font;
|
|
|
|
|
font.setPointSize( 8 );
|
|
|
|
|
m_label->setFont( font );
|
|
|
|
|
m_label->setDefaultTextColor( edgeColor );
|
|
|
|
|
m_label->setAcceptedMouseButtons( Qt::NoButton );
|
|
|
|
|
}
|
|
|
|
|
m_label->setPlainText( label );
|
|
|
|
|
if ( !toolTip.isEmpty() ) m_label->setToolTip( toolTip );
|
|
|
|
|
updatePath();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void GraphEdge::applyPen()
|
|
|
|
|
{
|
|
|
|
|
const QColor color = isSelected() ? selectedEdgeColor : ( m_isConfig ? configPortColor : edgeColor );
|
|
|
|
|
QPen pen( color, isSelected() ? 3.0 : 1.7 );
|
|
|
|
|
if ( m_isConfig ) pen.setStyle( Qt::DashLine );
|
|
|
|
|
setPen( pen );
|
|
|
|
|
m_arrow->setBrush( color );
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
void GraphEdge::updatePath()
|
|
|
|
|
{
|
|
|
|
|
const QPointF start = m_from->portPosition( m_outputKey, true );
|
|
|
|
|
const QPointF end = m_to->portPosition( m_inputKey, false );
|
|
|
|
|
const qreal bend = std::max( 35.0, std::abs( end.x() - start.x() ) / 2.0 );
|
|
|
|
|
QPainterPath path( start );
|
|
|
|
|
path.cubicTo( start + QPointF( bend, 0 ), end - QPointF( bend, 0 ), end );
|
|
|
|
|
setPath( path );
|
|
|
|
|
m_arrow->setPolygon( QPolygonF{ end, end + QPointF( -9, -5 ), end + QPointF( -9, 5 ) } );
|
2026-10-03 09:59:29 +02:00
|
|
|
|
|
|
|
|
// Place the label on the curve shortly before the target
|
|
|
|
|
if ( m_label )
|
|
|
|
|
{
|
|
|
|
|
const QPointF anchor = path.pointAtPercent( 0.85 );
|
|
|
|
|
const QRectF bounds = m_label->boundingRect();
|
|
|
|
|
m_label->setPos( anchor.x() - bounds.width() / 2.0, anchor.y() - bounds.height() );
|
|
|
|
|
}
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// A wide stroke, so the thin curve is easy to click
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
QPainterPath GraphEdge::shape() const
|
|
|
|
|
{
|
|
|
|
|
QPainterPathStroker stroker;
|
|
|
|
|
stroker.setWidth( 12 );
|
|
|
|
|
return stroker.createStroke( path() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
QVariant GraphEdge::itemChange( GraphicsItemChange change, const QVariant& value )
|
|
|
|
|
{
|
|
|
|
|
if ( change == ItemSelectedHasChanged ) applyPen();
|
|
|
|
|
return QGraphicsPathItem::itemChange( change, value );
|
|
|
|
|
}
|
|
|
|
|
} // namespace RiuWorkflowGraphViewItems
|
|
|
|
|
|
|
|
|
|
using namespace RiuWorkflowGraphViewItems;
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// Ports of a task in edit mode: every input and output the task type declares
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
std::pair<std::vector<PortInfo>, std::vector<PortInfo>> editModePorts( const QJsonObject& task, const QJsonArray& edges )
|
|
|
|
|
{
|
|
|
|
|
const QString name = task.value( "name" ).toString();
|
|
|
|
|
|
|
|
|
|
std::vector<PortInfo> inputs;
|
|
|
|
|
bool hasWholeInput = false;
|
|
|
|
|
for ( const QJsonValue& value : task.value( "input_ports" ).toArray() )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject port = value.toObject();
|
|
|
|
|
const QString field = port.value( "name" ).toString();
|
|
|
|
|
const bool required = port.value( "required" ).toBool();
|
|
|
|
|
const bool provided = port.value( "wired" ).toBool() || port.value( "configured" ).toBool() || port.value( "covered" ).toBool();
|
2026-10-02 15:00:39 +02:00
|
|
|
|
|
|
|
|
// The whole input model is only shown when it is wired, or when it is the only way into the task
|
|
|
|
|
if ( field.isEmpty() )
|
|
|
|
|
{
|
|
|
|
|
if ( !port.value( "wired" ).toBool() && !task.value( "whole_input" ).toBool() ) continue;
|
|
|
|
|
hasWholeInput = true;
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
|
|
|
|
|
PortInfo info;
|
2026-10-02 15:43:09 +02:00
|
|
|
info.key = keyForField( field );
|
|
|
|
|
info.label = field.isEmpty() ? "input" : field;
|
|
|
|
|
info.typeName = port.value( "type" ).toString();
|
|
|
|
|
info.iconResource = port.value( "icon" ).toString();
|
2026-10-02 13:13:27 +02:00
|
|
|
QStringList toolTip{ QString( "%1: %2" ).arg( info.label, port.value( "type" ).toString( "any" ) ) };
|
|
|
|
|
if ( !port.value( "description" ).toString().isEmpty() ) toolTip << port.value( "description" ).toString();
|
|
|
|
|
toolTip << ( required ? "Required" : "Optional" );
|
|
|
|
|
if ( port.value( "configured" ).toBool() ) toolTip << "Set in the job configuration";
|
|
|
|
|
if ( port.value( "covered" ).toBool() ) toolTip << "Provided by a whole-output connection";
|
|
|
|
|
info.toolTip = toolTip.join( "\n" );
|
2026-10-03 09:59:29 +02:00
|
|
|
if ( !port.value( "collect" ).toString().isEmpty() )
|
|
|
|
|
{
|
|
|
|
|
info.collect = true;
|
|
|
|
|
toolTip << ( port.value( "collect" ).toString() == "dict" ? "Connect several outputs to collect them, each under its own key"
|
|
|
|
|
: "Connect several outputs to collect them into a list" );
|
|
|
|
|
info.toolTip = toolTip.join( "\n" );
|
|
|
|
|
}
|
|
|
|
|
if ( port.value( "map_over" ).toBool() )
|
|
|
|
|
{
|
|
|
|
|
info.toolTip = QString( "%1: %2\n%3\nThe task runs once per item; set the items in the job configuration" )
|
|
|
|
|
.arg( info.label, info.typeName, port.value( "description" ).toString() );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( !provided && !field.isEmpty() ) info.style = required ? PortInfo::Style::Missing : PortInfo::Style::Hollow;
|
|
|
|
|
inputs.push_back( info );
|
|
|
|
|
}
|
|
|
|
|
for ( const QJsonValue& value : edges )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject edge = value.toObject();
|
|
|
|
|
if ( edge.value( "to" ).toString() == name && edge.value( "input" ).toString().isEmpty() && !hasWholeInput )
|
|
|
|
|
{
|
2026-10-02 14:38:42 +02:00
|
|
|
inputs.insert( inputs.begin(),
|
2026-10-02 15:43:09 +02:00
|
|
|
PortInfo{ .key = "model",
|
|
|
|
|
.label = "input",
|
|
|
|
|
.toolTip = {},
|
|
|
|
|
.typeName = task.value( "input_types" ).toObject().value( "" ).toString(),
|
|
|
|
|
.iconResource = task.value( "input_icons" ).toObject().value( "" ).toString() } );
|
2026-10-02 13:13:27 +02:00
|
|
|
hasWholeInput = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<PortInfo> outputs;
|
|
|
|
|
for ( const QJsonValue& value : task.value( "output_ports" ).toArray() )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject port = value.toObject();
|
|
|
|
|
const QString field = port.value( "name" ).toString();
|
|
|
|
|
PortInfo info;
|
2026-10-02 15:43:09 +02:00
|
|
|
info.key = keyForField( field );
|
|
|
|
|
info.label = field.isEmpty() ? "output" : field;
|
|
|
|
|
info.typeName = port.value( "type" ).toString();
|
|
|
|
|
info.iconResource = port.value( "icon" ).toString();
|
2026-10-02 13:13:27 +02:00
|
|
|
QStringList toolTip{ QString( "%1: %2" ).arg( info.label, port.value( "type" ).toString( "any" ) ) };
|
|
|
|
|
if ( !port.value( "description" ).toString().isEmpty() ) toolTip << port.value( "description" ).toString();
|
|
|
|
|
info.toolTip = toolTip.join( "\n" );
|
|
|
|
|
outputs.push_back( info );
|
|
|
|
|
}
|
2026-10-02 14:38:42 +02:00
|
|
|
if ( outputs.empty() )
|
2026-10-02 15:43:09 +02:00
|
|
|
outputs.push_back( PortInfo{ .key = "model",
|
|
|
|
|
.label = "output",
|
|
|
|
|
.toolTip = {},
|
|
|
|
|
.typeName = task.value( "output_types" ).toObject().value( "" ).toString(),
|
|
|
|
|
.iconResource = task.value( "output_icons" ).toObject().value( "" ).toString() } );
|
2026-10-02 13:13:27 +02:00
|
|
|
return { inputs, outputs };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 15:43:09 +02:00
|
|
|
std::vector<PortInfo> portInfos( const QStringList& keys, bool output, const QJsonObject& types, const QJsonObject& icons )
|
2026-10-02 13:13:27 +02:00
|
|
|
{
|
|
|
|
|
std::vector<PortInfo> result;
|
|
|
|
|
for ( const QString& key : keys )
|
2026-10-02 14:38:42 +02:00
|
|
|
{
|
|
|
|
|
const QString typeName = types.value( fieldForKey( key ) ).toString();
|
|
|
|
|
const QString label = key == "model" ? ( output ? "output" : "input" ) : fieldForKey( key );
|
2026-10-02 15:43:09 +02:00
|
|
|
result.push_back( PortInfo{ .key = key,
|
|
|
|
|
.label = label,
|
|
|
|
|
.toolTip = typeName.isEmpty() ? label : QString( "%1: %2" ).arg( label, typeName ),
|
|
|
|
|
.typeName = typeName,
|
|
|
|
|
.iconResource = icons.value( fieldForKey( key ) ).toString() } );
|
2026-10-02 14:38:42 +02:00
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
return result;
|
|
|
|
|
}
|
2026-09-23 15:14:16 +02:00
|
|
|
} // namespace
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
RiuWorkflowGraphView::RiuWorkflowGraphView( QWidget* parent )
|
|
|
|
|
: QGraphicsView( parent )
|
|
|
|
|
, m_scene( new QGraphicsScene( this ) )
|
|
|
|
|
{
|
|
|
|
|
setScene( m_scene );
|
|
|
|
|
setRenderHint( QPainter::Antialiasing );
|
|
|
|
|
setDragMode( QGraphicsView::ScrollHandDrag );
|
|
|
|
|
setTransformationAnchor( QGraphicsView::AnchorUnderMouse );
|
2026-10-02 13:13:27 +02:00
|
|
|
setAcceptDrops( true );
|
|
|
|
|
setFocusPolicy( Qt::StrongFocus );
|
|
|
|
|
|
|
|
|
|
connect( m_scene, &QGraphicsScene::selectionChanged, this, &RiuWorkflowGraphView::onSelectionChanged );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
RiuWorkflowGraphView::~RiuWorkflowGraphView() = default;
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::setEditable( bool editable )
|
|
|
|
|
{
|
|
|
|
|
m_editable = editable;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
bool RiuWorkflowGraphView::isEditable() const
|
|
|
|
|
{
|
|
|
|
|
return m_editable;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::setConnectionValidator( const ConnectionValidator& validator )
|
|
|
|
|
{
|
|
|
|
|
m_validator = validator;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::setPendingNodePosition( const QString& taskName, const QPointF& scenePos )
|
|
|
|
|
{
|
|
|
|
|
m_pendingPositions[taskName] = scenePos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// Keep the position of a task the user has placed when it is renamed
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::renameNodePosition( const QString& oldName, const QString& newName )
|
|
|
|
|
{
|
|
|
|
|
rememberPositions();
|
|
|
|
|
for ( const QString& prefix : { QString( "task:" ), QString( "config:" ) } )
|
|
|
|
|
{
|
|
|
|
|
if ( m_positions.contains( prefix + oldName ) ) m_positions[prefix + newName] = m_positions.take( prefix + oldName );
|
|
|
|
|
}
|
|
|
|
|
if ( m_lastSelectedTask == oldName ) m_lastSelectedTask = newName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::rememberPositions()
|
|
|
|
|
{
|
|
|
|
|
for ( auto it = m_taskNodes.cbegin(); it != m_taskNodes.cend(); ++it )
|
|
|
|
|
m_positions["task:" + it.key()] = it.value()->pos();
|
|
|
|
|
for ( auto it = m_configNodes.cbegin(); it != m_configNodes.cend(); ++it )
|
|
|
|
|
m_positions["config:" + it.key()] = it.value()->pos();
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 13:13:27 +02:00
|
|
|
void RiuWorkflowGraphView::fitGraph()
|
|
|
|
|
{
|
|
|
|
|
m_fitOnResize = true;
|
|
|
|
|
if ( !m_scene->sceneRect().isEmpty() ) fitInView( m_scene->sceneRect(), Qt::KeepAspectRatio );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// Rebuild the scene. With ViewportPolicy::Keep, tasks keep their positions and the view keeps
|
|
|
|
|
/// its zoom and scroll position, so edits do not make the graph jump.
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::showGraph( const QJsonObject& graph, const QString& error, ViewportPolicy policy )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
finishConnectionDrag( false );
|
|
|
|
|
|
|
|
|
|
const QTransform oldTransform = transform();
|
|
|
|
|
const QPointF oldCenter = mapToScene( viewport()->rect().center() );
|
|
|
|
|
const bool keepViewport = policy == ViewportPolicy::Keep && !m_taskNodes.isEmpty() && !m_fitOnResize;
|
|
|
|
|
const QString selectedTask = m_lastSelectedTask;
|
|
|
|
|
if ( policy == ViewportPolicy::Keep )
|
|
|
|
|
rememberPositions();
|
|
|
|
|
else
|
|
|
|
|
m_positions.clear();
|
|
|
|
|
|
|
|
|
|
m_rebuilding = true;
|
|
|
|
|
m_taskNodes.clear();
|
|
|
|
|
m_configNodes.clear();
|
2026-09-23 15:14:16 +02:00
|
|
|
m_runStatusLabel = nullptr;
|
|
|
|
|
m_scene->clear();
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( policy == ViewportPolicy::Fit ) m_fitOnResize = true;
|
2026-09-23 15:14:16 +02:00
|
|
|
|
|
|
|
|
if ( !error.isEmpty() || graph.value( "tasks" ).toArray().isEmpty() )
|
|
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
QString message = error;
|
|
|
|
|
if ( message.isEmpty() )
|
|
|
|
|
message = m_editable ? "No tasks yet.\nDrag a task from the palette, or right-click to add one." : "No tasks in this workflow";
|
|
|
|
|
m_scene->addText( message );
|
|
|
|
|
m_scene->setSceneRect( m_scene->itemsBoundingRect().adjusted( -30, -30, 30, 30 ) );
|
|
|
|
|
fitInView( m_scene->sceneRect(), Qt::KeepAspectRatio );
|
|
|
|
|
m_rebuilding = false;
|
|
|
|
|
m_pendingPositions.clear();
|
2026-09-23 15:14:16 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
const bool editMode = m_editable && graph.value( "editable" ).toBool();
|
|
|
|
|
const QJsonArray tasks = graph.value( "tasks" ).toArray();
|
|
|
|
|
const QJsonArray edges = graph.value( "edges" ).toArray();
|
|
|
|
|
const QString result = graph.value( "result_task" ).toString();
|
|
|
|
|
|
|
|
|
|
QStringList taskNames;
|
|
|
|
|
std::vector<std::pair<QString, QString>> layoutEdges;
|
|
|
|
|
QMap<QString, QJsonObject> taskData;
|
|
|
|
|
QMap<QString, QStringList> configFields;
|
2026-09-23 15:14:16 +02:00
|
|
|
for ( const QJsonValue& task : tasks )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject data = task.toObject();
|
|
|
|
|
const QString name = data.value( "name" ).toString();
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( name.isEmpty() || taskData.contains( name ) ) continue;
|
|
|
|
|
taskNames.append( name );
|
|
|
|
|
taskData.insert( name, data );
|
|
|
|
|
|
2026-09-23 15:14:16 +02:00
|
|
|
QStringList configured;
|
|
|
|
|
for ( const QJsonValue& field : data.value( "config_fields" ).toArray() )
|
|
|
|
|
{
|
|
|
|
|
const QString fieldName = field.toObject().value( "name" ).toString();
|
|
|
|
|
if ( !fieldName.isEmpty() ) configured.append( "field:" + fieldName );
|
|
|
|
|
}
|
|
|
|
|
configured.removeDuplicates();
|
|
|
|
|
configFields.insert( name, configured );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
for ( const QJsonValue& value : edges )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
const QJsonObject edge = value.toObject();
|
|
|
|
|
layoutEdges.emplace_back( edge.value( "from" ).toString(), edge.value( "to" ).toString() );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
// Read-only graphs only show the ports in use, plus the declared outputs of end tasks
|
|
|
|
|
QMap<QString, QStringList> usedInputs;
|
|
|
|
|
QMap<QString, QStringList> usedOutputs;
|
2026-09-23 15:14:16 +02:00
|
|
|
for ( const QJsonValue& value : edges )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject edge = value.toObject();
|
|
|
|
|
const QString from = edge.value( "from" ).toString();
|
|
|
|
|
const QString to = edge.value( "to" ).toString();
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( !taskData.contains( from ) || !taskData.contains( to ) || from == to ) continue;
|
|
|
|
|
usedInputs[to].append( keyForField( edge.value( "input" ).toString() ) );
|
|
|
|
|
usedOutputs[from].append( keyForField( edge.value( "output" ).toString() ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for ( const QString& name : taskNames )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject data = taskData.value( name );
|
|
|
|
|
|
|
|
|
|
std::vector<PortInfo> inputs;
|
|
|
|
|
std::vector<PortInfo> outputs;
|
|
|
|
|
if ( editMode )
|
|
|
|
|
{
|
|
|
|
|
std::tie( inputs, outputs ) = editModePorts( data, edges );
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
QStringList in = fieldPorts( data.value( "inputs" ).toArray() ) + usedInputs.value( name );
|
|
|
|
|
in.append( configFields.value( name ) );
|
|
|
|
|
QStringList out = usedOutputs.value( name );
|
|
|
|
|
if ( out.isEmpty() ) out = fieldPorts( data.value( "outputs" ).toArray() );
|
|
|
|
|
if ( out.isEmpty() ) out.append( "model" );
|
|
|
|
|
in.removeDuplicates();
|
|
|
|
|
out.removeDuplicates();
|
|
|
|
|
in.sort();
|
|
|
|
|
out.sort();
|
2026-10-02 15:43:09 +02:00
|
|
|
inputs = portInfos( in, false, data.value( "input_types" ).toObject(), data.value( "input_icons" ).toObject() );
|
|
|
|
|
outputs = portInfos( out, true, data.value( "output_types" ).toObject(), data.value( "output_icons" ).toObject() );
|
2026-10-02 13:13:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-03 10:08:00 +02:00
|
|
|
// Config fields follow the order of the task's inputs, so their connections do not cross
|
|
|
|
|
QStringList& configured = configFields[name];
|
|
|
|
|
auto inputIndex = [&inputs]( const QString& key )
|
|
|
|
|
{
|
|
|
|
|
const auto it = std::find_if( inputs.begin(), inputs.end(), [&key]( const PortInfo& port ) { return port.key == key; } );
|
|
|
|
|
return std::distance( inputs.begin(), it );
|
|
|
|
|
};
|
|
|
|
|
std::stable_sort( configured.begin(),
|
|
|
|
|
configured.end(),
|
|
|
|
|
[&inputIndex]( const QString& a, const QString& b ) { return inputIndex( a ) < inputIndex( b ); } );
|
|
|
|
|
|
2026-10-03 09:59:29 +02:00
|
|
|
const QJsonObject map = data.value( "map" ).toObject();
|
|
|
|
|
QString subtitle;
|
|
|
|
|
if ( !map.isEmpty() ) subtitle = QString::fromUtf8( "⟳ map over %1" ).arg( map.value( "over" ).toString() );
|
|
|
|
|
auto* node = new GraphNode( name, inputs, outputs, false, subtitle );
|
|
|
|
|
if ( !map.isEmpty() )
|
|
|
|
|
{
|
|
|
|
|
QStringList mapToolTip{
|
|
|
|
|
QString( "Runs once per item of '%1' (%2)" ).arg( map.value( "over" ).toString(), map.value( "type" ).toString() ) };
|
|
|
|
|
if ( !map.value( "key_as" ).toString().isEmpty() ) mapToolTip << QString( "Key → %1" ).arg( map.value( "key_as" ).toString() );
|
|
|
|
|
if ( !map.value( "value_as" ).toString().isEmpty() )
|
|
|
|
|
mapToolTip << QString( "Value → %1" ).arg( map.value( "value_as" ).toString() );
|
|
|
|
|
mapToolTip << ( map.value( "error_mode" ).toString() == "collect_all"
|
|
|
|
|
? "Every item runs; the task fails afterwards if any item failed"
|
|
|
|
|
: "The first failed item stops the task" );
|
|
|
|
|
mapToolTip << "The output is a dict with one entry per item";
|
|
|
|
|
node->setSubtitleToolTip( mapToolTip.join( "\n" ) );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
const QString taskId = data.value( "task_id" ).toString();
|
|
|
|
|
const QString description = data.value( "description" ).toString();
|
|
|
|
|
QStringList toolTip{ name };
|
|
|
|
|
if ( !taskId.isEmpty() ) toolTip << taskId;
|
|
|
|
|
if ( !description.isEmpty() ) toolTip << description;
|
|
|
|
|
if ( name == result ) toolTip << "Result task: its output is the result of the workflow";
|
|
|
|
|
if ( data.contains( "known_type" ) && !data.value( "known_type" ).toBool() )
|
|
|
|
|
toolTip << "Unknown task type: the package providing it is not installed";
|
|
|
|
|
node->setTitle( name == result ? QString::fromUtf8( "\u2605 " ) + name : name, toolTip.join( "\n" ) );
|
|
|
|
|
node->setUnknownType( data.contains( "known_type" ) && !data.value( "known_type" ).toBool() );
|
|
|
|
|
node->setIssues( data.value( "issues" ).toArray() );
|
2026-09-23 15:14:16 +02:00
|
|
|
m_scene->addItem( node );
|
2026-10-02 13:13:27 +02:00
|
|
|
m_taskNodes.insert( name, node );
|
|
|
|
|
|
|
|
|
|
if ( !configFields.value( name ).isEmpty() )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 15:43:09 +02:00
|
|
|
auto* configNode = new GraphNode( name,
|
|
|
|
|
{},
|
|
|
|
|
portInfos( configFields.value( name ),
|
|
|
|
|
true,
|
|
|
|
|
data.value( "input_types" ).toObject(),
|
|
|
|
|
data.value( "input_icons" ).toObject() ),
|
|
|
|
|
true );
|
2026-09-23 15:14:16 +02:00
|
|
|
m_scene->addItem( configNode );
|
2026-10-02 13:13:27 +02:00
|
|
|
m_configNodes.insert( name, configNode );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
auto slotHeight = [this]( const QString& name )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
qreal height = m_taskNodes.value( name )->height();
|
|
|
|
|
if ( auto* config = m_configNodes.value( name, nullptr ) ) height = std::max( height, config->height() );
|
2026-09-23 15:14:16 +02:00
|
|
|
return height;
|
|
|
|
|
};
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
const auto layout = RiuWorkflowGraphLayout::computeLayers( taskNames, layoutEdges );
|
|
|
|
|
qreal totalHeight = 0;
|
|
|
|
|
for ( const QStringList& layer : layout.layers )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
|
|
|
|
qreal height = 0;
|
|
|
|
|
for ( const QString& name : layer )
|
|
|
|
|
height += slotHeight( name ) + rowGap;
|
|
|
|
|
totalHeight = std::max( totalHeight, height - rowGap );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
for ( size_t column = 0; column < layout.layers.size(); ++column )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
const QStringList& layer = layout.layers[column];
|
|
|
|
|
qreal layerHeight = 0;
|
|
|
|
|
for ( const QString& name : layer )
|
2026-09-23 15:14:16 +02:00
|
|
|
layerHeight += slotHeight( name ) + rowGap;
|
|
|
|
|
qreal y = ( totalHeight - layerHeight + rowGap ) / 2.0;
|
2026-10-02 13:13:27 +02:00
|
|
|
for ( const QString& name : layer )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
auto* node = m_taskNodes.value( name );
|
|
|
|
|
const qreal x = column * columnStep + configWidth + configGap;
|
2026-09-23 15:14:16 +02:00
|
|
|
node->setPos( x, y + ( slotHeight( name ) - node->height() ) / 2.0 );
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( auto* config = m_configNodes.value( name, nullptr ) )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
|
|
|
|
config->setPos( x - configWidth - configGap, y + ( slotHeight( name ) - config->height() ) / 2.0 );
|
|
|
|
|
}
|
|
|
|
|
y += slotHeight( name ) + rowGap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
// Positions the user (or a drop) has chosen override the layout
|
|
|
|
|
for ( auto it = m_taskNodes.cbegin(); it != m_taskNodes.cend(); ++it )
|
|
|
|
|
{
|
|
|
|
|
GraphNode* config = m_configNodes.value( it.key(), nullptr );
|
|
|
|
|
const QPointF layoutPos = it.value()->pos();
|
|
|
|
|
QPointF position = layoutPos;
|
|
|
|
|
bool positioned = false;
|
|
|
|
|
if ( m_pendingPositions.contains( it.key() ) )
|
|
|
|
|
{
|
|
|
|
|
position = m_pendingPositions.value( it.key() );
|
|
|
|
|
positioned = true;
|
|
|
|
|
}
|
|
|
|
|
else if ( m_positions.contains( "task:" + it.key() ) )
|
|
|
|
|
{
|
|
|
|
|
position = m_positions.value( "task:" + it.key() );
|
|
|
|
|
positioned = true;
|
|
|
|
|
}
|
|
|
|
|
it.value()->setPos( position );
|
|
|
|
|
|
|
|
|
|
if ( config )
|
|
|
|
|
{
|
|
|
|
|
if ( m_positions.contains( "config:" + it.key() ) && !m_pendingPositions.contains( it.key() ) )
|
|
|
|
|
config->setPos( m_positions.value( "config:" + it.key() ) );
|
|
|
|
|
else if ( positioned )
|
|
|
|
|
config->setPos( position + QPointF( -configWidth - configGap, 0 ) );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m_pendingPositions.clear();
|
|
|
|
|
|
|
|
|
|
const bool selectableEdges = editMode;
|
2026-09-23 15:14:16 +02:00
|
|
|
for ( const QJsonValue& value : edges )
|
|
|
|
|
{
|
|
|
|
|
const QJsonObject edge = value.toObject();
|
|
|
|
|
const QString from = edge.value( "from" ).toString();
|
|
|
|
|
const QString to = edge.value( "to" ).toString();
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( !m_taskNodes.contains( from ) || !m_taskNodes.contains( to ) || from == to ) continue;
|
2026-10-03 09:59:29 +02:00
|
|
|
auto* graphEdge = new GraphEdge( m_taskNodes.value( from ),
|
2026-10-02 13:13:27 +02:00
|
|
|
keyForField( edge.value( "output" ).toString() ),
|
|
|
|
|
m_taskNodes.value( to ),
|
|
|
|
|
keyForField( edge.value( "input" ).toString() ),
|
|
|
|
|
false,
|
2026-10-03 09:59:29 +02:00
|
|
|
selectableEdges );
|
|
|
|
|
if ( edge.contains( "collect" ) )
|
|
|
|
|
{
|
|
|
|
|
const bool keyed = edge.value( "collect" ).toString() == "dict";
|
|
|
|
|
graphEdge->setLabel( edge.value( "label" ).toString(),
|
|
|
|
|
keyed ? QString( "Collected under the key '%1'" ).arg( edge.value( "label" ).toString() )
|
|
|
|
|
: QString( "Collected as item %1 of the list" ).arg( edge.value( "label" ).toString() ) );
|
|
|
|
|
}
|
|
|
|
|
m_scene->addItem( graphEdge );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
for ( auto it = m_configNodes.cbegin(); it != m_configNodes.cend(); ++it )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
|
|
|
|
for ( const QString& key : configFields.value( it.key() ) )
|
|
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
m_scene->addItem( new GraphEdge( it.value(), key, m_taskNodes.value( it.key() ), key, true, false ) );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_runStatusLabel = m_scene->addText( {} );
|
|
|
|
|
m_runStatusLabel->setDefaultTextColor( QColor( 45, 65, 80 ) );
|
|
|
|
|
m_runStatusLabel->setPos( 0, -45 );
|
|
|
|
|
m_scene->setSceneRect( m_scene->itemsBoundingRect().adjusted( -50, -50, 50, 50 ) );
|
2026-10-02 13:13:27 +02:00
|
|
|
|
|
|
|
|
if ( keepViewport )
|
|
|
|
|
{
|
|
|
|
|
setTransform( oldTransform );
|
|
|
|
|
centerOn( oldCenter );
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
fitInView( m_scene->sceneRect(), Qt::KeepAspectRatio );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_lastSelectedTask.clear();
|
|
|
|
|
if ( auto* node = m_taskNodes.value( selectedTask, nullptr ) )
|
|
|
|
|
{
|
|
|
|
|
node->setSelected( true );
|
|
|
|
|
m_lastSelectedTask = selectedTask;
|
|
|
|
|
}
|
|
|
|
|
m_rebuilding = false;
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 13:13:27 +02:00
|
|
|
void RiuWorkflowGraphView::setTaskIssues( const QString& taskName, const QJsonArray& issues )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( auto* node = m_taskNodes.value( taskName, nullptr ) ) node->setIssues( issues );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::selectTask( const QString& taskName )
|
|
|
|
|
{
|
|
|
|
|
auto* node = m_taskNodes.value( taskName, nullptr );
|
|
|
|
|
if ( !node ) return;
|
|
|
|
|
|
|
|
|
|
m_rebuilding = true;
|
|
|
|
|
m_scene->clearSelection();
|
|
|
|
|
node->setSelected( true );
|
|
|
|
|
m_lastSelectedTask = taskName;
|
|
|
|
|
m_rebuilding = false;
|
|
|
|
|
ensureVisible( node );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
RiuWorkflowGraphView::ItemRef RiuWorkflowGraphView::ItemRef::forTask( const QString& taskName )
|
|
|
|
|
{
|
|
|
|
|
ItemRef item;
|
|
|
|
|
item.kind = Kind::Task;
|
|
|
|
|
item.task = taskName;
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
RiuWorkflowGraphView::ItemRef
|
|
|
|
|
RiuWorkflowGraphView::ItemRef::forEdge( const QString& from, const QString& output, const QString& to, const QString& input )
|
|
|
|
|
{
|
|
|
|
|
ItemRef item;
|
|
|
|
|
item.kind = Kind::Edge;
|
|
|
|
|
item.from = from;
|
|
|
|
|
item.output = output;
|
|
|
|
|
item.to = to;
|
|
|
|
|
item.input = input;
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
RiuWorkflowGraphView::ItemRef RiuWorkflowGraphView::selectedItem() const
|
|
|
|
|
{
|
|
|
|
|
for ( QGraphicsItem* item : m_scene->selectedItems() )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( auto* node = dynamic_cast<GraphNode*>( item ) ) return ItemRef::forTask( node->name() );
|
|
|
|
|
if ( auto* edge = dynamic_cast<GraphEdge*>( item ); edge && !edge->isConfig() )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
return ItemRef::forEdge( edge->fromNode()->name(), edge->outputField(), edge->toNode()->name(), edge->inputField() );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::onSelectionChanged()
|
|
|
|
|
{
|
|
|
|
|
if ( m_rebuilding ) return;
|
|
|
|
|
|
|
|
|
|
const ItemRef selected = selectedItem();
|
|
|
|
|
if ( selected.kind != ItemRef::Kind::Task || selected.task == m_lastSelectedTask ) return;
|
|
|
|
|
|
|
|
|
|
m_lastSelectedTask = selected.task;
|
|
|
|
|
emit nodeSelected( selected.task );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::setTaskInputValue( const QString& taskName, const QString& fieldName, const QString& value )
|
|
|
|
|
{
|
|
|
|
|
if ( auto* node = m_configNodes.value( taskName, nullptr ) ) node->setConfigValue( fieldName, value );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
void RiuWorkflowGraphView::resetTaskStates()
|
|
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
for ( GraphNode* node : m_taskNodes )
|
|
|
|
|
node->setTaskState( {} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::setTaskState( const QString& taskName, const QString& state, const QString& error )
|
|
|
|
|
{
|
|
|
|
|
if ( auto* node = m_taskNodes.value( taskName, nullptr ) ) node->setTaskState( state, error );
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 09:59:29 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::setTaskItemState( const QString& taskName, const QString& item, const QString& state, const QString& error )
|
|
|
|
|
{
|
|
|
|
|
if ( auto* node = m_taskNodes.value( taskName, nullptr ) ) node->setItemState( item, state, error );
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::setRunStatus( const QString& status )
|
|
|
|
|
{
|
|
|
|
|
if ( !m_runStatusLabel ) return;
|
|
|
|
|
m_runStatusLabel->setPlainText( QFontMetrics( m_runStatusLabel->font() ).elidedText( status, Qt::ElideRight, 580 ) );
|
|
|
|
|
m_runStatusLabel->setToolTip( status );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// The task port nearest to the cursor, within a few pixels
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
PortItem* RiuWorkflowGraphView::portAt( const QPoint& viewPos ) const
|
|
|
|
|
{
|
|
|
|
|
const QPointF scenePos = mapToScene( viewPos );
|
|
|
|
|
const qreal distance = portHitDist / std::max( 0.05, transform().m11() );
|
|
|
|
|
PortItem* nearest = nullptr;
|
|
|
|
|
qreal best = distance;
|
|
|
|
|
for ( QGraphicsItem* item : m_scene->items( QRectF( scenePos - QPointF( distance, distance ), QSizeF( 2 * distance, 2 * distance ) ) ) )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
auto* port = dynamic_cast<PortItem*>( item );
|
|
|
|
|
if ( !port || port->isConfig() ) continue;
|
|
|
|
|
const QPointF delta = port->mapToScene( port->rect().center() ) - scenePos;
|
|
|
|
|
const qreal d = std::hypot( delta.x(), delta.y() );
|
|
|
|
|
if ( d <= best )
|
|
|
|
|
{
|
|
|
|
|
best = d;
|
|
|
|
|
nearest = port;
|
|
|
|
|
}
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
return nearest;
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 13:13:27 +02:00
|
|
|
RiuWorkflowGraphView::ItemRef RiuWorkflowGraphView::itemRefAt( const QPoint& viewPos ) const
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
for ( QGraphicsItem* item : items( viewPos ) )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
QGraphicsItem* current = item;
|
|
|
|
|
while ( current )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( auto* node = dynamic_cast<GraphNode*>( current ) ) return ItemRef::forTask( node->name() );
|
|
|
|
|
if ( auto* edge = dynamic_cast<GraphEdge*>( current ) )
|
|
|
|
|
{
|
|
|
|
|
if ( edge->isConfig() ) break;
|
|
|
|
|
return ItemRef::forEdge( edge->fromNode()->name(), edge->outputField(), edge->toNode()->name(), edge->inputField() );
|
|
|
|
|
}
|
|
|
|
|
current = current->parentItem();
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
/// Highlight the ports the dragged port can connect to
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::startConnectionDrag( PortItem* port )
|
|
|
|
|
{
|
|
|
|
|
m_dragPort = port;
|
|
|
|
|
m_dragInvalidReasons.clear();
|
|
|
|
|
|
|
|
|
|
for ( GraphNode* node : m_taskNodes )
|
|
|
|
|
{
|
|
|
|
|
for ( PortItem* candidate : node->ports() )
|
|
|
|
|
{
|
|
|
|
|
if ( candidate->isOutput() == port->isOutput() ) continue;
|
|
|
|
|
|
|
|
|
|
PortItem* outputPort = port->isOutput() ? port : candidate;
|
|
|
|
|
PortItem* inputPort = port->isOutput() ? candidate : port;
|
|
|
|
|
|
|
|
|
|
std::expected<void, QString> valid;
|
|
|
|
|
if ( outputPort->node() == inputPort->node() )
|
|
|
|
|
valid = std::unexpected( QString( "A task cannot be connected to itself" ) );
|
|
|
|
|
else if ( m_validator )
|
|
|
|
|
valid = m_validator( outputPort->node()->name(), outputPort->field(), inputPort->node()->name(), inputPort->field() );
|
|
|
|
|
|
|
|
|
|
if ( valid )
|
|
|
|
|
candidate->setHighlight( PortItem::Highlight::Valid );
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
candidate->setHighlight( PortItem::Highlight::Invalid, valid.error() );
|
|
|
|
|
m_dragInvalidReasons.insert( candidate, valid.error() );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_dragPath = m_scene->addPath( QPainterPath(), QPen( validTargetColor.darker( 120 ), 2.0, Qt::DashLine ) );
|
|
|
|
|
m_dragPath->setZValue( 3 );
|
|
|
|
|
updateConnectionDrag( port->mapToScene( port->rect().center() ) );
|
|
|
|
|
setCursor( Qt::CrossCursor );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-10-02 13:13:27 +02:00
|
|
|
void RiuWorkflowGraphView::updateConnectionDrag( const QPointF& scenePos )
|
2026-09-23 15:14:16 +02:00
|
|
|
{
|
2026-10-02 13:13:27 +02:00
|
|
|
if ( !m_dragPort || !m_dragPath ) return;
|
|
|
|
|
|
|
|
|
|
m_dragTarget = nullptr;
|
|
|
|
|
QPointF end = scenePos;
|
|
|
|
|
if ( PortItem* target = portAt( mapFromScene( scenePos ) );
|
|
|
|
|
target && target->isOutput() != m_dragPort->isOutput() && target->node() != m_dragPort->node() )
|
|
|
|
|
{
|
|
|
|
|
if ( !m_dragInvalidReasons.contains( target ) )
|
|
|
|
|
{
|
|
|
|
|
m_dragTarget = target;
|
|
|
|
|
end = target->mapToScene( target->rect().center() );
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
QToolTip::showText( mapToGlobal( mapFromScene( scenePos ) ), m_dragInvalidReasons.value( target ), this );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QPointF start = m_dragPort->mapToScene( m_dragPort->rect().center() );
|
|
|
|
|
QPointF from = m_dragPort->isOutput() ? start : end;
|
|
|
|
|
QPointF to = m_dragPort->isOutput() ? end : start;
|
|
|
|
|
const qreal bend = std::max( 35.0, std::abs( to.x() - from.x() ) / 2.0 );
|
|
|
|
|
QPainterPath path( from );
|
|
|
|
|
path.cubicTo( from + QPointF( bend, 0 ), to - QPointF( bend, 0 ), to );
|
|
|
|
|
m_dragPath->setPath( path );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::finishConnectionDrag( bool connect )
|
|
|
|
|
{
|
|
|
|
|
if ( !m_dragPort ) return;
|
|
|
|
|
|
|
|
|
|
PortItem* source = m_dragPort;
|
|
|
|
|
PortItem* target = m_dragTarget;
|
|
|
|
|
m_dragPort = nullptr;
|
|
|
|
|
m_dragTarget = nullptr;
|
|
|
|
|
m_dragInvalidReasons.clear();
|
|
|
|
|
if ( m_dragPath )
|
|
|
|
|
{
|
|
|
|
|
m_scene->removeItem( m_dragPath );
|
|
|
|
|
delete m_dragPath;
|
|
|
|
|
m_dragPath = nullptr;
|
|
|
|
|
}
|
|
|
|
|
for ( GraphNode* node : m_taskNodes )
|
|
|
|
|
{
|
|
|
|
|
for ( PortItem* port : node->ports() )
|
|
|
|
|
port->setHighlight( PortItem::Highlight::None );
|
|
|
|
|
}
|
|
|
|
|
unsetCursor();
|
|
|
|
|
QToolTip::hideText();
|
|
|
|
|
|
|
|
|
|
if ( !connect || !target ) return;
|
|
|
|
|
|
|
|
|
|
PortItem* outputPort = source->isOutput() ? source : target;
|
|
|
|
|
PortItem* inputPort = source->isOutput() ? target : source;
|
|
|
|
|
emit connectRequested( outputPort->node()->name(), outputPort->field(), inputPort->node()->name(), inputPort->field() );
|
2026-09-23 15:14:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
void RiuWorkflowGraphView::resizeEvent( QResizeEvent* event )
|
|
|
|
|
{
|
|
|
|
|
QGraphicsView::resizeEvent( event );
|
|
|
|
|
if ( m_fitOnResize && !m_scene->sceneRect().isEmpty() ) fitInView( m_scene->sceneRect(), Qt::KeepAspectRatio );
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
void RiuWorkflowGraphView::mousePressEvent( QMouseEvent* event )
|
|
|
|
|
{
|
|
|
|
|
if ( event->button() == Qt::LeftButton ) m_fitOnResize = false;
|
2026-10-02 13:13:27 +02:00
|
|
|
|
|
|
|
|
if ( m_editable && event->button() == Qt::LeftButton )
|
|
|
|
|
{
|
|
|
|
|
if ( PortItem* port = portAt( event->pos() ) )
|
|
|
|
|
{
|
|
|
|
|
startConnectionDrag( port );
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-23 15:14:16 +02:00
|
|
|
QGraphicsView::mousePressEvent( event );
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 13:13:27 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::mouseMoveEvent( QMouseEvent* event )
|
|
|
|
|
{
|
|
|
|
|
if ( m_dragPort )
|
|
|
|
|
{
|
|
|
|
|
updateConnectionDrag( mapToScene( event->pos() ) );
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
QGraphicsView::mouseMoveEvent( event );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::mouseReleaseEvent( QMouseEvent* event )
|
|
|
|
|
{
|
|
|
|
|
if ( m_dragPort )
|
|
|
|
|
{
|
|
|
|
|
updateConnectionDrag( mapToScene( event->pos() ) );
|
|
|
|
|
finishConnectionDrag( true );
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
QGraphicsView::mouseReleaseEvent( event );
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 16:12:01 +02:00
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
2026-09-23 15:14:16 +02:00
|
|
|
void RiuWorkflowGraphView::wheelEvent( QWheelEvent* event )
|
|
|
|
|
{
|
|
|
|
|
if ( event->modifiers() & Qt::ControlModifier )
|
|
|
|
|
{
|
|
|
|
|
m_fitOnResize = false;
|
|
|
|
|
const qreal factor = event->angleDelta().y() > 0 ? 1.15 : 1.0 / 1.15;
|
|
|
|
|
scale( factor, factor );
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
QGraphicsView::wheelEvent( event );
|
|
|
|
|
}
|
2026-10-02 13:13:27 +02:00
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::keyPressEvent( QKeyEvent* event )
|
|
|
|
|
{
|
|
|
|
|
if ( event->key() == Qt::Key_Escape && m_dragPort )
|
|
|
|
|
{
|
|
|
|
|
finishConnectionDrag( false );
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( m_editable )
|
|
|
|
|
{
|
|
|
|
|
const ItemRef selected = selectedItem();
|
|
|
|
|
if ( ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace ) && selected.kind != ItemRef::Kind::None )
|
|
|
|
|
{
|
|
|
|
|
emit deleteRequested( selected );
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if ( event->key() == Qt::Key_F2 && selected.kind == ItemRef::Kind::Task )
|
|
|
|
|
{
|
|
|
|
|
emit renameRequested( selected.task );
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
QGraphicsView::keyPressEvent( event );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::contextMenuEvent( QContextMenuEvent* event )
|
|
|
|
|
{
|
|
|
|
|
const ItemRef item = itemRefAt( event->pos() );
|
|
|
|
|
|
|
|
|
|
m_rebuilding = true;
|
|
|
|
|
m_scene->clearSelection();
|
|
|
|
|
for ( QGraphicsItem* graphicsItem : items( event->pos() ) )
|
|
|
|
|
{
|
|
|
|
|
QGraphicsItem* current = graphicsItem;
|
|
|
|
|
while ( current && !( current->flags() & QGraphicsItem::ItemIsSelectable ) )
|
|
|
|
|
current = current->parentItem();
|
|
|
|
|
if ( current )
|
|
|
|
|
{
|
|
|
|
|
current->setSelected( true );
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m_rebuilding = false;
|
|
|
|
|
onSelectionChanged();
|
|
|
|
|
|
|
|
|
|
emit contextMenuRequested( item, event->globalPos(), mapToScene( event->pos() ) );
|
|
|
|
|
event->accept();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::dragEnterEvent( QDragEnterEvent* event )
|
|
|
|
|
{
|
|
|
|
|
if ( m_editable && event->mimeData()->hasFormat( taskMimeType ) )
|
|
|
|
|
event->acceptProposedAction();
|
|
|
|
|
else
|
|
|
|
|
event->ignore();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::dragMoveEvent( QDragMoveEvent* event )
|
|
|
|
|
{
|
|
|
|
|
if ( m_editable && event->mimeData()->hasFormat( taskMimeType ) )
|
|
|
|
|
event->acceptProposedAction();
|
|
|
|
|
else
|
|
|
|
|
event->ignore();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
///
|
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|
|
|
void RiuWorkflowGraphView::dropEvent( QDropEvent* event )
|
|
|
|
|
{
|
|
|
|
|
if ( !m_editable || !event->mimeData()->hasFormat( taskMimeType ) )
|
|
|
|
|
{
|
|
|
|
|
event->ignore();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString taskId = QString::fromUtf8( event->mimeData()->data( taskMimeType ) );
|
|
|
|
|
event->acceptProposedAction();
|
|
|
|
|
m_fitOnResize = false;
|
|
|
|
|
emit addTaskRequested( taskId, mapToScene( event->position().toPoint() ) );
|
|
|
|
|
}
|