Workflows: Show ResInsight object icons on graph ports

Ports holding a ResInsight object (instance, case, view, well path, surface)
show the project tree icon of that object in front of the type. The rips class
is taken from the schema's Python bases, directly or via ObjectModel[Class].
This commit is contained in:
Kristian Bendiksen committed 2026-10-21 11:19:35 +02:00
1 parent a47a871fea
commit d994e2c95e
7 files changed
+172 -47

No files matched your search

@@ -218,6 +218,7 @@ QJsonArray inputPortsJson( const RimWorkflowDefinition& definit
wired = wired || edge.input == port.name;
result.append( QJsonObject{ { "name", port.name },
{ "type", port.typeName },
{ "icon", port.iconResource },
{ "description", port.description },
{ "required", port.required },
{ "configurable", port.configurable },
@@ -236,8 +237,11 @@ QJsonArray outputPortsJson( const std::vector<RimWorkflowPort>& ports, const std
bool wired = false;
for ( const auto& edge : outgoing )
wired = wired || edge.output == port.name;
result.append(
QJsonObject{ { "name", port.name }, { "type", port.typeName }, { "description", port.description }, { "wired", wired } } );
result.append( QJsonObject{ { "name", port.name },
{ "type", port.typeName },
{ "icon", port.iconResource },
{ "description", port.description },
{ "wired", wired } } );
}
return result;
}
@@ -288,6 +292,8 @@ QJsonObject graphTask( const RimWorkflowDefinition& definition,
{ "output_ports", outputPortsJson( outputPorts, definition.outgoingEdges( node.name ) ) },
{ "input_types", RimWorkflowPortCompatibility::portTypes( inputPorts ) },
{ "output_types", RimWorkflowPortCompatibility::portTypes( outputPorts ) },
{ "input_icons", RimWorkflowPortCompatibility::portIcons( inputPorts ) },
{ "output_icons", RimWorkflowPortCompatibility::portIcons( outputPorts ) },
{ "is_result", node.name == resultTask },
{ "issues", taskIssues } };
}
@@ -120,7 +120,9 @@ QJsonObject taskFromDescribe( const QJsonObject& task )
{ "outputs", outputs },
{ "config_fields", configFields },
{ "input_types", RimWorkflowPortCompatibility::portTypes( RimWorkflowPortCompatibility::inputPorts( task ) ) },
{ "output_types", RimWorkflowPortCompatibility::portTypes( RimWorkflowPortCompatibility::outputPorts( task ) ) } };
{ "output_types", RimWorkflowPortCompatibility::portTypes( RimWorkflowPortCompatibility::outputPorts( task ) ) },
{ "input_icons", RimWorkflowPortCompatibility::portIcons( RimWorkflowPortCompatibility::inputPorts( task ) ) },
{ "output_icons", RimWorkflowPortCompatibility::portIcons( RimWorkflowPortCompatibility::outputPorts( task ) ) } };
}
} // namespace
@@ -23,6 +23,7 @@
#include <QJsonArray>
#include <QMap>
#include <QRegularExpression>
#include <QSet>
#include <QStringList>
@@ -155,12 +156,13 @@ std::vector<RimWorkflowPort> RimWorkflowPortCompatibility::inputPorts( const QJs
const QJsonObject inputSchema = taskType.value( "input_schema" ).toObject();
std::vector<RimWorkflowPort> ports;
ports.push_back( { .name = "",
.schema = inputSchema,
.rootSchema = inputSchema,
.description = inputSchema.value( "description" ).toString(),
.typeName = typeName( inputSchema, inputSchema ),
.required = true } );
ports.push_back( { .name = "",
.schema = inputSchema,
.rootSchema = inputSchema,
.description = inputSchema.value( "description" ).toString(),
.typeName = typeName( inputSchema, inputSchema ),
.iconResource = iconResource( inputSchema, inputSchema ),
.required = true } );
const QStringList required = RimWorkflowSchemaTools::requiredFields( inputSchema );
const QJsonObject properties = inputSchema.value( "properties" ).toObject();
@@ -173,6 +175,7 @@ std::vector<RimWorkflowPort> RimWorkflowPortCompatibility::inputPorts( const QJs
.rootSchema = inputSchema,
.description = property.value( "description" ).toString(),
.typeName = typeName( property, inputSchema ),
.iconResource = iconResource( property, inputSchema ),
.required = required.contains( name ),
.configurable = RimWorkflowSchemaTools::isConfigurable( property, inputSchema ) } );
}
@@ -187,22 +190,24 @@ std::vector<RimWorkflowPort> RimWorkflowPortCompatibility::outputPorts( const QJ
const QJsonObject outputSchema = taskType.value( "output_schema" ).toObject();
std::vector<RimWorkflowPort> ports;
ports.push_back( { .name = "",
.schema = outputSchema,
.rootSchema = outputSchema,
.description = outputSchema.value( "description" ).toString(),
.typeName = typeName( outputSchema, outputSchema ) } );
ports.push_back( { .name = "",
.schema = outputSchema,
.rootSchema = outputSchema,
.description = outputSchema.value( "description" ).toString(),
.typeName = typeName( outputSchema, outputSchema ),
.iconResource = iconResource( outputSchema, outputSchema ) } );
const QJsonObject properties = outputSchema.value( "properties" ).toObject();
for ( const QString& name : RimWorkflowSchemaTools::propertyNames( outputSchema ) )
{
const QJsonObject property = properties.value( name ).toObject();
if ( isHiddenObjectValue( name, property, outputSchema ) ) continue;
ports.push_back( { .name = name,
.schema = property,
.rootSchema = outputSchema,
.description = property.value( "description" ).toString(),
.typeName = typeName( property, outputSchema ) } );
ports.push_back( { .name = name,
.schema = property,
.rootSchema = outputSchema,
.description = property.value( "description" ).toString(),
.typeName = typeName( property, outputSchema ),
.iconResource = iconResource( property, outputSchema ) } );
}
return ports;
}
@@ -346,6 +351,63 @@ QString RimWorkflowPortCompatibility::typeName( const QJsonObject& schema, const
return pythonNames.value( type, type );
}
//--------------------------------------------------------------------------------------------------
/// Icon of the ResInsight object a schema holds, or an empty string. The rips class is found in the
/// Python class bases: directly for opaque rips objects, or as `ObjectModel[Class]` for wrappers.
/// The icons are the ones the matching ResInsight project classes use.
//--------------------------------------------------------------------------------------------------
QString RimWorkflowPortCompatibility::iconResource( const QJsonObject& schema, const QJsonObject& rootSchema )
{
const QJsonObject resolved = RimWorkflowSchemaTools::resolveReferences( schema, rootSchema );
for ( const char* unionKey : { "anyOf", "oneOf" } )
{
for ( const QJsonValue& option : resolved.value( unionKey ).toArray() )
{
const QString icon = iconResource( option.toObject(), rootSchema );
if ( !icon.isEmpty() ) return icon;
}
}
if ( resolved.value( "type" ).toString() == "array" ) return iconResource( resolved.value( "items" ).toObject(), rootSchema );
static const QMap<QString, QString> icons = { { "Instance", ":/AppLogo48x48.png" },
{ "Case", ":/Case48x48.png" },
{ "Reservoir", ":/Case48x48.png" },
{ "EclipseCase", ":/Case48x48.png" },
{ "View", ":/3DView16x16.png" },
{ "EclipseView", ":/3DView16x16.png" },
{ "WellPath", ":/Well.svg" },
{ "Surface", ":/ReservoirSurface16x16.png" } };
QJsonArray bases = resolved.value( "x-ri-python-bases" ).toArray();
if ( bases.isEmpty() && resolved.contains( "x-ri-python-type" ) ) bases.append( resolved.value( "x-ri-python-type" ) );
static const QRegularExpression objectModel( R"(ObjectModel\[(\w+)\]$)" );
for ( const QJsonValue& value : bases )
{
const QString base = value.toString();
QString className;
if ( const auto match = objectModel.match( base ); match.hasMatch() )
className = match.captured( 1 );
else if ( base.startsWith( "rips." ) )
className = base.section( '.', -1 );
if ( icons.contains( className ) ) return icons.value( className );
}
return {};
}
//--------------------------------------------------------------------------------------------------
/// Port name -> icon resource, for the ports that hold a ResInsight object
//--------------------------------------------------------------------------------------------------
QJsonObject RimWorkflowPortCompatibility::portIcons( const std::vector<RimWorkflowPort>& ports )
{
QJsonObject icons;
for ( const auto& port : ports )
{
if ( !port.iconResource.isEmpty() ) icons[port.name] = port.iconResource;
}
return icons;
}
//--------------------------------------------------------------------------------------------------
/// Port name -> type name, with an empty name for the whole model. Used to label ports in the graph.
//--------------------------------------------------------------------------------------------------
@@ -37,6 +37,7 @@ struct RimWorkflowPort
QJsonObject rootSchema{};
QString description{};
QString typeName{};
QString iconResource{};
bool required = true;
bool configurable = false;
@@ -58,7 +59,9 @@ bool isCompatible( const QJsonObject& produced, const QJsonObject& producedRoot,
bool isCompatible( const RimWorkflowPort& produced, const RimWorkflowPort& expected );
bool isAny( const QJsonObject& schema, const QJsonObject& rootSchema );
QString typeName( const QJsonObject& schema, const QJsonObject& rootSchema );
QString iconResource( const QJsonObject& schema, const QJsonObject& rootSchema );
QJsonObject portTypes( const std::vector<RimWorkflowPort>& ports );
QJsonObject portIcons( const std::vector<RimWorkflowPort>& ports );
QStringList coveredByWholeOutput( const QJsonObject& upstreamTaskType, const QJsonObject& downstreamTaskType );
bool wouldCreateCycle( const RimWorkflowDefinition& definition, const QString& from, const QString& to );
@@ -163,6 +163,25 @@ TEST( RimWorkflowDefinition, graphFromDefinition )
EXPECT_EQ( "RipsInstance", exportTask.value( "input_types" ).toObject().value( "resinsight" ).toString() );
EXPECT_EQ( "str", exportTask.value( "output_types" ).toObject().value( "export_file" ).toString() );
// Icons of ResInsight objects, found through `ObjectModel[Class]` and rips bases
const QJsonObject exportIcons = exportTask.value( "input_icons" ).toObject();
EXPECT_EQ( ":/AppLogo48x48.png", exportIcons.value( "resinsight" ).toString() );
EXPECT_FALSE( exportTask.value( "output_icons" ).toObject().contains( "export_file" ) );
EXPECT_EQ( ":/Case48x48.png", taskByName( graph, "select_eclipse_case" ).value( "output_icons" ).toObject().value( "" ).toString() );
EXPECT_EQ( ":/Well.svg", taskByName( graph, "select_well_path_1" ).value( "output_icons" ).toObject().value( "" ).toString() );
EXPECT_EQ( ":/Case48x48.png",
RimWorkflowPortCompatibility::iconResource( QJsonObject{ { "x-ri-python-type", "rips.generated.generated_classes.Reservoir" } },
{} ) );
EXPECT_EQ( ":/3DView16x16.png",
RimWorkflowPortCompatibility::iconResource( QJsonObject{ { "anyOf",
QJsonArray{ QJsonObject{ { "type", "null" } },
QJsonObject{ { "x-ri-python-bases",
QJsonArray{ "pkg.GridView",
"taskmaestro.object_model."
"ObjectModel["
"View]" } } } } } },
{} ) );
// The connect task's opaque `value` is not a port of its own
const QJsonObject connect = taskByName( graph, "connect_to_resinsight" );
EXPECT_TRUE( connect.value( "outputs" ).toArray().isEmpty() );
@@ -23,10 +23,12 @@
#include <QFontMetrics>
#include <QGraphicsEllipseItem>
#include <QGraphicsPathItem>
#include <QGraphicsPixmapItem>
#include <QGraphicsPolygonItem>
#include <QGraphicsRectItem>
#include <QGraphicsScene>
#include <QGraphicsTextItem>
#include <QIcon>
#include <QJsonValue>
#include <QKeyEvent>
#include <QMimeData>
@@ -54,6 +56,7 @@ constexpr qreal configGap = 110.0;
constexpr qreal rowGap = 55.0;
constexpr qreal portRadius = 5.0;
constexpr qreal portHitDist = 14.0;
constexpr int iconSize = 12;
const QColor taskBorder( 60, 85, 115 );
const QColor taskBackground( 226, 238, 250 );
@@ -123,6 +126,7 @@ struct PortInfo
QString label;
QString toolTip;
QString typeName{};
QString iconResource{};
Style style = Style::Normal;
};
@@ -373,14 +377,15 @@ void GraphNode::addPorts( const std::vector<PortInfo>& ports, bool output )
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();
auto addText = [&]( const QString& content, const QFont& font, const QColor& color, qreal top )
auto addText = [&]( const QString& content, const QFont& font, const QColor& color, qreal top, qreal indent = 0.0 )
{
auto* text = new QGraphicsTextItem( QFontMetrics( font ).elidedText( content, Qt::ElideRight, textWidth ), this );
auto* text = new QGraphicsTextItem( QFontMetrics( font ).elidedText( content, Qt::ElideRight, textWidth - indent ), this );
text->setFont( font );
text->setToolTip( toolTip );
text->setDefaultTextColor( color );
text->setPos( output ? m_width - text->boundingRect().width() - 12 : 10, top );
text->setPos( output ? m_width - text->boundingRect().width() - 12 : 10 + indent, top );
text->setAcceptedMouseButtons( Qt::NoButton );
return text;
};
QFont labelFont;
@@ -394,7 +399,21 @@ void GraphNode::addPorts( const std::vector<PortInfo>& ports, bool output )
QFont typeFont;
typeFont.setPointSize( 7 );
typeFont.setItalic( true );
addText( info.typeName, typeFont, QColor( 110, 120, 135 ), y - 3 );
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 );
}
}
}
}
@@ -705,9 +724,10 @@ std::pair<std::vector<PortInfo>, std::vector<PortInfo>> editModePorts( const QJs
}
PortInfo info;
info.key = keyForField( field );
info.label = field.isEmpty() ? "input" : field;
info.typeName = port.value( "type" ).toString();
info.key = keyForField( field );
info.label = field.isEmpty() ? "input" : field;
info.typeName = port.value( "type" ).toString();
info.iconResource = port.value( "icon" ).toString();
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" );
@@ -723,10 +743,11 @@ std::pair<std::vector<PortInfo>, std::vector<PortInfo>> editModePorts( const QJs
if ( edge.value( "to" ).toString() == name && edge.value( "input" ).toString().isEmpty() && !hasWholeInput )
{
inputs.insert( inputs.begin(),
PortInfo{ .key = "model",
.label = "input",
.toolTip = {},
.typeName = task.value( "input_types" ).toObject().value( "" ).toString() } );
PortInfo{ .key = "model",
.label = "input",
.toolTip = {},
.typeName = task.value( "input_types" ).toObject().value( "" ).toString(),
.iconResource = task.value( "input_icons" ).toObject().value( "" ).toString() } );
hasWholeInput = true;
}
}
@@ -737,36 +758,39 @@ std::pair<std::vector<PortInfo>, std::vector<PortInfo>> editModePorts( const QJs
const QJsonObject port = value.toObject();
const QString field = port.value( "name" ).toString();
PortInfo info;
info.key = keyForField( field );
info.label = field.isEmpty() ? "output" : field;
info.typeName = port.value( "type" ).toString();
info.key = keyForField( field );
info.label = field.isEmpty() ? "output" : field;
info.typeName = port.value( "type" ).toString();
info.iconResource = port.value( "icon" ).toString();
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 );
}
if ( outputs.empty() )
outputs.push_back( PortInfo{ .key = "model",
.label = "output",
.toolTip = {},
.typeName = task.value( "output_types" ).toObject().value( "" ).toString() } );
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() } );
return { inputs, outputs };
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<PortInfo> portInfos( const QStringList& keys, bool output, const QJsonObject& types )
std::vector<PortInfo> portInfos( const QStringList& keys, bool output, const QJsonObject& types, const QJsonObject& icons )
{
std::vector<PortInfo> result;
for ( const QString& key : keys )
{
const QString typeName = types.value( fieldForKey( key ) ).toString();
const QString label = key == "model" ? ( output ? "output" : "input" ) : fieldForKey( key );
result.push_back( PortInfo{ .key = key,
.label = label,
.toolTip = typeName.isEmpty() ? label : QString( "%1: %2" ).arg( label, typeName ),
.typeName = typeName } );
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() } );
}
return result;
}
@@ -963,8 +987,8 @@ void RiuWorkflowGraphView::showGraph( const QJsonObject& graph, const QString& e
out.removeDuplicates();
in.sort();
out.sort();
inputs = portInfos( in, false, data.value( "input_types" ).toObject() );
outputs = portInfos( out, true, data.value( "output_types" ).toObject() );
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() );
}
auto* node = new GraphNode( name, inputs, outputs );
@@ -984,8 +1008,13 @@ void RiuWorkflowGraphView::showGraph( const QJsonObject& graph, const QString& e
if ( !configFields.value( name ).isEmpty() )
{
auto* configNode =
new GraphNode( name, {}, portInfos( configFields.value( name ), true, data.value( "input_types" ).toObject() ), true );
auto* configNode = new GraphNode( name,
{},
portInfos( configFields.value( name ),
true,
data.value( "input_types" ).toObject(),
data.value( "input_icons" ).toObject() ),
true );
m_scene->addItem( configNode );
m_configNodes.insert( name, configNode );
}
+5 -1
View File
@@ -156,7 +156,11 @@ in a central dock tab:
Each port shows its name with its type below it in smaller text (from the
task schemas, as `input_types`/`output_types` in the graph JSON). The
tooltip also shows the description.
tooltip also shows the description. Ports that hold a ResInsight object
(instance, case, view, well path, surface) show that object's project tree
icon in front of the type (`input_icons`/`output_icons`). The rips class is
found in the `x-ri-python-bases`, either directly or as `ObjectModel[Class]`
(`RimWorkflowPortCompatibility::iconResource`).
`RiuWorkflowGraphLayout` computes the layout: topological ranks with a Kahn
sort and longest-path ranks, then one barycenter pass to order nodes. It