mirror of
https://github.com/OPM/ResInsight.git
synced 2026-11-13 09:37:29 -06:00
Add a per-workflow Vertical Layout toggle to the workflow editor toolbar and canvas context menu. In vertical mode ranks are stacked top to bottom, task inputs sit on the top edge and outputs on the bottom edge, config boxes are placed above their task and control edges run between the sides.
1169 lines
50 KiB
C++
1169 lines
50 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 "RimWorkflow.h"
|
|
|
|
#include "RimWorkflowDefinitionTools.h"
|
|
#include "RimWorkflowDescribeTools.h"
|
|
#include "RimWorkflowHelperProcess.h"
|
|
#include "RimWorkflowJob.h"
|
|
#include "RimWorkflowValidationTools.h"
|
|
|
|
#include "RiaLogging.h"
|
|
#include "RiaPreferences.h"
|
|
|
|
#include "RiuMainWindow.h"
|
|
|
|
#include "cafCmdFeatureMenuBuilder.h"
|
|
#include "cafPdmUiOrdering.h"
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QStandardPaths>
|
|
#include <QTemporaryDir>
|
|
|
|
#include <algorithm>
|
|
|
|
CAF_PDM_SOURCE_INIT( RimWorkflow, "Workflow" );
|
|
|
|
namespace
|
|
{
|
|
constexpr size_t maxUndoSteps = 100;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
RimWorkflow::RimWorkflow()
|
|
{
|
|
CAF_PDM_InitObject( "Workflow", ":/Folder.png" );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_name, "Name", "Name" );
|
|
m_name.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_description, "Description", "Description" );
|
|
m_description.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_workflowDirectory, "WorkflowDirectory", "Directory" );
|
|
m_workflowDirectory.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_registeredId, "RegisteredId", "Registered Id" );
|
|
m_registeredId.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitField( &m_editable, "Editable", false, "Editable" );
|
|
m_editable.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_readOnlyReason, "ReadOnlyReason", "Read-only Because" );
|
|
m_readOnlyReason.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_loadError, "LoadError", "Load Error" );
|
|
m_loadError.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_jobs, "Jobs", "" );
|
|
|
|
CAF_PDM_InitField( &m_inlineConfigValues, "InlineConfigValues", true, "Inline Config Values" );
|
|
m_inlineConfigValues.uiCapability()->setUiHidden( true );
|
|
|
|
CAF_PDM_InitField( &m_verticalLayout, "VerticalLayout", false, "Vertical Layout" );
|
|
m_verticalLayout.uiCapability()->setUiHidden( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_hiddenTasks, "HiddenTasks", "Hidden Tasks" );
|
|
m_hiddenTasks.uiCapability()->setUiHidden( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_collapsedTasks, "CollapsedTasks", "Collapsed Tasks" );
|
|
m_collapsedTasks.uiCapability()->setUiHidden( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_nestedWorkflows, "NestedWorkflows", "Nested Workflows" );
|
|
m_nestedWorkflows.uiCapability()->setUiTreeChildrenHidden( true );
|
|
m_nestedWorkflows.xmlCapability()->disableIO();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
RimWorkflow::~RimWorkflow() = default;
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::name() const
|
|
{
|
|
return m_name();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setWorkflowDirectory( const QString& directory )
|
|
{
|
|
m_workflowDirectory = directory;
|
|
m_sourceDirectory = directory;
|
|
m_name = QFileInfo( QDir( directory ).absolutePath() ).fileName();
|
|
updateUiName();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::workflowDirectory() const
|
|
{
|
|
return m_workflowDirectory().path();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QJsonObject RimWorkflow::graph() const
|
|
{
|
|
return m_graph;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::loadError() const
|
|
{
|
|
return m_loadError();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Whether the graph shows config values in the task boxes instead of in separate config boxes
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::inlineConfigValues() const
|
|
{
|
|
return m_inlineConfigValues();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setInlineConfigValues( bool inlineValues )
|
|
{
|
|
m_inlineConfigValues = inlineValues;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Whether the graph flows from top to bottom instead of from left to right
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::verticalLayout() const
|
|
{
|
|
return m_verticalLayout();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setVerticalLayout( bool vertical )
|
|
{
|
|
m_verticalLayout = vertical;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Tasks the user has hidden in the graph. A view setting: the definition is not changed.
|
|
//--------------------------------------------------------------------------------------------------
|
|
QStringList RimWorkflow::hiddenTasks() const
|
|
{
|
|
return QStringList( m_hiddenTasks().begin(), m_hiddenTasks().end() );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::isTaskHidden( const QString& taskName ) const
|
|
{
|
|
return std::ranges::contains( m_hiddenTasks(), taskName );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setTaskHidden( const QString& taskName, bool hidden )
|
|
{
|
|
std::vector<QString> tasks = m_hiddenTasks();
|
|
std::erase( tasks, taskName );
|
|
if ( hidden ) tasks.push_back( taskName );
|
|
m_hiddenTasks = tasks;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::showAllTasks()
|
|
{
|
|
m_hiddenTasks = std::vector<QString>();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Tasks the user has collapsed to their title in the graph. A view setting like the hidden tasks.
|
|
//--------------------------------------------------------------------------------------------------
|
|
QStringList RimWorkflow::collapsedTasks() const
|
|
{
|
|
return QStringList( m_collapsedTasks().begin(), m_collapsedTasks().end() );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::isTaskCollapsed( const QString& taskName ) const
|
|
{
|
|
return std::ranges::contains( m_collapsedTasks(), taskName );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setTaskCollapsed( const QString& taskName, bool collapsed )
|
|
{
|
|
std::vector<QString> tasks = m_collapsedTasks();
|
|
std::erase( tasks, taskName );
|
|
if ( collapsed ) tasks.push_back( taskName );
|
|
m_collapsedTasks = tasks;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setCollapsedTasks( const QStringList& taskNames )
|
|
{
|
|
std::vector<QString> tasks;
|
|
for ( const QString& taskName : taskNames )
|
|
{
|
|
if ( !std::ranges::contains( tasks, taskName ) ) tasks.push_back( taskName );
|
|
}
|
|
m_collapsedTasks = tasks;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::vector<RimWorkflowJob*> RimWorkflow::jobs() const
|
|
{
|
|
std::vector<RimWorkflowJob*> result;
|
|
result.reserve( m_jobs.size() );
|
|
for ( RimWorkflowJob* job : m_jobs.childrenByType() )
|
|
{
|
|
if ( job ) result.push_back( job );
|
|
}
|
|
return result;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::addJob( RimWorkflowJob* job )
|
|
{
|
|
if ( !job ) return;
|
|
m_jobs.push_back( job );
|
|
job->syncTaskInputs( m_graph.value( "tasks" ).toArray() );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::syncJobs( const QMap<QString, QString>& renames )
|
|
{
|
|
const QJsonArray tasks = m_graph.value( "tasks" ).toArray();
|
|
for ( RimWorkflowJob* job : jobs() )
|
|
job->syncTaskInputs( tasks, renames );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const
|
|
{
|
|
menuBuilder << "RicNewWorkflowJobFeature";
|
|
menuBuilder << "Separator";
|
|
menuBuilder << "RicSaveWorkflowFeature";
|
|
menuBuilder << "RicSaveWorkflowAsFeature";
|
|
menuBuilder << "RicDuplicateWorkflowAsEditableFeature";
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
|
|
{
|
|
m_name.uiCapability()->setUiReadOnly( !isEditable() || isLocked() );
|
|
|
|
uiOrdering.add( &m_name );
|
|
uiOrdering.add( &m_description );
|
|
if ( m_source == Source::Registered )
|
|
uiOrdering.add( &m_registeredId );
|
|
else if ( !workflowDirectory().isEmpty() )
|
|
uiOrdering.add( &m_workflowDirectory );
|
|
uiOrdering.add( &m_editable );
|
|
if ( !m_readOnlyReason().isEmpty() ) uiOrdering.add( &m_readOnlyReason );
|
|
if ( !m_loadError().isEmpty() ) uiOrdering.add( &m_loadError );
|
|
uiOrdering.skipRemainingFields( true );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue )
|
|
{
|
|
if ( changedField != &m_name ) return;
|
|
|
|
auto result = applyEdit( RimWorkflowDefinitionTools::renameWorkflow( m_definition, m_name() ) );
|
|
if ( !result )
|
|
{
|
|
RiaLogging::warning( result.error().toStdString() );
|
|
m_name = m_definition.name;
|
|
updateUiName();
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::setLoadError( const QString& message, QString* errorMessage )
|
|
{
|
|
m_loadError = message;
|
|
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( m_name(), message ).toStdString() );
|
|
if ( errorMessage ) *errorMessage = message;
|
|
return false;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::isNestedTask( const QString& taskName ) const
|
|
{
|
|
for ( const QJsonValue& task : m_graph.value( "tasks" ).toArray() )
|
|
{
|
|
if ( task.toObject().value( "name" ).toString() == taskName ) return task.toObject().value( "nested_workflow" ).isObject();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// The workflow wrapped by a task, loaded the first time it is asked for. Null if the task is not
|
|
/// a nested workflow. A workflow that cannot be loaded has a load error.
|
|
//--------------------------------------------------------------------------------------------------
|
|
RimWorkflow* RimWorkflow::nestedWorkflow( const QString& taskName )
|
|
{
|
|
for ( RimWorkflow* nested : m_nestedWorkflows.childrenByType() )
|
|
{
|
|
if ( nested->m_nestedTaskName == taskName ) return nested;
|
|
}
|
|
if ( !isNestedTask( taskName ) ) return nullptr;
|
|
|
|
auto* nested = new RimWorkflow;
|
|
nested->m_nestedTaskName = taskName;
|
|
nested->m_name = taskName;
|
|
m_nestedWorkflows.push_back( nested );
|
|
nested->loadNested();
|
|
return nested;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// The workflow a nested workflow belongs to
|
|
//--------------------------------------------------------------------------------------------------
|
|
RimWorkflow* RimWorkflow::parentWorkflow() const
|
|
{
|
|
return m_source == Source::Nested ? firstAncestorOfType<RimWorkflow>() : nullptr;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::nestedTaskName() const
|
|
{
|
|
return m_nestedTaskName;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QStringList RimWorkflow::nestedTaskPath() const
|
|
{
|
|
QStringList path;
|
|
for ( const RimWorkflow* workflow = this; workflow->parentWorkflow(); workflow = workflow->parentWorkflow() )
|
|
path.prepend( workflow->m_nestedTaskName );
|
|
return path;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QMap<QString, QJsonObject> RimWorkflow::nestedEntries() const
|
|
{
|
|
return m_source == Source::Nested ? m_nestedEntries : RimWorkflowDefinitionTools::nestedWorkflowEntries( m_definition );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Describe the workflow of a nested task through `taskmaestro_helper describe-nested`. A YAML
|
|
/// entry is described from its file, relative to the file of the entry. A nested workflow in a
|
|
/// registered workflow is found from the root workflow and the task path.
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::loadNested()
|
|
{
|
|
m_source = Source::Nested;
|
|
|
|
RimWorkflowDefinition definition;
|
|
definition.name = m_nestedTaskName;
|
|
definition.editable = false;
|
|
definition.readOnlyReasons.append( QString( "Nested workflow of task '%1'" ).arg( m_nestedTaskName ) );
|
|
|
|
const auto failed = [&]( const QString& message )
|
|
{
|
|
setDefinition( definition, Source::Nested );
|
|
setLoadError( message, nullptr );
|
|
};
|
|
|
|
const std::optional<QStringList> args = describeNestedArguments();
|
|
if ( !args ) return failed( QString( "Cannot find the workflow of nested task '%1'" ).arg( m_nestedTaskName ) );
|
|
|
|
auto output = RimWorkflowHelperProcess::runSync( *args );
|
|
if ( !output ) return failed( output.error() );
|
|
|
|
setNestedDescription( *output, definition );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Arguments to `taskmaestro_helper describe-nested` for this nested workflow. A nested workflow in a
|
|
/// registered workflow is found from the root workflow and the task path, otherwise from the YAML
|
|
/// file of the entry. Empty if the workflow cannot be found.
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::optional<QStringList> RimWorkflow::describeNestedArguments()
|
|
{
|
|
RimWorkflow* parent = firstAncestorOfType<RimWorkflow>();
|
|
if ( !parent ) return std::nullopt;
|
|
|
|
RimWorkflow* root = parent;
|
|
while ( root->parentWorkflow() )
|
|
root = root->parentWorkflow();
|
|
|
|
if ( root->source() != Source::Registered ) return describeNestedYamlArguments( *parent );
|
|
|
|
QStringList args{ "describe-nested", "--registered", root->registeredId() };
|
|
for ( const QString& task : nestedTaskPath() )
|
|
args << "--task" << task;
|
|
return args;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Arguments to describe a `workflow:` entry of the parent from its YAML file, relative to the
|
|
/// directory of the parent. Sets the source directory to the directory of the YAML file.
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::optional<QStringList> RimWorkflow::describeNestedYamlArguments( const RimWorkflow& parent )
|
|
{
|
|
const QJsonObject entry = parent.nestedEntries().value( m_nestedTaskName );
|
|
const QString file = entry.value( "workflow" ).toString();
|
|
if ( file.isEmpty() || parent.sourceDirectory().isEmpty() ) return std::nullopt;
|
|
|
|
const QDir directory( parent.sourceDirectory() );
|
|
const QString path = QDir::cleanPath( directory.absoluteFilePath( file ) );
|
|
|
|
QStringList args{ "describe-nested", "--yaml", path };
|
|
const QString input = entry.value( "workflow_input" ).toString();
|
|
if ( !input.isEmpty() ) args << "--input" << QDir::cleanPath( directory.absoluteFilePath( input ) );
|
|
|
|
// An empty import directory would resolve to the current working directory
|
|
for ( const RimWorkflow* workflow = &parent; workflow; workflow = workflow->parentWorkflow() )
|
|
{
|
|
if ( !workflow->sourceDirectory().isEmpty() ) args << "--import-dir" << workflow->sourceDirectory();
|
|
}
|
|
|
|
m_sourceDirectory = QFileInfo( path ).absolutePath();
|
|
return args;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Set the definition and nested entries from the output of `taskmaestro_helper describe-nested`
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setNestedDescription( const QJsonObject& output, RimWorkflowDefinition definition )
|
|
{
|
|
const QJsonObject nested = output.value( "nested" ).toObject();
|
|
m_nestedEntries.clear();
|
|
for ( auto it = nested.begin(); it != nested.end(); ++it )
|
|
m_nestedEntries.insert( it.key(), it.value().toObject() );
|
|
|
|
definition.name = output.value( "name" ).toString( m_nestedTaskName );
|
|
definition.describe = output.value( "describe" ).toObject();
|
|
definition.describeError = output.value( "describe_error" ).toObject();
|
|
setDefinition( definition, Source::Nested );
|
|
if ( !definition.describe.isEmpty() ) return;
|
|
|
|
const QString message = definition.describeError.isEmpty() ? QString( "The nested workflow could not be described" )
|
|
: RimWorkflowHelperProcess::errorMessage( definition.describeError );
|
|
setLoadError( message, nullptr );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Load a YAML folder through `taskmaestro_helper load --describe`
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::loadFromDirectory( QString* errorMessage )
|
|
{
|
|
m_graph = {};
|
|
m_loadError = "";
|
|
|
|
const QString directory = workflowDirectory();
|
|
if ( directory.isEmpty() ) return false;
|
|
if ( !QDir( directory ).exists( "workflow.yaml" ) ) return setLoadError( "Missing workflow.yaml", errorMessage );
|
|
|
|
auto output = RimWorkflowHelperProcess::runSync( { "load", QDir( directory ).absolutePath(), "--describe" } );
|
|
if ( !output ) return setLoadError( output.error(), errorMessage );
|
|
|
|
auto definition = RimWorkflowDefinitionTools::fromJson( output.value() );
|
|
if ( !definition ) return setLoadError( definition.error(), errorMessage );
|
|
|
|
m_registeredId = "";
|
|
setDefinition( definition.value(), Source::YamlDirectory );
|
|
recordModificationTime();
|
|
|
|
const auto validation =
|
|
RimWorkflowValidationTools::issuesFromDescribe( m_definition.describe, m_definition.describeError, m_definition.nodeNames() );
|
|
setValidationResult( m_revision, validation.issues, validation.describeSucceeded );
|
|
if ( !validation.describeSucceeded )
|
|
{
|
|
m_loadError = RimWorkflowHelperProcess::errorMessage( m_definition.describeError );
|
|
RiaLogging::warning( QString( "Workflow '%1' is not valid: %2" ).arg( m_name(), m_loadError() ).toStdString() );
|
|
}
|
|
|
|
RiaLogging::info(
|
|
QString( "Loaded workflow '%1' (%2 tasks) from %3" ).arg( m_name() ).arg( m_definition.nodes.size() ).arg( directory ).toStdString() );
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// A workflow registered by an installed package, as listed by `taskmaestro_helper catalog`.
|
|
/// Registered workflows are read-only; Duplicate as Editable makes an editable copy.
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::loadFromRegistered( const QJsonObject& catalogEntry, QString* errorMessage )
|
|
{
|
|
m_graph = {};
|
|
m_loadError = "";
|
|
m_registeredId = catalogEntry.value( "id" ).toString();
|
|
m_name = catalogEntry.value( "name" ).toString( m_registeredId() );
|
|
m_sourceDirectory.clear();
|
|
|
|
RimWorkflowDefinition definition;
|
|
if ( catalogEntry.value( "definition" ).isObject() )
|
|
{
|
|
auto parsed = RimWorkflowDefinitionTools::fromJson( catalogEntry.value( "definition" ).toObject() );
|
|
if ( !parsed ) return setLoadError( parsed.error(), errorMessage );
|
|
definition = parsed.value();
|
|
}
|
|
else
|
|
{
|
|
definition.name = m_name();
|
|
definition.editable = false;
|
|
}
|
|
|
|
// `definition.editable` tells whether an editable copy is possible; the installed workflow
|
|
// itself is always read-only (see isEditable())
|
|
for ( const QJsonValue& reason : catalogEntry.value( "readonly_reasons" ).toArray() )
|
|
{
|
|
if ( !definition.readOnlyReasons.contains( reason.toString() ) ) definition.readOnlyReasons.append( reason.toString() );
|
|
}
|
|
if ( !definition.readOnlyReasons.isEmpty() ) definition.editable = false;
|
|
if ( definition.editable ) definition.readOnlyReasons.append( "Installed workflows are read-only; duplicate to edit" );
|
|
if ( definition.describe.isEmpty() ) definition.describe = catalogEntry.value( "describe" ).toObject();
|
|
|
|
setDefinition( definition, Source::Registered );
|
|
if ( m_graph.value( "tasks" ).toArray().isEmpty() ) return setLoadError( "The workflow could not be described", errorMessage );
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// A new workflow, or an editable copy. `sourceDirectory` holds Python modules used by the tasks,
|
|
/// and `targetDirectory` is the folder the first Save writes to.
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setUnsavedDefinition( const RimWorkflowDefinition& definition, const QString& sourceDirectory, const QString& targetDirectory )
|
|
{
|
|
m_workflowDirectory = targetDirectory;
|
|
m_registeredId = "";
|
|
m_loadError = "";
|
|
m_sourceDirectory = sourceDirectory;
|
|
|
|
RimWorkflowDefinition editable = definition;
|
|
if ( definition.editable ) editable.readOnlyReasons.clear();
|
|
editable.readOnlyReasons.append( RimWorkflowDefinitionTools::structuralReadOnlyReasons( definition ) );
|
|
editable.readOnlyReasons.removeDuplicates();
|
|
editable.editable = editable.readOnlyReasons.isEmpty();
|
|
setDefinition( editable, Source::Unsaved );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setDefinition( const RimWorkflowDefinition& definition, Source source )
|
|
{
|
|
// Nested workflows are loaded again when opened, from the new definition
|
|
m_nestedWorkflows.deleteChildren();
|
|
|
|
m_source = source;
|
|
m_definition = definition;
|
|
m_savedDefinition = definition;
|
|
m_undoStack.clear();
|
|
m_redoStack.clear();
|
|
m_validationIssues.clear();
|
|
m_validationRevision = -1;
|
|
++m_revision;
|
|
|
|
if ( !definition.name.isEmpty() ) m_name = definition.name;
|
|
m_description = definition.headerComment;
|
|
m_editable = isEditable();
|
|
m_readOnlyReason = definition.readOnlyReasons.join( "\n" );
|
|
|
|
rebuildGraph();
|
|
ensureDefaultJob();
|
|
syncJobs();
|
|
updateUiName();
|
|
notifyDefinitionChanged();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
RimWorkflow::Source RimWorkflow::source() const
|
|
{
|
|
return m_source;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::registeredId() const
|
|
{
|
|
return m_registeredId();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// The folder with the Python modules of the workflow (the YAML folder or the folder it was copied from)
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::sourceDirectory() const
|
|
{
|
|
return m_sourceDirectory;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
const RimWorkflowDefinition& RimWorkflow::definition() const
|
|
{
|
|
return m_definition;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::isEditable() const
|
|
{
|
|
return m_definition.editable && m_source != Source::Registered && m_source != Source::Nested;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::readOnlyReason() const
|
|
{
|
|
return m_readOnlyReason();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::isDirty() const
|
|
{
|
|
return m_source == Source::Unsaved || m_definition != m_savedDefinition;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// The workflow cannot be edited while one of its jobs is running
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::isLocked() const
|
|
{
|
|
for ( RimWorkflowJob* job : jobs() )
|
|
{
|
|
if ( job->isRunning() ) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
int RimWorkflow::revision() const
|
|
{
|
|
return m_revision;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Apply the result of an edit operation from RimWorkflowDefinitionTools. `renames` maps old task
|
|
/// names to new names, so that input values follow renamed tasks.
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::expected<void, QString> RimWorkflow::applyEdit( const std::expected<RimWorkflowDefinition, QString>& result,
|
|
const QMap<QString, QString>& renames )
|
|
{
|
|
if ( !result ) return std::unexpected( result.error() );
|
|
if ( !isEditable() )
|
|
return std::unexpected( m_readOnlyReason().isEmpty() ? QString( "The workflow is read-only" ) : m_readOnlyReason() );
|
|
if ( isLocked() ) return std::unexpected( "The workflow cannot be edited while a job is running" );
|
|
if ( result.value() == m_definition ) return {};
|
|
|
|
m_undoStack.push_back( { m_definition, renames } );
|
|
if ( m_undoStack.size() > maxUndoSteps ) m_undoStack.erase( m_undoStack.begin() );
|
|
m_redoStack.clear();
|
|
|
|
m_definition = result.value();
|
|
definitionChanged( renames );
|
|
return {};
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::canUndo() const
|
|
{
|
|
return isEditable() && !isLocked() && !m_undoStack.empty();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::canRedo() const
|
|
{
|
|
return isEditable() && !isLocked() && !m_redoStack.empty();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QMap<QString, QString> RimWorkflow::undoRenames() const
|
|
{
|
|
return m_undoStack.empty() ? QMap<QString, QString>() : invertedRenames( m_undoStack.back().renames );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QMap<QString, QString> RimWorkflow::redoRenames() const
|
|
{
|
|
return m_redoStack.empty() ? QMap<QString, QString>() : m_redoStack.back().renames;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::undo()
|
|
{
|
|
if ( !canUndo() ) return;
|
|
|
|
UndoEntry entry = m_undoStack.back();
|
|
m_undoStack.pop_back();
|
|
m_redoStack.push_back( { m_definition, entry.renames } );
|
|
m_definition = entry.definition;
|
|
definitionChanged( invertedRenames( entry.renames ) );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::redo()
|
|
{
|
|
if ( !canRedo() ) return;
|
|
|
|
UndoEntry entry = m_redoStack.back();
|
|
m_redoStack.pop_back();
|
|
m_undoStack.push_back( { m_definition, entry.renames } );
|
|
m_definition = entry.definition;
|
|
definitionChanged( entry.renames );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::definitionChanged( const QMap<QString, QString>& renames )
|
|
{
|
|
++m_revision;
|
|
m_name = m_definition.name;
|
|
m_description = m_definition.headerComment;
|
|
|
|
// Hidden and collapsed tasks follow renames, and deleted tasks are forgotten
|
|
for ( auto* field : { &m_hiddenTasks, &m_collapsedTasks } )
|
|
{
|
|
std::vector<QString> tasks;
|
|
for ( const QString& taskName : field->v() )
|
|
{
|
|
const QString name = renames.value( taskName, taskName );
|
|
if ( m_definition.findNode( name ) && !std::ranges::contains( tasks, name ) ) tasks.push_back( name );
|
|
}
|
|
*field = tasks;
|
|
}
|
|
|
|
// Keep the issues that have not been checked again until the next validation
|
|
rebuildGraph();
|
|
syncJobs( renames );
|
|
updateUiName();
|
|
notifyDefinitionChanged();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Let the workflow editor show the new state
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::notifyDefinitionChanged()
|
|
{
|
|
if ( auto* window = RiuMainWindow::instance() ) window->workflowDefinitionChanged( this );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QMap<QString, QString> RimWorkflow::invertedRenames( const QMap<QString, QString>& renames )
|
|
{
|
|
QMap<QString, QString> inverted;
|
|
for ( auto it = renames.begin(); it != renames.end(); ++it )
|
|
inverted.insert( it.value(), it.key() );
|
|
return inverted;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::rebuildGraph()
|
|
{
|
|
if ( isEditable() )
|
|
{
|
|
std::vector<RimWorkflowIssue> describeIssues;
|
|
if ( isValidationCurrent() ) describeIssues = m_validationIssues;
|
|
m_graph = RimWorkflowDefinitionTools::graphFromDefinition( m_definition, true, describeIssues );
|
|
}
|
|
else if ( !m_definition.describe.isEmpty() )
|
|
{
|
|
m_graph = RimWorkflowDescribeTools::graphFromDescribe( m_definition.describe );
|
|
m_graph["editable"] = false;
|
|
}
|
|
else
|
|
{
|
|
m_graph = RimWorkflowDefinitionTools::graphFromDefinition( m_definition, false );
|
|
}
|
|
m_graph = RimWorkflowDefinitionTools::markNestedWorkflows( m_graph, nestedEntries(), m_definition.describe );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::updateUiName()
|
|
{
|
|
setUiName( isDirty() && isEditable() ? m_name() + " *" : m_name() );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::ensureDefaultJob()
|
|
{
|
|
// A nested workflow runs as one task of its parent's jobs
|
|
if ( !m_jobs.empty() || m_source == Source::Nested ) return;
|
|
|
|
auto* job = new RimWorkflowJob;
|
|
job->setJobName( "Default" );
|
|
m_jobs.push_back( job );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setValidationResult( int revision, const std::vector<RimWorkflowIssue>& issues, bool describeSucceeded )
|
|
{
|
|
if ( revision != m_revision ) return;
|
|
|
|
m_validationIssues = issues;
|
|
m_validationRevision = revision;
|
|
m_describeSucceeded = describeSucceeded;
|
|
rebuildGraph();
|
|
notifyDefinitionChanged();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::vector<RimWorkflowIssue> RimWorkflow::validationIssues() const
|
|
{
|
|
return m_validationIssues;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::isValidationCurrent() const
|
|
{
|
|
return m_validationRevision == m_revision;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// True when the current revision has errors, from the static checks or from the last describe
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::lastValidationFailed() const
|
|
{
|
|
if ( isValidationCurrent() && !m_describeSucceeded ) return true;
|
|
if ( !isEditable() ) return false;
|
|
for ( const auto& issue : RimWorkflowDefinitionTools::validate( m_definition ) )
|
|
{
|
|
if ( issue.severity == RimWorkflowIssue::Severity::Error ) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// input.yaml content written on save: the literal values of the first job, falling back to the
|
|
/// values loaded from the file for tasks that have no inputs in the job
|
|
//--------------------------------------------------------------------------------------------------
|
|
QJsonObject RimWorkflow::inputValuesForSave() const
|
|
{
|
|
QJsonObject inputs = m_definition.inputs;
|
|
const auto allJobs = jobs();
|
|
if ( allJobs.empty() ) return inputs;
|
|
|
|
const QJsonObject literal = allJobs.front()->literalInputValues();
|
|
for ( const auto& node : m_definition.nodes )
|
|
{
|
|
if ( literal.contains( node.name ) ) inputs[node.name] = literal.value( node.name );
|
|
}
|
|
return inputs;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::expected<QJsonObject, QString> RimWorkflow::saveToDirectory( const QString& directory, bool backup )
|
|
{
|
|
QStringList args{ "save", "--out-dir", directory, "--describe" };
|
|
if ( backup ) args << "--backup";
|
|
|
|
auto output =
|
|
RimWorkflowHelperProcess::runSync( args, definitionJsonForSave(), RimWorkflowHelperProcess::defaultTimeoutMs, m_sourceDirectory );
|
|
if ( !output ) return output;
|
|
|
|
m_definition.inputs = inputValuesForSave();
|
|
m_definition.describe = output->value( "describe" ).toObject();
|
|
m_definition.describeError = output->value( "describe_error" ).toObject();
|
|
m_definition.source = QJsonObject{ { "workflow_yaml", output->value( "workflow_yaml" ) },
|
|
{ "input_yaml", output->value( "input_yaml" ) },
|
|
{ "registered_id", QJsonValue::Null } };
|
|
m_savedDefinition = m_definition;
|
|
m_source = Source::YamlDirectory;
|
|
m_loadedModificationTime = QFileInfo( QDir( directory ).absoluteFilePath( "workflow.yaml" ) ).lastModified();
|
|
|
|
const auto validation = RimWorkflowValidationTools::issuesFromHelperResult( output.value(), m_definition.nodeNames() );
|
|
setValidationResult( m_revision, validation.issues, validation.describeSucceeded );
|
|
updateUiName();
|
|
return output;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::recordModificationTime()
|
|
{
|
|
m_loadedModificationTime = QFileInfo( QDir( workflowDirectory() ).absoluteFilePath( "workflow.yaml" ) ).lastModified();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Reasons to think twice before overwriting the workflow folder: workflow.yaml was changed by
|
|
/// someone else since it was loaded, or the folder is a link into a git checkout
|
|
//--------------------------------------------------------------------------------------------------
|
|
QStringList RimWorkflow::saveInPlaceWarnings() const
|
|
{
|
|
QStringList warnings;
|
|
if ( m_source != Source::YamlDirectory || workflowDirectory().isEmpty() ) return warnings;
|
|
|
|
const QFileInfo yamlFile( QDir( workflowDirectory() ).absoluteFilePath( "workflow.yaml" ) );
|
|
if ( m_loadedModificationTime.isValid() && yamlFile.exists() && yamlFile.lastModified() != m_loadedModificationTime )
|
|
{
|
|
warnings << "workflow.yaml has been changed on disk since it was loaded. Saving overwrites those changes.";
|
|
}
|
|
|
|
const QFileInfo directoryInfo( workflowDirectory() );
|
|
if ( directoryInfo.isSymLink() || yamlFile.isSymLink() )
|
|
{
|
|
QDir dir( yamlFile.canonicalPath() );
|
|
do
|
|
{
|
|
if ( dir.exists( ".git" ) )
|
|
{
|
|
warnings
|
|
<< QString( "The workflow folder links into the git checkout '%1'. Saving changes files there." ).arg( dir.absolutePath() );
|
|
break;
|
|
}
|
|
} while ( dir.cdUp() );
|
|
}
|
|
return warnings;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Write workflow.yaml and input.yaml to the workflow folder. A backup is made the first time.
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::expected<void, QString> RimWorkflow::save()
|
|
{
|
|
if ( !isEditable() ) return std::unexpected( "The workflow is read-only" );
|
|
if ( workflowDirectory().isEmpty() ) return std::unexpected( "The workflow has no folder; use Save As" );
|
|
if ( m_source == Source::Unsaved ) return saveAs( workflowDirectory() );
|
|
|
|
auto output = saveToDirectory( workflowDirectory(), true );
|
|
if ( !output ) return std::unexpected( output.error() );
|
|
|
|
RiaLogging::info( QString( "Saved workflow '%1' to %2" ).arg( m_name(), workflowDirectory() ).toStdString() );
|
|
return {};
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Save to a new folder. Python modules next to the original workflow.yaml are copied along.
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::expected<void, QString> RimWorkflow::saveAs( const QString& directory )
|
|
{
|
|
if ( !isEditable() ) return std::unexpected( "The workflow is read-only" );
|
|
if ( directory.isEmpty() ) return std::unexpected( "No folder given" );
|
|
|
|
QDir target( directory );
|
|
if ( !target.exists() && !QDir().mkpath( directory ) )
|
|
return std::unexpected( QString( "Could not create the folder '%1'" ).arg( directory ) );
|
|
|
|
if ( !m_sourceDirectory.isEmpty() && QDir( m_sourceDirectory ).absolutePath() != target.absolutePath() )
|
|
{
|
|
if ( auto copied = copyPythonModules( m_sourceDirectory, target.absolutePath() ); !copied )
|
|
return std::unexpected( QString( "The workflow was not saved: %1" ).arg( copied.error() ) );
|
|
}
|
|
|
|
auto output = saveToDirectory( target.absolutePath(), false );
|
|
if ( !output ) return std::unexpected( output.error() );
|
|
|
|
m_workflowDirectory = target.absolutePath();
|
|
m_sourceDirectory = target.absolutePath();
|
|
m_registeredId = "";
|
|
updateUiName();
|
|
notifyDefinitionChanged();
|
|
|
|
RiaLogging::info( QString( "Saved workflow '%1' to %2" ).arg( m_name(), target.absolutePath() ).toStdString() );
|
|
return {};
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// The temporary folder that live validation writes the YAML of unsaved edits to. Runs use their
|
|
/// own folder (see prepareRunSource()), so validation never rewrites a file a run is reading.
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::expected<QString, QString> RimWorkflow::editDirectory()
|
|
{
|
|
if ( !m_editDirectory )
|
|
{
|
|
m_editDirectory = std::make_unique<QTemporaryDir>( QDir::tempPath() + "/resinsight_workflow_edit_XXXXXX" );
|
|
if ( !m_editDirectory->isValid() )
|
|
{
|
|
m_editDirectory.reset();
|
|
return std::unexpected( "Could not create a temporary folder for the workflow" );
|
|
}
|
|
}
|
|
return m_editDirectory->path();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// The definition as sent to `taskmaestro_helper save`
|
|
//--------------------------------------------------------------------------------------------------
|
|
QByteArray RimWorkflow::definitionJsonForSave() const
|
|
{
|
|
RimWorkflowDefinition toSave = m_definition;
|
|
toSave.inputs = inputValuesForSave();
|
|
return QJsonDocument( RimWorkflowDefinitionTools::toJson( toSave ) ).toJson( QJsonDocument::Compact );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// The arguments of `taskmaestro_helper run` for the current state of the workflow. Unsaved edits
|
|
/// are written as workflow.yaml to `runDirectory`, a folder private to the run.
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::expected<RimWorkflow::RunSource, QString> RimWorkflow::prepareRunSource( const QString& runDirectory )
|
|
{
|
|
if ( m_source == Source::Registered )
|
|
{
|
|
if ( m_registeredId().isEmpty() ) return std::unexpected( "The workflow has no registered id" );
|
|
return RunSource{ .baseArgs = { "run", "--registered", m_registeredId() } };
|
|
}
|
|
|
|
if ( m_source == Source::YamlDirectory && !isDirty() )
|
|
{
|
|
if ( workflowDirectory().isEmpty() ) return std::unexpected( "The workflow has no folder" );
|
|
return RunSource{ .baseArgs = { "run", workflowDirectory() }, .extraPythonPath = workflowDirectory() };
|
|
}
|
|
|
|
if ( runDirectory.isEmpty() ) return std::unexpected( "No folder to write the edited workflow to" );
|
|
auto output = RimWorkflowHelperProcess::runSync( { "save", "--out-dir", runDirectory, "--workflow-only" },
|
|
definitionJsonForSave(),
|
|
RimWorkflowHelperProcess::defaultTimeoutMs,
|
|
m_sourceDirectory );
|
|
if ( !output ) return std::unexpected( output.error() );
|
|
return RunSource{ .baseArgs = { "run", runDirectory }, .extraPythonPath = m_sourceDirectory };
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Copies the `*.py` files and the package folders (with `__init__.py`), without `__pycache__`.
|
|
/// Files that already exist in the target are kept. Fails on the first file that cannot be copied.
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::expected<void, QString> RimWorkflow::copyPythonModules( const QString& sourceDirectory, const QString& targetDirectory )
|
|
{
|
|
const QDir source( sourceDirectory );
|
|
QDir target( targetDirectory );
|
|
if ( !target.exists() && !QDir().mkpath( targetDirectory ) )
|
|
return std::unexpected( QString( "Could not create the folder '%1'" ).arg( targetDirectory ) );
|
|
|
|
for ( const QString& fileName : source.entryList( { "*.py" }, QDir::Files ) )
|
|
{
|
|
if ( target.exists( fileName ) ) continue;
|
|
if ( !QFile::copy( source.absoluteFilePath( fileName ), target.absoluteFilePath( fileName ) ) )
|
|
return std::unexpected( QString( "Could not copy '%1' to '%2'" ).arg( source.absoluteFilePath( fileName ), targetDirectory ) );
|
|
}
|
|
|
|
for ( const QString& folderName : source.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
|
|
{
|
|
if ( folderName == "__pycache__" || !QFileInfo::exists( source.absoluteFilePath( folderName + "/__init__.py" ) ) ) continue;
|
|
if ( auto copied = copyPythonModules( source.absoluteFilePath( folderName ), target.absoluteFilePath( folderName ) ); !copied )
|
|
return copied;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::findPythonExecutable()
|
|
{
|
|
QStringList candidates;
|
|
if ( auto* prefs = RiaPreferences::current() )
|
|
{
|
|
QString configured = prefs->pythonExecutable();
|
|
if ( !configured.isEmpty() && configured != "python" ) candidates << configured;
|
|
}
|
|
candidates << "python3" << "python";
|
|
|
|
for ( const QString& candidate : candidates )
|
|
{
|
|
if ( candidate.contains( '/' ) || candidate.contains( '\\' ) )
|
|
{
|
|
if ( QFileInfo( candidate ).isExecutable() ) return candidate;
|
|
}
|
|
else if ( !QStandardPaths::findExecutable( candidate ).isEmpty() )
|
|
{
|
|
return candidate;
|
|
}
|
|
}
|
|
return {};
|
|
}
|