Files
ResInsight/ApplicationLibCode/ProjectDataModel/Workflow/RimWorkflowTaskInput.cpp
T
Kristian Bendiksen e6dc62a160 Workflows: Add interactive editor for taskmaestro workflows
Build, copy and edit taskmaestro workflows in ResInsight and save them as
standard workflow.yaml + input.yaml.

- Python helper: catalog, load, save and export-registered subcommands,
  and run --registered for installed workflows
- C++ definition model with pure edit operations, port type checks,
  config field derivation, static validation and undo/redo
- Installed folder listing registered workflows (read-only, runnable,
  can be duplicated)
- Graph editor dock with task palette, drag-to-connect, context menus,
  automatic layout and debounced live validation
- New/Duplicate/Save/Save As/Rescan commands; unsaved edits run from a
  temporary folder and prompt before being discarded
- Update the design document and the user README
2026-10-23 20:23:54 +02:00

263 lines
12 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// 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 "RimWorkflowTaskInput.h"
#include "RimWorkflowArrayBinding.h"
#include "RimWorkflowBoolBinding.h"
#include "RimWorkflowCaseBinding.h"
#include "RimWorkflowDateBinding.h"
#include "RimWorkflowFilePathBinding.h"
#include "RimWorkflowFloatBinding.h"
#include "RimWorkflowIntBinding.h"
#include "RimWorkflowMappingBinding.h"
#include "RimWorkflowStringBinding.h"
#include "RimWorkflowVec3Binding.h"
#include "RimWorkflowViewBinding.h"
#include "RimWorkflowWellPathBinding.h"
#include "cafPdmUiOrdering.h"
#include "cafPdmUiTextEditor.h"
#include "cafPdmUiTreeOrdering.h"
#include <QJsonArray>
CAF_PDM_SOURCE_INIT( RimWorkflowTaskInput, "WorkflowTaskInput" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimWorkflowTaskInput::RimWorkflowTaskInput()
{
CAF_PDM_InitObject( "Task", ":/Bullet.png" );
CAF_PDM_InitFieldNoDefault( &m_items, "Bindings", "" );
CAF_PDM_InitFieldNoDefault( &m_taskName, "TaskName", "Task" );
m_taskName.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitFieldNoDefault( &m_taskType, "TaskType", "Task Type" );
m_taskType.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitFieldNoDefault( &m_taskDescription, "TaskDescription", "Description" );
m_taskDescription.uiCapability()->setUiReadOnly( true );
m_taskDescription.uiCapability()->setUiEditorTypeName( caf::PdmUiTextEditor::uiEditorTypeName() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimWorkflowTaskInput::taskName() const
{
return m_taskName();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimWorkflowTaskInput::setTaskName( const QString& name )
{
m_taskName = name;
setUiName( name );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimWorkflowTaskInput::taskType() const
{
return m_taskType();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimWorkflowTaskInput::setTaskInfo( const QString& taskType, const QString& description )
{
m_taskType = taskType;
m_taskDescription = description;
}
//--------------------------------------------------------------------------------------------------
/// The class keyword of the binding used for a config field schema
//--------------------------------------------------------------------------------------------------
QString RimWorkflowTaskInput::bindingClassKeyword( const QJsonObject& fieldSchema )
{
if ( fieldSchema.value( "map_over" ).toBool() ) return RimWorkflowMappingBinding::classKeywordStatic();
const QString resinsightType = fieldSchema.value( "resinsight_type" ).toString();
if ( resinsightType == "EclipseCase" ) return RimWorkflowCaseBinding::classKeywordStatic();
if ( resinsightType == "WellPath" ) return RimWorkflowWellPathBinding::classKeywordStatic();
if ( resinsightType == "View" ) return RimWorkflowViewBinding::classKeywordStatic();
if ( resinsightType == "Vec3" ) return RimWorkflowVec3Binding::classKeywordStatic();
const QString type = fieldSchema.value( "type" ).toString( "string" );
const QString format = fieldSchema.value( "format" ).toString();
if ( type == "string" && format == "date" ) return RimWorkflowDateBinding::classKeywordStatic();
if ( type == "string" && ( format == "path" || format == "file-path" || format == "directory-path" ) )
return RimWorkflowFilePathBinding::classKeywordStatic();
if ( type == "boolean" ) return RimWorkflowBoolBinding::classKeywordStatic();
if ( type == "integer" ) return RimWorkflowIntBinding::classKeywordStatic();
if ( type == "number" ) return RimWorkflowFloatBinding::classKeywordStatic();
if ( type == "array" ) return RimWorkflowArrayBinding::classKeywordStatic();
return RimWorkflowStringBinding::classKeywordStatic();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimWorkflowFieldBinding* RimWorkflowTaskInput::createBinding( const QJsonObject& fieldSchema )
{
const QString keyword = bindingClassKeyword( fieldSchema );
if ( keyword == RimWorkflowCaseBinding::classKeywordStatic() ) return new RimWorkflowCaseBinding;
if ( keyword == RimWorkflowWellPathBinding::classKeywordStatic() ) return new RimWorkflowWellPathBinding;
if ( keyword == RimWorkflowViewBinding::classKeywordStatic() ) return new RimWorkflowViewBinding;
if ( keyword == RimWorkflowVec3Binding::classKeywordStatic() ) return new RimWorkflowVec3Binding;
if ( keyword == RimWorkflowDateBinding::classKeywordStatic() ) return new RimWorkflowDateBinding;
if ( keyword == RimWorkflowFilePathBinding::classKeywordStatic() ) return new RimWorkflowFilePathBinding;
if ( keyword == RimWorkflowBoolBinding::classKeywordStatic() ) return new RimWorkflowBoolBinding;
if ( keyword == RimWorkflowIntBinding::classKeywordStatic() ) return new RimWorkflowIntBinding;
if ( keyword == RimWorkflowFloatBinding::classKeywordStatic() ) return new RimWorkflowFloatBinding;
if ( keyword == RimWorkflowArrayBinding::classKeywordStatic() ) return new RimWorkflowArrayBinding;
if ( keyword == RimWorkflowMappingBinding::classKeywordStatic() ) return new RimWorkflowMappingBinding;
return new RimWorkflowStringBinding;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimWorkflowTaskInput::detachedKey( const QString& fieldName, const QString& classKeyword )
{
return fieldName + "/" + classKeyword;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimWorkflowTaskInput::buildFromSchema( const QJsonArray& configFields )
{
QMap<QString, QJsonValue> noDetachedValues;
deleteAllItems();
syncFromSchema( configFields, noDetachedValues );
}
//--------------------------------------------------------------------------------------------------
/// Update the bindings to a new list of config fields. A binding is kept with its value when the
/// field still has the same kind of binding. Values of removed bindings are moved to
/// `detachedValues`, and restored from there when the field comes back.
//--------------------------------------------------------------------------------------------------
void RimWorkflowTaskInput::syncFromSchema( const QJsonArray& configFields, QMap<QString, QJsonValue>& detachedValues )
{
std::vector<RimWorkflowFieldBinding*> existing = items();
m_items.clearWithoutDelete();
for ( const QJsonValue& value : configFields )
{
QJsonObject schema = value.toObject();
const QString field = schema.value( "name" ).toString();
const QString keyword = bindingClassKeyword( schema );
RimWorkflowFieldBinding* binding = nullptr;
for ( auto it = existing.begin(); it != existing.end(); ++it )
{
if ( *it && ( *it )->fieldName() == field && ( *it )->classKeyword() == keyword )
{
binding = *it;
existing.erase( it );
break;
}
}
if ( binding )
{
const QJsonValue current = binding->toJsonValue();
// Keep a cleared value cleared instead of restoring the default
if ( current.isNull() )
schema.remove( "default" );
else
schema["default"] = current;
}
else
{
binding = createBinding( schema );
const QString key = detachedKey( field, keyword );
if ( detachedValues.contains( key ) ) schema["default"] = detachedValues.take( key );
}
binding->applySchema( schema );
addItem( binding );
}
for ( RimWorkflowFieldBinding* removed : existing )
{
if ( !removed ) continue;
const QJsonValue value = removed->toJsonValue();
if ( !value.isNull() ) detachedValues.insert( detachedKey( removed->fieldName(), removed->classKeyword() ), value );
delete removed;
}
}
//--------------------------------------------------------------------------------------------------
/// All values that are set, including references to ResInsight objects
//--------------------------------------------------------------------------------------------------
QJsonObject RimWorkflowTaskInput::jsonValues() const
{
QJsonObject values;
for ( const RimWorkflowFieldBinding* binding : items() )
{
const QJsonValue value = binding->toJsonValue();
if ( !value.isNull() ) values.insert( binding->fieldName(), value );
}
return values;
}
//--------------------------------------------------------------------------------------------------
/// Values that can be stored in input.yaml, without references to objects in the project
//--------------------------------------------------------------------------------------------------
QJsonObject RimWorkflowTaskInput::literalValues() const
{
QJsonObject values;
for ( const RimWorkflowFieldBinding* binding : items() )
{
if ( binding->isObjectReference() ) continue;
const QJsonValue value = binding->toJsonValue();
if ( !value.isNull() ) values.insert( binding->fieldName(), value );
}
return values;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimWorkflowTaskInput::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
if ( !m_taskType().isEmpty() ) uiOrdering.add( &m_taskType );
if ( !m_taskDescription().isEmpty() ) uiOrdering.add( &m_taskDescription );
for ( RimWorkflowFieldBinding* binding : items() )
{
if ( binding && binding->valueField() ) uiOrdering.add( binding->valueField() );
}
uiOrdering.skipRemainingFields( true );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimWorkflowTaskInput::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName )
{
uiTreeOrdering.skipRemainingChildren( true );
}