///////////////////////////////////////////////////////////////////////////////// // // 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 // 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QAction* addMenuAction( QMenu* menu, const QString& text, std::function 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 { 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( 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( 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( m_tabs.size() ) ) return; if ( index == m_currentTab && m_workflow == m_tabs[index].workflow ) return; if ( keepViewState && m_currentTab < static_cast( 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( 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( 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( 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 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& result, const QMap& 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& 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& 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( Op::Equals ) ); opCombo->addItem( "is one of", static_cast( Op::In ) ); opCombo->addItem( "is true", static_cast( Op::Truthy ) ); opCombo->addItem( "is false or empty", static_cast( 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( 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( 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( 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> 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 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 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(); }