Files
ResInsight/ApplicationLibCode/UserInterface/RiuWorkflowEditorWidget.cpp
T
Kristian Bendiksen ec59629930 #15094 Workflows: Add vertical graph layout option
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.
2026-11-04 17:26:39 +01:00

1614 lines
71 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 "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>
#include <QApplication>
#include <QCheckBox>
#include <QComboBox>
#include <QCursor>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QInputDialog>
#include <QJsonArray>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSplitter>
#include <QStackedWidget>
#include <QTabBar>
#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_inlineConfigAction = addAction( "Inline Config Values",
"",
[this]()
{
if ( !m_workflow ) return;
m_workflow->setInlineConfigValues( m_inlineConfigAction->isChecked() );
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Keep );
} );
m_inlineConfigAction->setCheckable( true );
m_inlineConfigAction->setToolTip( "Show the job configuration values in the task boxes instead of in separate config boxes" );
// Tasks the user has moved are laid out again, since their positions suit the old orientation
m_verticalLayoutAction = addAction( "Vertical Layout",
"",
[this]()
{
if ( !m_workflow ) return;
m_workflow->setVerticalLayout( m_verticalLayoutAction->isChecked() );
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Fit );
} );
m_verticalLayoutAction->setCheckable( true );
m_verticalLayoutAction->setToolTip( "Lay out the graph from top to bottom instead of from left to right" );
m_tabs.push_back( {} );
m_tabBar = new QTabBar( this );
m_tabBar->setDocumentMode( true );
m_tabBar->setExpanding( false );
m_tabBar->setTabsClosable( true );
m_tabBar->setVisible( false );
layout->addWidget( m_tabBar );
connect( m_tabBar, &QTabBar::currentChanged, this, [this]( int index ) { activateTab( index, true ); } );
connect( m_tabBar, &QTabBar::tabCloseRequested, this, &RiuWorkflowEditorWidget::closeTab );
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 );
// Queued, as the graph is rebuilt and the view must not delete items while handling the click
connect(
m_graphView,
&RiuWorkflowGraphView::collapseToggled,
this,
[this]( const QString& taskName ) { setTaskCollapsed( taskName, m_workflow && !m_workflow->isTaskCollapsed( taskName ) ); },
Qt::QueuedConnection );
connect( m_graphView, &RiuWorkflowGraphView::openNestedRequested, this, &RiuWorkflowEditorWidget::openNestedWorkflow, Qt::QueuedConnection );
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_tabs.front().workflow.p();
if ( workflowChanged )
{
m_validationRunner->cancel();
m_message.clear();
closeNestedTabs();
}
m_tabs.front().workflow = workflow;
m_rootJob = job;
const bool currentTabRemoved = !workflowChanged && reopenNestedTabs();
updateTabBar();
m_workflow = m_tabs[m_currentTab].workflow;
m_job = m_currentTab == 0 ? job : nullptr;
auto policy = workflowChanged ? RiuWorkflowGraphView::ViewportPolicy::Fit : RiuWorkflowGraphView::ViewportPolicy::Keep;
if ( currentTabRemoved )
{
// The view state of the graph belongs to the removed tab
const auto& viewState = m_tabs[m_currentTab].viewState;
if ( viewState )
m_graphView->setViewState( *viewState );
else
policy = RiuWorkflowGraphView::ViewportPolicy::Fit;
}
showWorkflow( policy );
scheduleValidation();
}
//--------------------------------------------------------------------------------------------------
/// The workflow of the first tab
//--------------------------------------------------------------------------------------------------
RimWorkflow* RiuWorkflowEditorWidget::workflow() const
{
return m_tabs.front().workflow.p();
}
//--------------------------------------------------------------------------------------------------
/// The workflow of the current tab
//--------------------------------------------------------------------------------------------------
RimWorkflow* RiuWorkflowEditorWidget::displayedWorkflow() const
{
return m_workflow.p();
}
//--------------------------------------------------------------------------------------------------
/// The job shown in the graph; null while a nested workflow is shown
//--------------------------------------------------------------------------------------------------
RimWorkflowJob* RiuWorkflowEditorWidget::job() const
{
return m_job.p();
}
//--------------------------------------------------------------------------------------------------
/// The job of the workflow of the first tab
//--------------------------------------------------------------------------------------------------
RimWorkflowJob* RiuWorkflowEditorWidget::rootJob() const
{
return m_rootJob.p();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::clear()
{
m_validationRunner->cancel();
closeNestedTabs();
m_tabs.front().workflow = nullptr;
m_workflow = nullptr;
m_job = nullptr;
m_rootJob = nullptr;
m_message.clear();
updateTabBar();
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Fit );
}
//--------------------------------------------------------------------------------------------------
/// Show the workflow of a nested task of the current tab in a tab of its own
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::openNestedWorkflow( const QString& taskName )
{
if ( !m_workflow ) return;
QStringList taskPath = m_tabs[m_currentTab].taskPath;
taskPath.append( taskName );
for ( size_t i = 0; i < m_tabs.size(); ++i )
{
if ( m_tabs[i].taskPath == taskPath )
{
activateTab( static_cast<int>( i ), true );
return;
}
}
QApplication::setOverrideCursor( Qt::WaitCursor );
RimWorkflow* nested = m_workflow->nestedWorkflow( taskName );
QApplication::restoreOverrideCursor();
if ( !nested )
{
showMessage( QString( "Task '%1' is not a nested workflow" ).arg( taskName ) );
return;
}
m_tabs.push_back( { nested, taskPath, std::nullopt } );
activateTab( static_cast<int>( m_tabs.size() ) - 1, true );
}
//--------------------------------------------------------------------------------------------------
/// Show the workflow of a tab. The view of the tab shown before is kept for when it is shown again.
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::activateTab( int index, bool keepViewState )
{
if ( index < 0 || index >= static_cast<int>( m_tabs.size() ) ) return;
if ( index == m_currentTab && m_workflow == m_tabs[index].workflow ) return;
if ( keepViewState && m_currentTab < static_cast<int>( m_tabs.size() ) ) m_tabs[m_currentTab].viewState = m_graphView->viewState();
m_validationRunner->cancel();
m_message.clear();
m_currentTab = index;
m_workflow = m_tabs[index].workflow;
m_job = index == 0 ? m_rootJob : nullptr;
updateTabBar();
if ( m_tabs[index].viewState )
{
m_graphView->setViewState( *m_tabs[index].viewState );
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Keep );
}
else
{
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Fit );
}
scheduleValidation();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::closeTab( int index )
{
if ( index <= 0 || index >= static_cast<int>( m_tabs.size() ) ) return;
const bool wasCurrent = index == m_currentTab;
m_tabs.erase( m_tabs.begin() + index );
if ( wasCurrent )
{
m_currentTab = index - 1;
m_workflow = nullptr; // Make activateTab() show the tab
activateTab( index - 1, false );
return;
}
if ( m_currentTab > index ) --m_currentTab;
updateTabBar();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::closeNestedTabs()
{
m_tabs.resize( 1 );
m_currentTab = 0;
}
//--------------------------------------------------------------------------------------------------
/// A reloaded workflow forgets its nested workflows. Load them again for the open tabs, and close
/// the tabs of tasks that are gone. Returns true if the current tab was closed.
//--------------------------------------------------------------------------------------------------
bool RiuWorkflowEditorWidget::reopenNestedTabs()
{
bool currentTabRemoved = false;
for ( int i = static_cast<int>( m_tabs.size() ) - 1; i > 0; --i )
{
Tab& tab = m_tabs[i];
if ( tab.workflow ) continue;
RimWorkflow* workflow = m_tabs.front().workflow.p();
for ( const QString& taskName : tab.taskPath )
workflow = workflow ? workflow->nestedWorkflow( taskName ) : nullptr;
tab.workflow = workflow;
if ( workflow ) continue;
m_tabs.erase( m_tabs.begin() + i );
if ( m_currentTab == i ) currentTabRemoved = true;
if ( m_currentTab >= i ) m_currentTab = std::max( 0, m_currentTab - 1 );
}
return currentTabRemoved;
}
//--------------------------------------------------------------------------------------------------
/// The tab bar is shown when a nested workflow is open
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::updateTabBar()
{
QSignalBlocker blocker( m_tabBar );
const int count = static_cast<int>( m_tabs.size() );
while ( m_tabBar->count() > count )
m_tabBar->removeTab( m_tabBar->count() - 1 );
while ( m_tabBar->count() < count )
m_tabBar->addTab( QString() );
RimWorkflow* root = m_tabs.front().workflow.p();
const QString rootName = root ? root->name() : QString();
for ( int i = 0; i < count; ++i )
{
const Tab& tab = m_tabs[i];
RimWorkflow* workflow = tab.workflow.p();
if ( i == 0 )
{
m_tabBar->setTabText( i, rootName );
m_tabBar->setTabToolTip( i, QString( "Workflow '%1'" ).arg( rootName ) );
for ( auto side : { QTabBar::LeftSide, QTabBar::RightSide } )
{
if ( m_tabBar->tabButton( i, side ) ) m_tabBar->setTabButton( i, side, nullptr );
}
continue;
}
m_tabBar->setTabText( i, tab.taskPath.last() );
m_tabBar->setTabToolTip( i,
QString( "Nested workflow '%1' of task %2" )
.arg( workflow ? workflow->name() : QString(),
( QStringList( rootName ) + tab.taskPath ).join( QString( " > " ) ) ) );
}
m_tabBar->setCurrentIndex( m_currentTab );
m_tabBar->setVisible( count > 1 );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiuWorkflowEditorWidget::isNestedTabShown() const
{
return m_currentTab > 0;
}
//--------------------------------------------------------------------------------------------------
/// Show the current revision of the workflow without moving the view
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::refresh()
{
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Keep );
scheduleValidation();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::updateJobState()
{
// The tasks of a nested workflow have no states; the status line shows the state of the outer task
if ( isNestedTabShown() )
{
updateStatus();
return;
}
// A hidden task is shown again when it fails
if ( effectiveHiddenTasks() != m_shownHiddenTasks )
{
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Keep );
return;
}
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;
// A hidden task is shown again when it gets errors
if ( effectiveHiddenTasks() != m_shownHiddenTasks )
{
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Keep );
return;
}
for ( const QJsonValue& value : m_workflow->graph().value( "tasks" ).toArray() )
{
const QJsonObject graphTask = value.toObject();
m_graphView->setTaskIssues( graphTask.value( "name" ).toString(), taskIssues( graphTask ) );
}
}
//--------------------------------------------------------------------------------------------------
/// The issues of a task of the workflow graph, with missing-value warnings taken from the displayed job
//--------------------------------------------------------------------------------------------------
QJsonArray RiuWorkflowEditorWidget::taskIssues( const QJsonObject& graphTask ) const
{
const QJsonArray issues = graphTask.value( "issues" ).toArray();
if ( !m_job ) return issues;
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() )
{
if ( binding && binding->fieldName() == fieldName )
hasValue = !binding->toJsonValue().isNull() || binding->isExplicitNull();
}
}
fieldHasValue[fieldName] = hasValue || !schema.value( "required" ).toBool();
}
return RimWorkflowValidationTools::issuesWithJobValues( taskName, issues, fieldHasValue );
}
//--------------------------------------------------------------------------------------------------
/// The hidden tasks of the workflow, except those that have failed or have errors
//--------------------------------------------------------------------------------------------------
QStringList RiuWorkflowEditorWidget::effectiveHiddenTasks() const
{
if ( !m_workflow ) return {};
const QStringList hidden = m_workflow->hiddenTasks();
if ( hidden.isEmpty() ) return {};
const auto states = m_job ? m_job->taskStates() : decltype( m_job->taskStates() )();
QStringList result;
for ( const QJsonValue& value : m_workflow->graph().value( "tasks" ).toArray() )
{
const QJsonObject graphTask = value.toObject();
const QString taskName = graphTask.value( "name" ).toString();
if ( !hidden.contains( taskName ) || states.value( taskName ) == "failed" ) continue;
const QJsonArray issues = taskIssues( graphTask );
const bool hasErrors = std::any_of( issues.begin(),
issues.end(),
[]( const QJsonValue& issue )
{ return issue.toObject().value( "severity" ).toString() != "warning"; } );
if ( !hasErrors ) result.append( taskName );
}
return result;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::setTaskHidden( const QString& taskName, bool hidden )
{
if ( !m_workflow ) return;
m_workflow->setTaskHidden( taskName, hidden );
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Keep );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::setTaskCollapsed( const QString& taskName, bool collapsed )
{
if ( !m_workflow ) return;
m_workflow->setTaskCollapsed( taskName, collapsed );
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Keep );
}
//--------------------------------------------------------------------------------------------------
/// Actions changing how a task is shown, available also when the workflow can't be edited
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::appendTaskViewActions( QMenu* menu, const QString& taskName )
{
if ( m_workflow->isNestedTask( taskName ) )
{
addMenuAction( menu, "Open Nested Workflow", [this, taskName]() { openNestedWorkflow( taskName ); } );
menu->addSeparator();
}
const bool collapsed = m_workflow->isTaskCollapsed( taskName );
addMenuAction( menu,
collapsed ? "Expand Task" : "Collapse Task",
[this, taskName, collapsed]() { setTaskCollapsed( taskName, !collapsed ); } );
addMenuAction( menu, "Hide Task", [this, taskName]() { setTaskHidden( taskName, true ); } );
}
//--------------------------------------------------------------------------------------------------
/// Collapse or expand all tasks of the workflow
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::appendCollapseMenu( QMenu* menu )
{
QStringList taskNames;
for ( const QJsonValue& task : m_workflow->graph().value( "tasks" ).toArray() )
taskNames.append( task.toObject().value( "name" ).toString() );
const QStringList collapsed = m_workflow->collapsedTasks();
addMenuAction( menu,
"Collapse All Tasks",
[this, taskNames]()
{
if ( !m_workflow ) return;
m_workflow->setCollapsedTasks( taskNames );
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Keep );
} )
->setEnabled( collapsed.size() < taskNames.size() );
addMenuAction( menu,
"Expand All Tasks",
[this]()
{
if ( !m_workflow ) return;
m_workflow->setCollapsedTasks( {} );
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Keep );
} )
->setEnabled( !collapsed.isEmpty() );
}
//--------------------------------------------------------------------------------------------------
/// Show the hidden tasks again, one at a time or all
//--------------------------------------------------------------------------------------------------
void RiuWorkflowEditorWidget::appendShowHiddenMenu( QMenu* menu )
{
QMenu* showMenu = menu->addMenu( "Show Hidden Tasks" );
const QStringList hidden = m_workflow ? m_workflow->hiddenTasks() : QStringList();
showMenu->setEnabled( !hidden.isEmpty() );
for ( const QString& taskName : hidden )
addMenuAction( showMenu, taskName, [this, taskName]() { setTaskHidden( taskName, false ); } );
if ( hidden.size() > 1 )
{
showMenu->addSeparator();
addMenuAction( showMenu,
"Show All",
[this]()
{
if ( !m_workflow ) return;
m_workflow->showAllTasks();
showWorkflow( RiuWorkflowGraphView::ViewportPolicy::Keep );
} );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
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 ) );
}
else if ( item.kind == RiuWorkflowGraphView::ItemRef::Kind::Condition )
{
applyEdit( RimWorkflowDefinitionTools::setTaskGuard( m_workflow->definition(), item.to, std::nullopt ) );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
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 };
// 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;
}
}
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 ) );
}
//--------------------------------------------------------------------------------------------------
/// 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 ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
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->setInlineConfigValues( m_workflow->inlineConfigValues() );
m_graphView->setOrientation( m_workflow->verticalLayout() ? RiuWorkflowGraphLayout::Orientation::Vertical
: RiuWorkflowGraphLayout::Orientation::Horizontal );
m_shownHiddenTasks = effectiveHiddenTasks();
m_graphView->setHiddenTasks( m_shownHiddenTasks );
m_graphView->setCollapsedTasks( m_workflow->collapsedTasks() );
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;
appendTaskViewActions( &menu, task );
}
else if ( item.kind == RiuWorkflowGraphView::ItemRef::Kind::Task && editable )
{
const QString task = item.task;
appendTaskViewActions( &menu, task );
menu.addSeparator();
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 );
appendConditionMenu( &menu, task );
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" );
}
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" );
}
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 );
menu.addAction( m_inlineConfigAction );
menu.addAction( m_verticalLayoutAction );
appendCollapseMenu( &menu );
appendShowHiddenMenu( &menu );
}
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 ); } );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
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 ); } );
}
//--------------------------------------------------------------------------------------------------
/// 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 );
if ( !edge || ( !edge->isCollected() && !edge->isFirstOf() ) ) return;
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 )
{
if ( other.to != edge->to || other.input != edge->input || other.collect != edge->collect ) continue;
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 && !isNestedTabShown() && workflow->loadError().isEmpty() && workflow->definition().editable );
m_fitAction->setEnabled( workflow != nullptr );
m_inlineConfigAction->setEnabled( workflow != nullptr );
m_inlineConfigAction->setChecked( !workflow || workflow->inlineConfigValues() );
m_verticalLayoutAction->setEnabled( workflow != nullptr );
m_verticalLayoutAction->setChecked( workflow && workflow->verticalLayout() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
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 ( isNestedTabShown() )
{
const QStringList taskPath = m_tabs[m_currentTab].taskPath;
lines << QString( "Nested workflow '%1' of task %2 (read-only)" ).arg( workflow->name(), taskPath.join( QString( " > " ) ) );
color = "#7f8c8d";
if ( !m_message.isEmpty() ) lines.prepend( m_message );
// taskmaestro runs a nested workflow as one task, so only the state of the outer task is known
const QString state = m_rootJob ? m_rootJob->taskStates().value( taskPath.first() ) : QString();
if ( !state.isEmpty() )
{
lines << QString( "Task '%1' %2" ).arg( taskPath.first(), state );
if ( state == "failed" ) color = "#c0392b";
if ( state == "running" ) color = "#9e7010";
if ( state == "completed" ) color = "#27ae60";
}
}
else if ( !workflow->isEditable() )
{
if ( !m_message.isEmpty() ) lines << m_message;
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();
}