2026-10-02 13:13:27 +02:00
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026- Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RiuWorkflowEditorWidget.h"
#include "RiuWorkflowTaskPalette.h"
#include "RiuWorkflowValidationRunner.h"
#include "WorkflowCommands/RicWorkflowFeatureTools.h"
#include "Workflow/RimWorkflow.h"
#include "Workflow/RimWorkflowDefinition.h"
#include "Workflow/RimWorkflowDefinitionTools.h"
#include "Workflow/RimWorkflowFieldBinding.h"
#include "Workflow/RimWorkflowJob.h"
#include "Workflow/RimWorkflowPortCompatibility.h"
#include "Workflow/RimWorkflowTaskCatalog.h"
#include "Workflow/RimWorkflowTaskInput.h"
#include "Workflow/RimWorkflowValidationTools.h"
#include <QAction>
2026-10-26 14:19:29 +01:00
#include <QCheckBox>
2026-10-02 13:13:27 +02:00
#include <QComboBox>
2026-10-26 14:19:29 +01:00
#include <QCursor>
2026-10-02 13:13:27 +02:00
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QInputDialog>
#include <QJsonArray>
#include <QLabel>
#include <QLineEdit>
2026-10-26 14:19:29 +01:00
#include <QListWidget>
2026-10-02 13:13:27 +02:00
#include <QMenu>
#include <QPushButton>
#include <QSplitter>
2026-10-26 14:19:29 +01:00
#include <QStackedWidget>
2026-10-02 13:13:27 +02:00
#include <QVBoxLayout>
#include <functional>
#include <map>
namespace
{
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QAction * addMenuAction ( QMenu * menu , const QString & text , std :: function < void () > slot , const QString & icon = {} )
{
QAction * action = menu -> addAction ( icon . isEmpty () ? QIcon () : QIcon ( icon ), text );
QObject :: connect ( action , & QAction :: triggered , menu , std :: move ( slot ) );
return action ;
}
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiuWorkflowEditorWidget :: RiuWorkflowEditorWidget ( QWidget * parent )
: QWidget ( parent )
{
auto * layout = new QVBoxLayout ( this );
layout -> setContentsMargins ( 0 , 0 , 0 , 0 );
layout -> setSpacing ( 2 );
auto addAction = [ this ]( const QString & text , const QString & icon , auto slot )
{
auto * action = new QAction ( icon . isEmpty () ? QIcon () : QIcon ( icon ), text , this );
connect ( action , & QAction :: triggered , this , slot );
return action ;
};
m_saveAction = addAction ( "Save" ,
":/Save.svg" ,
[ this ]()
{
if ( m_workflow ) RicWorkflowFeatureTools :: saveWorkflow ( m_workflow );
} );
m_saveAsAction = addAction ( "Save As..." ,
":/SaveAs.svg" ,
[ this ]()
{
if ( m_workflow ) RicWorkflowFeatureTools :: saveWorkflowAs ( m_workflow );
} );
m_undoAction = addAction ( "Undo" ,
":/undo.png" ,
[ this ]()
{
if ( ! m_workflow || ! m_workflow -> canUndo () || m_workflow -> isLocked () ) return ;
renameNodePositions ( m_workflow -> undoRenames () );
m_workflow -> undo ();
} );
m_redoAction = addAction ( "Redo" ,
":/redo.png" ,
[ this ]()
{
if ( ! m_workflow || ! m_workflow -> canRedo () || m_workflow -> isLocked () ) return ;
renameNodePositions ( m_workflow -> redoRenames () );
m_workflow -> redo ();
} );
m_duplicateAction = addAction ( "Duplicate as Editable" ,
":/Copy.svg" ,
[ this ]()
{
if ( m_workflow ) RicWorkflowFeatureTools :: duplicateAsEditable ( m_workflow );
} );
m_fitAction = addAction ( "Fit" , "" , [ this ]() { m_graphView -> fitGraph (); } );
m_splitter = new QSplitter ( Qt :: Horizontal , this );
m_palette = new RiuWorkflowTaskPalette ( m_splitter );
m_graphView = new RiuWorkflowGraphView ( m_splitter );
m_splitter -> addWidget ( m_graphView );
m_splitter -> addWidget ( m_palette );
m_splitter -> setStretchFactor ( 0 , 1 );
m_splitter -> setStretchFactor ( 1 , 0 );
m_splitter -> setSizes ( { 800 , 220 } );
layout -> addWidget ( m_splitter , 1 );
m_statusLabel = new QLabel ( this );
m_statusLabel -> setWordWrap ( true );
m_statusLabel -> setTextInteractionFlags ( Qt :: TextSelectableByMouse );
m_statusLabel -> setContentsMargins ( 4 , 2 , 4 , 2 );
layout -> addWidget ( m_statusLabel );
m_validationRunner = new RiuWorkflowValidationRunner ( this );
connect ( m_validationRunner , & RiuWorkflowValidationRunner :: validationStarted , this , & RiuWorkflowEditorWidget :: updateStatus );
connect ( m_validationRunner , & RiuWorkflowValidationRunner :: validationFinished , this , & RiuWorkflowEditorWidget :: onValidationFinished );
m_graphView -> setConnectionValidator (
[ this ]( const QString & from , const QString & output , const QString & to , const QString & input ) -> std :: expected < void , QString >
{
if ( ! m_workflow ) return std :: unexpected ( QString ( "No workflow" ) );
return RimWorkflowPortCompatibility :: canConnect ( m_workflow -> definition (), from , output , to , input );
} );
connect ( m_graphView , & RiuWorkflowGraphView :: nodeSelected , this , & RiuWorkflowEditorWidget :: taskSelected );
connect ( m_graphView , & RiuWorkflowGraphView :: connectRequested , this , & RiuWorkflowEditorWidget :: connectPorts );
connect ( m_graphView , & RiuWorkflowGraphView :: deleteRequested , this , & RiuWorkflowEditorWidget :: deleteItem );
connect ( m_graphView , & RiuWorkflowGraphView :: renameRequested , this , & RiuWorkflowEditorWidget :: renameTask );
connect ( m_graphView ,
& RiuWorkflowGraphView :: addTaskRequested ,
this ,
[ this ]( const QString & taskId , const QPointF & scenePos ) { addTask ( taskId , scenePos ); } );
connect ( m_graphView , & RiuWorkflowGraphView :: contextMenuRequested , this , & RiuWorkflowEditorWidget :: showContextMenu );
connect ( m_palette , & RiuWorkflowTaskPalette :: taskActivated , this , [ this ]( const QString & taskId ) { addTask ( taskId ); } );
updateActions ();
updateStatus ();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiuWorkflowEditorWidget ::~ RiuWorkflowEditorWidget ()
{
m_validationRunner -> cancel ();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: setWorkflow ( RimWorkflow * workflow , RimWorkflowJob * job )
{
const bool workflowChanged = workflow != m_workflow . p ();
if ( workflowChanged )
{
m_validationRunner -> cancel ();
m_message . clear ();
}
m_workflow = workflow ;
m_job = job ;
showWorkflow ( workflowChanged ? RiuWorkflowGraphView :: ViewportPolicy :: Fit : RiuWorkflowGraphView :: ViewportPolicy :: Keep );
scheduleValidation ();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimWorkflow * RiuWorkflowEditorWidget :: workflow () const
{
return m_workflow . p ();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimWorkflowJob * RiuWorkflowEditorWidget :: job () const
{
return m_job . p ();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: clear ()
{
m_validationRunner -> cancel ();
m_workflow = nullptr ;
m_job = nullptr ;
m_message . clear ();
showWorkflow ( RiuWorkflowGraphView :: ViewportPolicy :: Fit );
}
//--------------------------------------------------------------------------------------------------
/// Show the current revision of the workflow without moving the view
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: refresh ()
{
showWorkflow ( RiuWorkflowGraphView :: ViewportPolicy :: Keep );
scheduleValidation ();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: updateJobState ()
{
m_graphView -> resetTaskStates ();
if ( ! m_job ) return ;
const auto states = m_job -> taskStates ();
const auto errors = m_job -> taskErrors ();
for ( auto it = states . cbegin (); it != states . cend (); ++ it )
m_graphView -> setTaskState ( it . key (), it . value (), errors . value ( it . key () ) );
const auto itemStates = m_job -> itemStates ();
const auto itemErrors = m_job -> itemErrors ();
for ( auto task = itemStates . cbegin (); task != itemStates . cend (); ++ task )
{
for ( auto item = task . value (). cbegin (); item != task . value (). cend (); ++ item )
m_graphView -> setTaskItemState ( task . key (), item . key (), item . value (), itemErrors . value ( task . key () ). value ( item . key () ) );
}
m_graphView -> setRunStatus ( m_job -> runStatus () );
// A running job locks the workflow
const bool editable = m_workflow && m_workflow -> isEditable () && ! m_workflow -> isLocked ();
m_graphView -> setEditable ( editable );
m_palette -> setEnabled ( editable );
updateActions ();
updateStatus ();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: updateTaskInputValues ()
{
if ( ! m_job ) return ;
for ( RimWorkflowTaskInput * task : m_job -> taskInputs () )
{
for ( RimWorkflowFieldBinding * binding : task -> items () )
{
if ( binding ) m_graphView -> setTaskInputValue ( task -> taskName (), binding -> fieldName (), binding -> displayValue () );
}
}
updateTaskIssues ();
}
//--------------------------------------------------------------------------------------------------
/// Show the issues of the workflow, with missing-value warnings taken from the displayed job
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: updateTaskIssues ()
{
if ( ! m_workflow || ! m_job ) return ;
for ( const QJsonValue & value : m_workflow -> graph (). value ( "tasks" ). toArray () )
{
const QJsonObject graphTask = value . toObject ();
const QString taskName = graphTask . value ( "name" ). toString ();
RimWorkflowTaskInput * taskInput = m_job -> taskInput ( taskName );
std :: map < QString , bool > fieldHasValue ;
for ( const QJsonValue & field : graphTask . value ( "config_fields" ). toArray () )
{
const QJsonObject schema = field . toObject ();
const QString fieldName = schema . value ( "name" ). toString ();
bool hasValue = false ;
if ( taskInput )
{
for ( const RimWorkflowFieldBinding * binding : taskInput -> items () )
{
2026-10-26 14:19:29 +01:00
if ( binding && binding -> fieldName () == fieldName )
hasValue = ! binding -> toJsonValue (). isNull () || binding -> isExplicitNull ();
2026-10-02 13:13:27 +02:00
}
}
fieldHasValue [ fieldName ] = hasValue || ! schema . value ( "required" ). toBool ();
}
m_graphView -> setTaskIssues ( taskName ,
RimWorkflowValidationTools :: issuesWithJobValues ( taskName ,
graphTask . value ( "issues" ). toArray (),
fieldHasValue ) );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiuWorkflowGraphView * RiuWorkflowEditorWidget :: graphView () const
{
return m_graphView ;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiuWorkflowTaskPalette * RiuWorkflowEditorWidget :: palette () const
{
return m_palette ;
}
//--------------------------------------------------------------------------------------------------
/// Apply an edit to the displayed workflow. The workflow notifies the main window, which refreshes
/// this widget.
//--------------------------------------------------------------------------------------------------
bool RiuWorkflowEditorWidget :: applyEdit ( const std :: expected < RimWorkflowDefinition , QString >& result , const QMap < QString , QString >& renames )
{
if ( ! m_workflow ) return false ;
auto applied = m_workflow -> applyEdit ( result , renames );
if ( ! applied )
{
showMessage ( applied . error () );
return false ;
}
m_message . clear ();
updateActions ();
updateStatus ();
return true ;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: addTask ( const QString & taskId , const std :: optional < QPointF >& scenePos )
{
if ( ! canEdit () ) return ;
QString addedName ;
const auto result = RimWorkflowDefinitionTools :: addTask ( m_workflow -> definition (), RimWorkflowTaskCatalog :: instance (), taskId , & addedName );
if ( result && scenePos ) m_graphView -> setPendingNodePosition ( addedName , * scenePos );
if ( applyEdit ( result ) ) m_graphView -> selectTask ( addedName );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: deleteItem ( const RiuWorkflowGraphView :: ItemRef & item )
{
if ( ! canEdit () ) return ;
if ( item . kind == RiuWorkflowGraphView :: ItemRef :: Kind :: Task )
{
applyEdit ( RimWorkflowDefinitionTools :: removeTask ( m_workflow -> definition (), item . task ) );
}
else if ( item . kind == RiuWorkflowGraphView :: ItemRef :: Kind :: Edge )
{
const RimWorkflowDefinitionEdge edge { . from = item . from , . output = item . output , . to = item . to , . input = item . input };
applyEdit ( RimWorkflowDefinitionTools :: disconnect ( m_workflow -> definition (), edge ) );
}
2026-10-26 14:19:29 +01:00
else if ( item . kind == RiuWorkflowGraphView :: ItemRef :: Kind :: Condition )
{
applyEdit ( RimWorkflowDefinitionTools :: setTaskGuard ( m_workflow -> definition (), item . to , std :: nullopt ) );
}
2026-10-02 13:13:27 +02:00
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: renameTask ( const QString & taskName )
{
if ( ! canEdit () || ! m_workflow -> definition (). findNode ( taskName ) ) return ;
bool ok = false ;
const QString newName =
QInputDialog :: getText ( this , "Rename Task" , "Task name (letters, digits and underscores):" , QLineEdit :: Normal , taskName , & ok ). trimmed ();
if ( ! ok || newName == taskName ) return ;
// The workflow may have changed or been deleted while the dialog was open
if ( ! canEdit () ) return ;
const auto result = RimWorkflowDefinitionTools :: renameTask ( m_workflow -> definition (), taskName , newName );
if ( result ) m_graphView -> renameNodePosition ( taskName , newName );
if ( applyEdit ( result , { { taskName , newName } } ) )
{
m_graphView -> selectTask ( newName );
}
else if ( result )
{
m_graphView -> renameNodePosition ( newName , taskName );
}
}
//--------------------------------------------------------------------------------------------------
/// Let renamed tasks keep their place in the graph
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: renameNodePositions ( const QMap < QString , QString >& renames )
{
for ( auto it = renames . cbegin (); it != renames . cend (); ++ it )
m_graphView -> renameNodePosition ( it . key (), it . value () );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: setResultTask ( const QString & taskName )
{
if ( ! canEdit () ) return ;
applyEdit ( RimWorkflowDefinitionTools :: setResultTask ( m_workflow -> definition (), taskName ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: setOptionalInputConfigured ( const QString & taskName , const QString & field , bool configured )
{
if ( ! canEdit () ) return ;
applyEdit ( RimWorkflowDefinitionTools :: setOptionalInputConfigured ( m_workflow -> definition (), taskName , field , configured ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: connectPorts ( const QString & from , const QString & output , const QString & to , const QString & input )
{
if ( ! canEdit () ) return ;
const RimWorkflowDefinitionEdge edge { . from = from , . output = output , . to = to , . input = input };
2026-10-26 14:19:29 +01:00
// A connected input may instead take the new output as an alternative, used when the earlier ones did not run
auto alternative = RimWorkflowDefinitionTools :: addFirstOfMember ( m_workflow -> definition (), edge );
if ( alternative )
{
QMenu menu ;
QAction * replace = menu . addAction ( "Replace Connection" );
replace -> setToolTip ( QString ( "Connect %1 to %2.%3 instead of the current connection" ). arg ( from , to , input ) );
QAction * add = menu . addAction ( "Add as Alternative (First Of)" );
add -> setToolTip ( QString ( "%1.%2 takes the output of the first connected task that ran" ). arg ( to , input ) );
menu . setToolTipsVisible ( true );
QAction * chosen = menu . exec ( QCursor :: pos () );
if ( ! chosen || ! canEdit () ) return ;
if ( chosen == add )
{
applyEdit ( RimWorkflowDefinitionTools :: addFirstOfMember ( m_workflow -> definition (), edge ) );
return ;
}
}
2026-10-02 13:13:27 +02:00
applyEdit ( RimWorkflowDefinitionTools :: connect ( m_workflow -> definition (), edge ) );
}
//--------------------------------------------------------------------------------------------------
/// Shows why the workflow cannot be edited
//--------------------------------------------------------------------------------------------------
bool RiuWorkflowEditorWidget :: canEdit ()
{
if ( ! m_workflow ) return false ;
if ( m_workflow -> isEditable () && ! m_workflow -> isLocked () ) return true ;
showMessage ( m_workflow -> isLocked () ? QString ( "The workflow is locked while a job is running" ) : m_workflow -> readOnlyReason () );
return false ;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
const RimWorkflowDefinitionEdge * RiuWorkflowEditorWidget :: findEdge ( const RiuWorkflowGraphView :: ItemRef & item ) const
{
if ( ! m_workflow || item . kind != RiuWorkflowGraphView :: ItemRef :: Kind :: Edge ) return nullptr ;
const RimWorkflowDefinitionEdge wanted { . from = item . from , . output = item . output , . to = item . to , . input = item . input };
for ( const auto & edge : m_workflow -> definition (). edges )
{
if ( edge . sameConnection ( wanted ) ) return & edge ;
}
return nullptr ;
}
//--------------------------------------------------------------------------------------------------
/// Run the task once per item of a mapping. The dialog edits the current map, if any.
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: editTaskMap ( const QString & taskName )
{
if ( ! canEdit () ) return ;
// A copy, as the workflow may change or be deleted while the dialog is open
const RimWorkflowDefinition definition = m_workflow -> definition ();
const RimWorkflowDefinitionNode * node = definition . findNode ( taskName );
if ( ! node ) return ;
QStringList fields ;
for ( const auto & port : RimWorkflowPortCompatibility :: inputPorts ( definition . taskType ( node -> taskId ) ) )
{
if ( ! port . isWhole () ) fields . append ( port . name );
}
if ( fields . size () < 2 )
{
showMessage ( QString ( "Task '%1' needs two inputs to receive the key and the value of each item" ). arg ( taskName ) );
return ;
}
const RimWorkflowTaskMap current = node -> map . value_or ( RimWorkflowTaskMap { . over = "items" } );
QDialog dialog ( this );
dialog . setWindowTitle ( QString ( "Map Over - %1" ). arg ( taskName ) );
auto * form = new QFormLayout ( & dialog );
auto * overEdit = new QLineEdit ( current . over , & dialog );
overEdit -> setToolTip ( "Name of the configured mapping. Each key and value of the mapping runs the task once." );
form -> addRow ( "Map over:" , overEdit );
auto addFieldCombo = [ & ]( const QString & label , const QString & value , const QString & fallback , const QString & toolTip )
{
auto * combo = new QComboBox ( & dialog );
combo -> addItems ( fields );
combo -> setCurrentText ( fields . contains ( value ) ? value : fallback );
combo -> setToolTip ( toolTip );
form -> addRow ( label , combo );
return combo ;
};
auto * keyCombo = addFieldCombo ( "Key goes to:" , current . keyAs , fields . front (), "The input that receives the key of each item" );
auto * valueCombo = addFieldCombo ( "Value goes to:" , current . valueAs , fields . at ( 1 ), "The input that receives the value of each item" );
auto * errorCombo = new QComboBox ( & dialog );
errorCombo -> addItem ( "Stop at the first failed item" , "fail_fast" );
errorCombo -> addItem ( "Run every item, then fail if any failed" , "collect_all" );
errorCombo -> setCurrentIndex ( std :: max ( 0 , errorCombo -> findData ( current . errorMode ) ) );
form -> addRow ( "On failure:" , errorCombo );
auto * errorLabel = new QLabel ( & dialog );
errorLabel -> setStyleSheet ( "color: #b00020;" );
errorLabel -> setWordWrap ( true );
form -> addRow ( errorLabel );
auto * buttons = new QDialogButtonBox ( QDialogButtonBox :: Ok | QDialogButtonBox :: Cancel , & dialog );
form -> addRow ( buttons );
auto mapFromDialog = [ & ]()
{
return RimWorkflowTaskMap { . over = overEdit -> text (). trimmed (),
. keyAs = keyCombo -> currentText (),
. valueAs = valueCombo -> currentText (),
. errorMode = errorCombo -> currentData (). toString () };
};
auto updateValidity = [ & ]()
{
const auto valid = RimWorkflowDefinitionTools :: checkTaskMap ( definition , taskName , mapFromDialog () );
errorLabel -> setText ( valid ? QString () : valid . error () );
errorLabel -> setVisible ( ! valid );
buttons -> button ( QDialogButtonBox :: Ok ) -> setEnabled ( valid . has_value () );
};
QObject :: connect ( overEdit , & QLineEdit :: textChanged , & dialog , updateValidity );
QObject :: connect ( keyCombo , & QComboBox :: currentIndexChanged , & dialog , updateValidity );
QObject :: connect ( valueCombo , & QComboBox :: currentIndexChanged , & dialog , updateValidity );
QObject :: connect ( buttons , & QDialogButtonBox :: accepted , & dialog , & QDialog :: accept );
QObject :: connect ( buttons , & QDialogButtonBox :: rejected , & dialog , & QDialog :: reject );
updateValidity ();
if ( dialog . exec () != QDialog :: Accepted || ! canEdit () ) return ;
const RimWorkflowDefinitionNode * currentNode = m_workflow -> definition (). findNode ( taskName );
if ( ! currentNode ) return ;
const RimWorkflowTaskMap map = mapFromDialog ();
if ( currentNode -> map && * currentNode -> map == map ) return ;
applyEdit ( RimWorkflowDefinitionTools :: setTaskMap ( m_workflow -> definition (), taskName , map ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: removeTaskMap ( const QString & taskName )
{
if ( ! canEdit () ) return ;
applyEdit ( RimWorkflowDefinitionTools :: setTaskMap ( m_workflow -> definition (), taskName , std :: nullopt ) );
}
2026-10-26 14:19:29 +01:00
//--------------------------------------------------------------------------------------------------
/// Run the task only for some branches of a switch task, or when an output field of another task
/// has a value. The dialog edits the current condition, if any.
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: editTaskGuard ( const QString & taskName )
{
using Op = RimWorkflowTaskGuard :: Op ;
if ( ! canEdit () ) return ;
// A copy, as the workflow may change or be deleted while the dialog is open
const RimWorkflowDefinition definition = m_workflow -> definition ();
const RimWorkflowDefinitionNode * node = definition . findNode ( taskName );
if ( ! node ) return ;
QStringList sources ;
for ( const auto & other : definition . nodes )
{
if ( other . name != taskName ) sources . append ( other . name );
}
if ( sources . isEmpty () )
{
showMessage ( QString ( "Add a task for '%1' to depend on first" ). arg ( taskName ) );
return ;
}
// Default to the first switch task, the usual source of a run condition
QString defaultSource = sources . front ();
for ( const QString & name : sources )
{
if ( RimWorkflowDefinitionTools :: isSwitchTask ( definition . taskType ( definition . findNode ( name ) -> taskId ) ) )
{
defaultSource = name ;
break ;
}
}
const RimWorkflowTaskGuard current = node -> when . value_or ( RimWorkflowTaskGuard { . source = defaultSource } );
QDialog dialog ( this );
dialog . setWindowTitle ( QString ( "Run When - %1" ). arg ( taskName ) );
auto * form = new QFormLayout ( & dialog );
form -> addRow ( new QLabel ( QString ( "'%1' runs only when the condition holds; otherwise it is skipped, "
"and so are the tasks that need its output." )
. arg ( taskName ),
& dialog ) );
auto * sourceCombo = new QComboBox ( & dialog );
sourceCombo -> addItems ( sources );
sourceCombo -> setCurrentText ( sources . contains ( current . source ) ? current . source : defaultSource );
sourceCombo -> setToolTip ( "The task whose output decides whether this task runs" );
form -> addRow ( "Depends on:" , sourceCombo );
auto * fieldCombo = new QComboBox ( & dialog );
fieldCombo -> setToolTip ( "The branch of a switch task, or an output field to test" );
form -> addRow ( "Test:" , fieldCombo );
auto * pages = new QStackedWidget ( & dialog );
form -> addRow ( pages );
// Page 0: branches of a switch task
auto * branchPage = new QWidget ( pages );
auto * branchLayout = new QVBoxLayout ( branchPage );
branchLayout -> setContentsMargins ( 0 , 0 , 0 , 0 );
auto * branchList = new QListWidget ( branchPage );
branchList -> setToolTip ( "Run for any of the checked branches" );
auto * otherwiseCheck = new QCheckBox ( "Otherwise (no other condition on the switch matches)" , branchPage );
branchLayout -> addWidget ( branchList );
branchLayout -> addWidget ( otherwiseCheck );
pages -> addWidget ( branchPage );
// Page 1: a field value
auto * valuePage = new QWidget ( pages );
auto * valueForm = new QFormLayout ( valuePage );
valueForm -> setContentsMargins ( 0 , 0 , 0 , 0 );
auto * opCombo = new QComboBox ( valuePage );
opCombo -> addItem ( "equals" , static_cast < int > ( Op :: Equals ) );
opCombo -> addItem ( "is one of" , static_cast < int > ( Op :: In ) );
opCombo -> addItem ( "is true" , static_cast < int > ( Op :: Truthy ) );
opCombo -> addItem ( "is false or empty" , static_cast < int > ( Op :: Falsy ) );
auto * valuesEdit = new QLineEdit ( valuePage );
valuesEdit -> setPlaceholderText ( "value, or several separated by commas" );
valueForm -> addRow ( "Condition:" , opCombo );
valueForm -> addRow ( "Value:" , valuesEdit );
pages -> addWidget ( valuePage );
auto * errorLabel = new QLabel ( & dialog );
errorLabel -> setStyleSheet ( "color: #b00020;" );
errorLabel -> setWordWrap ( true );
form -> addRow ( errorLabel );
auto * buttons = new QDialogButtonBox ( QDialogButtonBox :: Ok | QDialogButtonBox :: Cancel , & dialog );
form -> addRow ( buttons );
auto sourceIsSwitch = [ & ]()
{
const auto * source = definition . findNode ( sourceCombo -> currentText () );
return source && RimWorkflowDefinitionTools :: isSwitchTask ( definition . taskType ( source -> taskId ) );
};
auto isBranchSelected = [ & ]() { return sourceIsSwitch () && fieldCombo -> currentIndex () == 0 ; };
// A condition that picks branches: a case, or equals/in on the branch of the switch
auto isBranchValueGuard = []( const RimWorkflowTaskGuard & guard )
{
const bool onBranch = guard . isSwitchBranch () || guard . field == "branch" ;
return onBranch && ( guard . op == Op :: Case || guard . op == Op :: Equals || guard . op == Op :: In );
};
auto guardFromDialog = [ & ]()
{
RimWorkflowTaskGuard guard { . source = sourceCombo -> currentText () };
if ( isBranchSelected () )
{
if ( otherwiseCheck -> isChecked () )
{
guard . op = Op :: Otherwise ;
return guard ;
}
for ( int row = 0 ; row < branchList -> count (); ++ row )
{
const QListWidgetItem * item = branchList -> item ( row );
if ( item -> checkState () == Qt :: Checked ) guard . values . append ( QJsonValue :: fromVariant ( item -> data ( Qt :: UserRole ) ) );
}
// Keep the `ref: ..., equals/in: ...` form of an existing condition on the branch
if ( guard . source == current . source && isBranchValueGuard ( current ) && current . op != Op :: Case )
{
guard . field = current . field ;
guard . op = current . op == Op :: Equals && guard . values . size () == 1 ? Op :: Equals : Op :: In ;
return guard ;
}
guard . op = Op :: Case ;
guard . caseAsList = guard . values . size () > 1 || ( current . op == Op :: Case && current . caseAsList );
return guard ;
}
guard . field = fieldCombo -> currentText ();
guard . op = static_cast < Op > ( opCombo -> currentData (). toInt () );
if ( guard . op == Op :: Equals || guard . op == Op :: In )
{
guard . values = RimWorkflowDefinitionTools :: guardValuesFromText ( valuesEdit -> text (),
RimWorkflowDefinitionTools :: guardFieldSchema ( definition ,
guard . source ,
guard . field ) );
}
return guard ;
};
auto updateValidity = [ & ]()
{
const bool branch = isBranchSelected ();
pages -> setCurrentIndex ( branch ? 0 : 1 );
branchList -> setEnabled ( ! otherwiseCheck -> isChecked () );
const auto op = static_cast < Op > ( opCombo -> currentData (). toInt () );
valuesEdit -> setEnabled ( op == Op :: Equals || op == Op :: In );
const auto valid = RimWorkflowDefinitionTools :: checkTaskGuard ( definition , taskName , guardFromDialog () );
errorLabel -> setText ( valid ? QString () : valid . error () );
errorLabel -> setVisible ( ! valid );
buttons -> button ( QDialogButtonBox :: Ok ) -> setEnabled ( valid . has_value () );
};
auto fillBranches = [ & ]( const RimWorkflowTaskGuard & checked )
{
const QSignalBlocker blocker ( branchList );
branchList -> clear ();
const auto * source = definition . findNode ( sourceCombo -> currentText () );
if ( ! source ) return ;
for ( const QJsonValue & label : RimWorkflowDefinitionTools :: switchBranches ( definition . taskType ( source -> taskId ) ) )
{
auto * item = new QListWidgetItem ( RimWorkflowDefinitionTools :: guardValueText ( label ), branchList );
item -> setData ( Qt :: UserRole , label . toVariant () );
item -> setFlags ( item -> flags () | Qt :: ItemIsUserCheckable );
const bool isChecked = checked . source == source -> name && isBranchValueGuard ( checked ) && checked . values . contains ( label );
item -> setCheckState ( isChecked ? Qt :: Checked : Qt :: Unchecked );
}
};
auto fillFields = [ & ]( const RimWorkflowTaskGuard & selected )
{
const QSignalBlocker blocker ( fieldCombo );
fieldCombo -> clear ();
if ( sourceIsSwitch () ) fieldCombo -> addItem ( "branch (switch)" );
for ( const QString & field : RimWorkflowDefinitionTools :: guardFields ( definition , sourceCombo -> currentText () ) )
{
if ( sourceIsSwitch () && field == "branch" ) continue ;
fieldCombo -> addItem ( field );
}
const bool sameSource = selected . source == sourceCombo -> currentText ();
if ( sameSource && ! selected . isSwitchBranch () ) fieldCombo -> setCurrentText ( selected . field );
fillBranches ( sameSource ? selected : RimWorkflowTaskGuard {} );
otherwiseCheck -> setChecked ( sameSource && selected . op == Op :: Otherwise );
};
if ( ! current . isSwitchBranch () )
{
opCombo -> setCurrentIndex ( std :: max ( 0 , opCombo -> findData ( static_cast < int > ( current . op ) ) ) );
valuesEdit -> setText ( RimWorkflowDefinitionTools :: guardValuesToText ( current . values ,
RimWorkflowDefinitionTools :: guardFieldSchema ( definition ,
current . source ,
current . field ) ) );
}
fillFields ( current );
QObject :: connect ( sourceCombo ,
& QComboBox :: currentIndexChanged ,
& dialog ,
[ & ]()
{
fillFields ( current );
updateValidity ();
} );
QObject :: connect ( fieldCombo , & QComboBox :: currentIndexChanged , & dialog , updateValidity );
QObject :: connect ( opCombo , & QComboBox :: currentIndexChanged , & dialog , updateValidity );
QObject :: connect ( valuesEdit , & QLineEdit :: textChanged , & dialog , updateValidity );
QObject :: connect ( branchList , & QListWidget :: itemChanged , & dialog , updateValidity );
QObject :: connect ( otherwiseCheck , & QCheckBox :: toggled , & dialog , updateValidity );
QObject :: connect ( buttons , & QDialogButtonBox :: accepted , & dialog , & QDialog :: accept );
QObject :: connect ( buttons , & QDialogButtonBox :: rejected , & dialog , & QDialog :: reject );
updateValidity ();
if ( dialog . exec () != QDialog :: Accepted || ! canEdit () ) return ;
const RimWorkflowDefinitionNode * currentNode = m_workflow -> definition (). findNode ( taskName );
if ( ! currentNode ) return ;
const RimWorkflowTaskGuard guard = guardFromDialog ();
if ( currentNode -> when && * currentNode -> when == guard ) return ;
applyEdit ( RimWorkflowDefinitionTools :: setTaskGuard ( m_workflow -> definition (), taskName , guard ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: removeTaskGuard ( const QString & taskName )
{
if ( ! canEdit () ) return ;
applyEdit ( RimWorkflowDefinitionTools :: setTaskGuard ( m_workflow -> definition (), taskName , std :: nullopt ) );
}
2026-10-02 13:13:27 +02:00
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: renameCollectKey ( const RiuWorkflowGraphView :: ItemRef & item )
{
if ( ! canEdit () ) return ;
const RimWorkflowDefinitionEdge * found = findEdge ( item );
if ( ! found || found -> collect != RimWorkflowDefinitionEdge :: Collect :: Dict ) return ;
// A copy, as the workflow may change or be deleted while the dialog is open
const RimWorkflowDefinitionEdge edge = * found ;
bool ok = false ;
const QString key =
QInputDialog :: getText ( this , "Rename Key" , QString ( "Key of '%1' in '%2':" ). arg ( edge . from , edge . input ), QLineEdit :: Normal , edge . key , & ok )
. trimmed ();
if ( ! ok || key == edge . key || ! canEdit () ) return ;
applyEdit ( RimWorkflowDefinitionTools :: setCollectKey ( m_workflow -> definition (), edge , key ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: moveCollectMember ( const RiuWorkflowGraphView :: ItemRef & item , int delta )
{
if ( ! canEdit () ) return ;
const RimWorkflowDefinitionEdge * edge = findEdge ( item );
if ( ! edge ) return ;
applyEdit ( RimWorkflowDefinitionTools :: moveCollectMember ( m_workflow -> definition (), * edge , delta ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: showMessage ( const QString & message )
{
m_message = message ;
updateStatus ();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: showWorkflow ( RiuWorkflowGraphView :: ViewportPolicy policy )
{
if ( ! m_workflow )
{
m_graphView -> setEditable ( false );
m_graphView -> showGraph ( {}, {}, policy );
}
else
{
m_graphView -> setEditable ( m_workflow -> isEditable () && ! m_workflow -> isLocked () );
m_graphView -> showGraph ( m_workflow -> graph (), m_workflow -> loadError (), policy );
updateTaskInputValues ();
if ( m_job ) updateJobState ();
}
m_palette -> setEnabled ( m_workflow && m_workflow -> isEditable () && ! m_workflow -> isLocked () );
updateActions ();
updateStatus ();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: showContextMenu ( const RiuWorkflowGraphView :: ItemRef & item , const QPoint & globalPos , const QPointF & scenePos )
{
if ( ! m_workflow ) return ;
const bool editable = m_workflow -> isEditable () && ! m_workflow -> isLocked ();
QMenu menu ;
if ( item . kind == RiuWorkflowGraphView :: ItemRef :: Kind :: Task && editable )
{
const QString task = item . task ;
addMenuAction ( & menu , "Rename..." , [ this , task ]() { renameTask ( task ); } );
addMenuAction ( & menu , "Set as Result Task" , [ this , task ]() { setResultTask ( task ); } )
-> setEnabled ( RimWorkflowDefinitionTools :: effectiveResultTask ( m_workflow -> definition () ) != task ||
m_workflow -> definition (). resultTask . isEmpty () );
appendOptionalInputsMenu ( & menu , task );
appendMapMenu ( & menu , task );
2026-10-26 14:19:29 +01:00
appendConditionMenu ( & menu , task );
2026-10-02 13:13:27 +02:00
menu . addSeparator ();
addMenuAction ( & menu , "Delete Task" , [ this , item ]() { deleteItem ( item ); }, ":/Erase.svg" );
}
else if ( item . kind == RiuWorkflowGraphView :: ItemRef :: Kind :: Edge && editable )
{
appendCollectMenu ( & menu , item );
addMenuAction ( & menu , "Delete Connection" , [ this , item ]() { deleteItem ( item ); }, ":/Erase.svg" );
}
2026-10-26 14:19:29 +01:00
else if ( item . kind == RiuWorkflowGraphView :: ItemRef :: Kind :: Condition && editable )
{
const QString task = item . to ;
addMenuAction ( & menu , "Edit Condition..." , [ this , task ]() { editTaskGuard ( task ); } );
addMenuAction ( & menu , "Remove Condition" , [ this , task ]() { removeTaskGuard ( task ); }, ":/Erase.svg" );
}
2026-10-02 13:13:27 +02:00
else
{
if ( editable )
{
appendAddTaskMenu ( & menu , scenePos );
menu . addSeparator ();
menu . addAction ( m_undoAction );
menu . addAction ( m_redoAction );
menu . addSeparator ();
menu . addAction ( m_saveAction );
menu . addAction ( m_saveAsAction );
}
menu . addAction ( m_duplicateAction );
menu . addSeparator ();
menu . addAction ( m_fitAction );
}
if ( ! menu . isEmpty () ) menu . exec ( globalPos );
}
//--------------------------------------------------------------------------------------------------
/// The installed task types, grouped by package
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: appendAddTaskMenu ( QMenu * menu , const QPointF & scenePos )
{
QMenu * addMenu = menu -> addMenu ( "Add Task" );
const RimWorkflowTaskCatalog & catalog = RimWorkflowTaskCatalog :: instance ();
if ( ! catalog . isValid () )
{
addMenu -> addAction ( QString ( "Task catalog unavailable: %1" ). arg ( catalog . loadError () ) ) -> setEnabled ( false );
return ;
}
std :: map < QString , std :: vector < QJsonObject >> groups ;
for ( const QJsonObject & task : catalog . tasks () )
{
const QString id = task . value ( "id" ). toString ();
groups [ id . contains ( '.' ) ? id . section ( '.' , 0 , - 2 ) : QString ( "other" )]. push_back ( task );
}
QSet < QString > usedTexts ;
for ( auto & [ group , tasks ] : groups )
{
std :: sort ( tasks . begin (),
tasks . end (),
[]( const QJsonObject & a , const QJsonObject & b ) { return a . value ( "id" ). toString () < b . value ( "id" ). toString (); } );
QMenu * groupMenu = groups . size () > 1 ? addMenu -> addMenu ( group ) : addMenu ;
for ( const QJsonObject & task : tasks )
{
const QString id = task . value ( "id" ). toString ();
QString text = task . value ( "name" ). toString ( id );
if ( text . isEmpty () || usedTexts . contains ( text ) ) text = id ;
usedTexts . insert ( text );
addMenuAction (
groupMenu , text , [ this , id , scenePos ]() { addTask ( id , scenePos ); }, ":/Plus.png" )
-> setToolTip ( task . value ( "description" ). toString () );
}
}
addMenu -> setToolTipsVisible ( true );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: appendMapMenu ( QMenu * menu , const QString & taskName )
{
const RimWorkflowDefinitionNode * node = m_workflow -> definition (). findNode ( taskName );
if ( ! node ) return ;
addMenuAction ( menu , node -> map ? "Edit Map..." : "Map Over..." , [ this , taskName ]() { editTaskMap ( taskName ); }, ":/Refresh.svg" ) -> setToolTip ( "Run the task once for each item of a mapping and collect the outputs by key" );
if ( node -> map ) addMenuAction ( menu , "Remove Map" , [ this , taskName ]() { removeTaskMap ( taskName ); } );
}
2026-10-26 14:19:29 +01:00
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: appendConditionMenu ( QMenu * menu , const QString & taskName )
{
const RimWorkflowDefinitionNode * node = m_workflow -> definition (). findNode ( taskName );
if ( ! node ) return ;
addMenuAction ( menu , node -> when ? "Edit Condition..." : "Run When..." , [ this , taskName ]() { editTaskGuard ( taskName ); } )
-> setToolTip ( "Run the task only for some branches of a switch task, or when an output of another task has a value" );
if ( node -> when ) addMenuAction ( menu , "Remove Condition" , [ this , taskName ]() { removeTaskGuard ( taskName ); } );
}
2026-10-02 13:13:27 +02:00
//--------------------------------------------------------------------------------------------------
/// Collected connections: dict members have a key, list members an order
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: appendCollectMenu ( QMenu * menu , const RiuWorkflowGraphView :: ItemRef & item )
{
const RimWorkflowDefinitionEdge * edge = findEdge ( item );
2026-10-26 14:19:29 +01:00
if ( ! edge || ( ! edge -> isCollected () && ! edge -> isFirstOf () ) ) return ;
2026-10-02 13:13:27 +02:00
if ( edge -> collect == RimWorkflowDefinitionEdge :: Collect :: Dict )
{
addMenuAction ( menu , "Rename Key..." , [ this , item ]() { renameCollectKey ( item ); } );
}
else
{
int index = 0 ;
int count = 0 ;
for ( const auto & other : m_workflow -> definition (). edges )
{
2026-10-26 14:19:29 +01:00
if ( other . to != edge -> to || other . input != edge -> input || other . collect != edge -> collect ) continue ;
2026-10-02 13:13:27 +02:00
if ( & other == edge ) index = count ;
++ count ;
}
for ( const auto & [ text , delta ] : { std :: pair { QString ( "Move Earlier" ), - 1 }, std :: pair { QString ( "Move Later" ), 1 } } )
{
addMenuAction ( menu , text , [ this , item , delta = delta ]() { moveCollectMember ( item , delta ); } )
-> setEnabled ( index + delta >= 0 && index + delta < count );
}
}
menu -> addSeparator ();
}
//--------------------------------------------------------------------------------------------------
/// Optional inputs are configured by the user only when checked; connected inputs are disabled
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: appendOptionalInputsMenu ( QMenu * menu , const QString & taskName )
{
const RimWorkflowDefinition & definition = m_workflow -> definition ();
const RimWorkflowDefinitionNode * node = definition . findNode ( taskName );
if ( ! node ) return ;
std :: vector < RimWorkflowPort > optionalPorts ;
for ( const auto & port : RimWorkflowPortCompatibility :: inputPorts ( definition . taskTypeForNode ( taskName ) ) )
{
if ( ! port . isWhole () && ! port . required ) optionalPorts . push_back ( port );
}
if ( optionalPorts . empty () ) return ;
QStringList connected ;
for ( const auto & edge : definition . incomingEdges ( taskName ) )
connected . append ( edge . input );
QMenu * optionalMenu = menu -> addMenu ( "Optional Inputs" );
for ( const auto & port : optionalPorts )
{
const bool configured = node -> explicitConfigFields . contains ( port . name );
const QString field = port . name ;
QAction * action = addMenuAction ( optionalMenu ,
field ,
[ this , taskName , field , configured ]()
{ setOptionalInputConfigured ( taskName , field , ! configured ); } );
action -> setCheckable ( true );
action -> setChecked ( configured );
action -> setEnabled ( ! connected . contains ( field ) );
action -> setToolTip ( port . description );
}
optionalMenu -> setToolTipsVisible ( true );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: updateActions ()
{
RimWorkflow * workflow = m_workflow . p ();
const bool editable = workflow && workflow -> isEditable () && ! workflow -> isLocked ();
m_saveAction -> setEnabled ( editable && ( workflow -> isDirty () || workflow -> source () == RimWorkflow :: Source :: Unsaved ) );
m_saveAsAction -> setEnabled ( editable );
m_undoAction -> setEnabled ( editable && workflow -> canUndo () );
m_redoAction -> setEnabled ( editable && workflow -> canRedo () );
m_duplicateAction -> setEnabled ( workflow && workflow -> loadError (). isEmpty () && workflow -> definition (). editable );
m_fitAction -> setEnabled ( workflow != nullptr );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: updateStatus ()
{
RimWorkflow * workflow = m_workflow . p ();
QStringList lines ;
QString color ;
if ( ! workflow )
{
m_statusLabel -> clear ();
return ;
}
if ( ! workflow -> loadError (). isEmpty () )
{
lines << workflow -> loadError ();
color = "#c0392b" ;
}
else if ( ! workflow -> isEditable () )
{
lines << QString ( "Read-only: %1. Use Duplicate as Editable to change it." ). arg ( workflow -> readOnlyReason () );
color = "#7f8c8d" ;
}
else
{
if ( ! m_message . isEmpty () )
{
lines << m_message ;
color = "#c0392b" ;
}
if ( workflow -> isLocked () ) lines << "The workflow is locked while a job is running." ;
const QJsonArray issues = workflow -> graph (). value ( "issues" ). toArray ();
for ( const QJsonValue & issue : issues )
lines << issue . toObject (). value ( "message" ). toString ();
if ( m_validationRunner -> isRunning () || ! workflow -> isValidationCurrent () )
{
lines << "Validating..." ;
}
else if ( workflow -> lastValidationFailed () )
{
if ( issues . isEmpty () ) lines << "taskmaestro reports problems; see the marked tasks." ;
if ( color . isEmpty () ) color = "#d35400" ;
}
else if ( issues . isEmpty () && m_message . isEmpty () )
{
lines << "Valid" ;
if ( color . isEmpty () ) color = "#27ae60" ;
}
if ( ! issues . isEmpty () && color . isEmpty () ) color = "#d35400" ;
if ( workflow -> source () == RimWorkflow :: Source :: Unsaved )
lines << "Not saved yet" ;
else if ( workflow -> isDirty () )
lines << "Unsaved changes" ;
}
m_statusLabel -> setStyleSheet ( color . isEmpty () ? QString () : QString ( "color: %1;" ). arg ( color ) );
m_statusLabel -> setText ( lines . join ( " | " ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: scheduleValidation ()
{
if ( m_workflow && m_workflow -> isEditable () && ! m_workflow -> isValidationCurrent () ) m_validationRunner -> schedule ( m_workflow );
updateStatus ();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget :: onValidationFinished ( RimWorkflow * workflow )
{
if ( workflow != m_workflow . p () ) return ;
updateActions ();
updateStatus ();
}