diff --git a/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowDefinitionTools.cpp b/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowDefinitionTools.cpp index 82e68561e3..b2c14ca3ca 100644 --- a/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowDefinitionTools.cpp +++ b/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowDefinitionTools.cpp @@ -286,6 +286,8 @@ QJsonObject graphTask( const RimWorkflowDefinition& definition, { "whole_input", inputPorts.size() == 1 && !inputSchema.value( "properties" ).toObject().isEmpty() }, { "input_ports", inputPortsJson( definition, node, inputPorts, definition.incomingEdges( node.name ) ) }, { "output_ports", outputPortsJson( outputPorts, definition.outgoingEdges( node.name ) ) }, + { "input_types", RimWorkflowPortCompatibility::portTypes( inputPorts ) }, + { "output_types", RimWorkflowPortCompatibility::portTypes( outputPorts ) }, { "is_result", node.name == resultTask }, { "issues", taskIssues } }; } diff --git a/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowDescribeTools.cpp b/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowDescribeTools.cpp index db4b897b9c..41a3b9bc25 100644 --- a/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowDescribeTools.cpp +++ b/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowDescribeTools.cpp @@ -18,6 +18,7 @@ #include "RimWorkflowDescribeTools.h" +#include "RimWorkflowPortCompatibility.h" #include "RimWorkflowSchemaTools.h" #include @@ -117,7 +118,9 @@ QJsonObject taskFromDescribe( const QJsonObject& task ) return QJsonObject{ { "name", task.value( "name" ).toString() }, { "inputs", inputs }, { "outputs", outputs }, - { "config_fields", configFields } }; + { "config_fields", configFields }, + { "input_types", RimWorkflowPortCompatibility::portTypes( RimWorkflowPortCompatibility::inputPorts( task ) ) }, + { "output_types", RimWorkflowPortCompatibility::portTypes( RimWorkflowPortCompatibility::outputPorts( task ) ) } }; } } // namespace diff --git a/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowPortCompatibility.cpp b/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowPortCompatibility.cpp index a6e543e2af..7eaf1ebdfe 100644 --- a/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowPortCompatibility.cpp +++ b/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowPortCompatibility.cpp @@ -346,6 +346,17 @@ QString RimWorkflowPortCompatibility::typeName( const QJsonObject& schema, const return pythonNames.value( type, type ); } +//-------------------------------------------------------------------------------------------------- +/// Port name -> type name, with an empty name for the whole model. Used to label ports in the graph. +//-------------------------------------------------------------------------------------------------- +QJsonObject RimWorkflowPortCompatibility::portTypes( const std::vector& ports ) +{ + QJsonObject types; + for ( const auto& port : ports ) + types[port.name] = port.typeName; + return types; +} + //-------------------------------------------------------------------------------------------------- /// Input fields of the downstream task supplied when it takes the whole upstream output //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowPortCompatibility.h b/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowPortCompatibility.h index 21e82c9874..3fc253cf28 100644 --- a/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowPortCompatibility.h +++ b/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowPortCompatibility.h @@ -55,9 +55,10 @@ std::optional inputPort( const QJsonObject& taskType, const QSt std::optional outputPort( const QJsonObject& taskType, const QString& name ); bool isCompatible( const QJsonObject& produced, const QJsonObject& producedRoot, const QJsonObject& expected, const QJsonObject& expectedRoot ); -bool isCompatible( const RimWorkflowPort& produced, const RimWorkflowPort& expected ); -bool isAny( const QJsonObject& schema, const QJsonObject& rootSchema ); -QString typeName( const QJsonObject& schema, const QJsonObject& rootSchema ); +bool isCompatible( const RimWorkflowPort& produced, const RimWorkflowPort& expected ); +bool isAny( const QJsonObject& schema, const QJsonObject& rootSchema ); +QString typeName( const QJsonObject& schema, const QJsonObject& rootSchema ); +QJsonObject portTypes( const std::vector& ports ); QStringList coveredByWholeOutput( const QJsonObject& upstreamTaskType, const QJsonObject& downstreamTaskType ); bool wouldCreateCycle( const RimWorkflowDefinition& definition, const QString& from, const QString& to ); diff --git a/ApplicationLibCode/UnitTests/RimWorkflowDefinition-Test.cpp b/ApplicationLibCode/UnitTests/RimWorkflowDefinition-Test.cpp index af50463239..81d9918452 100644 --- a/ApplicationLibCode/UnitTests/RimWorkflowDefinition-Test.cpp +++ b/ApplicationLibCode/UnitTests/RimWorkflowDefinition-Test.cpp @@ -159,6 +159,10 @@ TEST( RimWorkflowDefinition, graphFromDefinition ) EXPECT_EQ( "resinsight", ports[1].toObject().value( "name" ).toString() ); EXPECT_TRUE( ports[1].toObject().value( "wired" ).toBool() ); + // Type names for labelling the ports + EXPECT_EQ( "RipsInstance", exportTask.value( "input_types" ).toObject().value( "resinsight" ).toString() ); + EXPECT_EQ( "str", exportTask.value( "output_types" ).toObject().value( "export_file" ).toString() ); + // 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() ); diff --git a/ApplicationLibCode/UnitTests/RimWorkflowDescribeTools-Test.cpp b/ApplicationLibCode/UnitTests/RimWorkflowDescribeTools-Test.cpp index 39be32dbee..0addc96d8b 100644 --- a/ApplicationLibCode/UnitTests/RimWorkflowDescribeTools-Test.cpp +++ b/ApplicationLibCode/UnitTests/RimWorkflowDescribeTools-Test.cpp @@ -82,6 +82,8 @@ TEST( RimWorkflowDescribeTools, configFieldTypesAndValues ) EXPECT_EQ( "Person to greet", name.value( "description" ).toString() ); EXPECT_FALSE( name.value( "required" ).toBool() ); EXPECT_EQ( "world", fieldByName( greetB, "name" ).value( "default" ).toString() ); + EXPECT_EQ( "str", greetA.value( "input_types" ).toObject().value( "name" ).toString() ); + EXPECT_EQ( "int", greetA.value( "input_types" ).toObject().value( "times" ).toString() ); const QJsonObject times = fieldByName( greetA, "times" ); EXPECT_EQ( "integer", times.value( "type" ).toString() ); diff --git a/ApplicationLibCode/UserInterface/RiuWorkflowGraphView.cpp b/ApplicationLibCode/UserInterface/RiuWorkflowGraphView.cpp index 9fdfec39f0..aadec8f6b0 100644 --- a/ApplicationLibCode/UserInterface/RiuWorkflowGraphView.cpp +++ b/ApplicationLibCode/UserInterface/RiuWorkflowGraphView.cpp @@ -122,6 +122,7 @@ struct PortInfo QString key; QString label; QString toolTip; + QString typeName{}; Style style = Style::Normal; }; @@ -369,15 +370,32 @@ void GraphNode::addPorts( const std::vector& ports, bool output ) auto* port = new PortItem( this, info, output, m_isConfig, QPointF( x, y ) ); portMap.insert( info.key, port ); - QFont font; - font.setPointSize( 9 ); - const QString display = QFontMetrics( font ).elidedText( info.label, Qt::ElideRight, m_isConfig ? m_width - 24 : m_width / 2 - 18 ); - auto* text = new QGraphicsTextItem( display, this ); - text->setFont( font ); - text->setToolTip( info.toolTip.isEmpty() ? info.label : info.toolTip ); - text->setDefaultTextColor( info.style == PortInfo::Style::Missing ? missingPortColor : QColor( 45, 65, 80 ) ); - text->setPos( output ? m_width - text->boundingRect().width() - 12 : 10, y - ( m_isConfig || !output ? 20 : 13 ) ); - text->setAcceptedMouseButtons( Qt::NoButton ); + 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* text = new QGraphicsTextItem( QFontMetrics( font ).elidedText( content, Qt::ElideRight, textWidth ), this ); + text->setFont( font ); + text->setToolTip( toolTip ); + text->setDefaultTextColor( color ); + text->setPos( output ? m_width - text->boundingRect().width() - 12 : 10, top ); + text->setAcceptedMouseButtons( Qt::NoButton ); + }; + + 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 ); + addText( info.typeName, typeFont, QColor( 110, 120, 135 ), y - 3 ); + } } } @@ -681,8 +699,9 @@ std::pair, std::vector> editModePorts( const QJs if ( field.isEmpty() ) hasWholeInput = true; PortInfo info; - info.key = keyForField( field ); - info.label = field.isEmpty() ? "input" : field; + info.key = keyForField( field ); + info.label = field.isEmpty() ? "input" : field; + info.typeName = port.value( "type" ).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" ); @@ -697,7 +716,11 @@ std::pair, std::vector> editModePorts( const QJs const QJsonObject edge = value.toObject(); if ( edge.value( "to" ).toString() == name && edge.value( "input" ).toString().isEmpty() && !hasWholeInput ) { - inputs.insert( inputs.begin(), PortInfo{ .key = "model", .label = "input", .toolTip = {} } ); + inputs.insert( inputs.begin(), + PortInfo{ .key = "model", + .label = "input", + .toolTip = {}, + .typeName = task.value( "input_types" ).toObject().value( "" ).toString() } ); hasWholeInput = true; } } @@ -708,25 +731,37 @@ std::pair, std::vector> 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.key = keyForField( field ); + info.label = field.isEmpty() ? "output" : field; + info.typeName = port.value( "type" ).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 = {} } ); + if ( outputs.empty() ) + outputs.push_back( PortInfo{ .key = "model", + .label = "output", + .toolTip = {}, + .typeName = task.value( "output_types" ).toObject().value( "" ).toString() } ); return { inputs, outputs }; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -std::vector portInfos( const QStringList& keys, bool output ) +std::vector portInfos( const QStringList& keys, bool output, const QJsonObject& types ) { std::vector result; for ( const QString& key : keys ) - result.push_back( PortInfo{ .key = key, .label = key == "model" ? ( output ? "output" : "input" ) : key.mid( 6 ), .toolTip = {} } ); + { + 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 } ); + } return result; } } // namespace @@ -922,8 +957,8 @@ void RiuWorkflowGraphView::showGraph( const QJsonObject& graph, const QString& e out.removeDuplicates(); in.sort(); out.sort(); - inputs = portInfos( in, false ); - outputs = portInfos( out, true ); + inputs = portInfos( in, false, data.value( "input_types" ).toObject() ); + outputs = portInfos( out, true, data.value( "output_types" ).toObject() ); } auto* node = new GraphNode( name, inputs, outputs ); @@ -943,7 +978,8 @@ void RiuWorkflowGraphView::showGraph( const QJsonObject& graph, const QString& e if ( !configFields.value( name ).isEmpty() ) { - auto* configNode = new GraphNode( name, {}, portInfos( configFields.value( name ), true ), true ); + auto* configNode = + new GraphNode( name, {}, portInfos( configFields.value( name ), true, data.value( "input_types" ).toObject() ), true ); m_scene->addItem( configNode ); m_configNodes.insert( name, configNode ); } diff --git a/docs/workflow-ui-design.md b/docs/workflow-ui-design.md index 79f57a0f80..5f31b772cd 100644 --- a/docs/workflow-ui-design.md +++ b/docs/workflow-ui-design.md @@ -154,6 +154,10 @@ in a central dock tab: - a status line for load errors, read-only reasons, validation results and unsaved changes. +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. + `RiuWorkflowGraphLayout` computes the layout: topological ranks with a Kahn sort and longest-path ranks, then one barycenter pass to order nodes. It tolerates cycles. The layout is always automatic; nodes moved by hand keep