From 47f96e246621660b0b941a0a6df9f1b5da33c7dd Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 7 Apr 2020 14:52:46 +1000 Subject: [PATCH] [FEATURE][processing] Allow copying/cut/paste of model components This commit allows users to copy and paste model components, both within the same model and between different models --- .../models/qgsprocessingmodelgroupbox.sip.in | 2 +- .../models/qgsmodelgraphicsscene.sip.in | 4 + .../models/qgsmodelgraphicsview.sip.in | 56 ++++ .../models/qgsprocessingmodelgroupbox.cpp | 5 +- .../models/qgsprocessingmodelgroupbox.h | 2 +- .../models/qgsmodeldesignerdialog.cpp | 46 +++ .../models/qgsmodeldesignerdialog.h | 3 + .../models/qgsmodelgraphicsscene.cpp | 12 + .../processing/models/qgsmodelgraphicsscene.h | 6 + .../models/qgsmodelgraphicsview.cpp | 272 +++++++++++++++++- .../processing/models/qgsmodelgraphicsview.h | 53 ++++ 11 files changed, 456 insertions(+), 5 deletions(-) diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelgroupbox.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelgroupbox.sip.in index fe9449389ca..8d3172ae09d 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelgroupbox.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelgroupbox.sip.in @@ -38,7 +38,7 @@ Saves this group box to a QVariant. .. seealso:: :py:func:`loadVariant` %End - bool loadVariant( const QVariantMap &map ); + bool loadVariant( const QVariantMap &map, bool ignoreUuid = false ); %Docstring Loads this group box from a QVariantMap. diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 701e1e2d599..f137b224099 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -51,6 +51,10 @@ QGraphicsScene subclass representing the model designer. Constructor for QgsModelGraphicsScene with the specified ``parent`` object. %End + QgsProcessingModelAlgorithm *model(); + + void setModel( QgsProcessingModelAlgorithm *model ); + void setFlags( QgsModelGraphicsScene::Flags flags ); %Docstring Sets the combination of ``flags`` controlling how the scene is rendered and behaves. diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in index 5b3846320e3..cb08f5767bf 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in @@ -80,6 +80,47 @@ Starts a macro command, containing a group of interactions in the view. Ends a macro command, containing a group of interactions in the view. %End + + enum ClipboardOperation + { + ClipboardCut, + ClipboardCopy, + }; + + void copySelectedItems( ClipboardOperation operation ); +%Docstring +Cuts or copies the selected items, respecting the specified ``operation``. + +.. seealso:: :py:func:`copyItems` + +.. seealso:: :py:func:`pasteItems` +%End + + void copyItems( const QList< QgsModelComponentGraphicItem * > &items, ClipboardOperation operation ); +%Docstring +Cuts or copies the a list of ``items``, respecting the specified ``operation``. + +.. seealso:: :py:func:`copySelectedItems` + +.. seealso:: :py:func:`pasteItems` +%End + + enum PasteMode + { + PasteModeCursor, + PasteModeCenter, + PasteModeInPlace, + }; + + void pasteItems( PasteMode mode ); +%Docstring +Pastes items from clipboard, using the specified ``mode``. + +.. seealso:: :py:func:`copySelectedItems` + +.. seealso:: :py:func:`hasItemsInClipboard` +%End + public slots: void snapSelected(); @@ -120,6 +161,21 @@ Emitted when a macro command containing a group of interactions is started in th void macroCommandEnded(); %Docstring Emitted when a macro command containing a group of interactions in the view has ended. +%End + + void beginCommand( const QString &text ); +%Docstring +Emitted when an undo command is started in the view. +%End + + void endCommand(); +%Docstring +Emitted when an undo command in the view has ended. +%End + + void deleteSelectedItems(); +%Docstring +Emitted when the selected items should be deleted; %End }; diff --git a/src/core/processing/models/qgsprocessingmodelgroupbox.cpp b/src/core/processing/models/qgsprocessingmodelgroupbox.cpp index dd4bf7428af..88a1fda9df7 100644 --- a/src/core/processing/models/qgsprocessingmodelgroupbox.cpp +++ b/src/core/processing/models/qgsprocessingmodelgroupbox.cpp @@ -39,10 +39,11 @@ QVariant QgsProcessingModelGroupBox::toVariant() const return map; } -bool QgsProcessingModelGroupBox::loadVariant( const QVariantMap &map ) +bool QgsProcessingModelGroupBox::loadVariant( const QVariantMap &map, bool ignoreUuid ) { restoreCommonProperties( map ); - mUuid = map.value( QStringLiteral( "uuid" ) ).toString(); + if ( !ignoreUuid ) + mUuid = map.value( QStringLiteral( "uuid" ) ).toString(); return true; } diff --git a/src/core/processing/models/qgsprocessingmodelgroupbox.h b/src/core/processing/models/qgsprocessingmodelgroupbox.h index a5d194e2555..ea68322b4f4 100644 --- a/src/core/processing/models/qgsprocessingmodelgroupbox.h +++ b/src/core/processing/models/qgsprocessingmodelgroupbox.h @@ -51,7 +51,7 @@ class CORE_EXPORT QgsProcessingModelGroupBox : public QgsProcessingModelComponen * Loads this group box from a QVariantMap. * \see toVariant() */ - bool loadVariant( const QVariantMap &map ); + bool loadVariant( const QVariantMap &map, bool ignoreUuid = false ); /** * Returns the unique ID associated with this group box. diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index de6fc2a2187..a01704dadd1 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -166,6 +166,39 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags mMenuView->insertMenu( mActionZoomIn, mGroupMenu ); connect( mGroupMenu, &QMenu::aboutToShow, this, &QgsModelDesignerDialog::populateZoomToMenu ); + //cut/copy/paste actions. Note these are not included in the ui file + //as ui files have no support for QKeySequence shortcuts + mActionCut = new QAction( tr( "Cu&t" ), this ); + mActionCut->setShortcuts( QKeySequence::Cut ); + mActionCut->setStatusTip( tr( "Cut" ) ); + mActionCut->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditCut.svg" ) ) ); + connect( mActionCut, &QAction::triggered, this, [ = ] + { + mView->copySelectedItems( QgsModelGraphicsView::ClipboardCut ); + } ); + + mActionCopy = new QAction( tr( "&Copy" ), this ); + mActionCopy->setShortcuts( QKeySequence::Copy ); + mActionCopy->setStatusTip( tr( "Copy" ) ); + mActionCopy->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditCopy.svg" ) ) ); + connect( mActionCopy, &QAction::triggered, this, [ = ] + { + mView->copySelectedItems( QgsModelGraphicsView::ClipboardCopy ); + } ); + + mActionPaste = new QAction( tr( "&Paste" ), this ); + mActionPaste->setShortcuts( QKeySequence::Paste ); + mActionPaste->setStatusTip( tr( "Paste" ) ); + mActionPaste->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditPaste.svg" ) ) ); + connect( mActionPaste, &QAction::triggered, this, [ = ] + { + mView->pasteItems( QgsModelGraphicsView::PasteModeCursor ); + } ); + mMenuEdit->insertAction( mActionDeleteComponents, mActionCut ); + mMenuEdit->insertAction( mActionDeleteComponents, mActionCopy ); + mMenuEdit->insertAction( mActionDeleteComponents, mActionPaste ); + mMenuEdit->insertSeparator( mActionDeleteComponents ); + QgsProcessingToolboxProxyModel::Filters filters = QgsProcessingToolboxProxyModel::FilterModeler; if ( settings.value( QStringLiteral( "Processing/Configuration/SHOW_ALGORITHMS_KNOWN_ISSUES" ), false ).toBool() ) { @@ -272,6 +305,18 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags mUndoStack->endMacro(); mIgnoreUndoStackChanges--; } ); + connect( mView, &QgsModelGraphicsView::beginCommand, this, [ = ]( const QString & text ) + { + beginUndoCommand( text ); + } ); + connect( mView, &QgsModelGraphicsView::endCommand, this, [ = ] + { + endUndoCommand(); + } ); + connect( mView, &QgsModelGraphicsView::deleteSelectedItems, this, [ = ] + { + deleteSelected(); + } ); connect( mActionAddGroupBox, &QAction::triggered, this, [ = ] { @@ -372,6 +417,7 @@ void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene ) mScene = scene; mScene->setParent( this ); mScene->setChildAlgorithmResults( mChildResults ); + mScene->setModel( mModel.get() ); mView->setModelScene( mScene ); diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index 711432adc6f..21adcb5a93b 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -170,6 +170,9 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode QMenu *mGroupMenu = nullptr; + QAction *mActionCut = nullptr; + QAction *mActionCopy = nullptr; + QAction *mActionPaste = nullptr; int mBlockUndoCommands = 0; int mIgnoreUndoStackChanges = 0; diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index 0eaa0da3672..f3447f54618 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -29,6 +29,16 @@ QgsModelGraphicsScene::QgsModelGraphicsScene( QObject *parent ) setItemIndexMethod( QGraphicsScene::NoIndex ); } +QgsProcessingModelAlgorithm *QgsModelGraphicsScene::model() +{ + return mModel; +} + +void QgsModelGraphicsScene::setModel( QgsProcessingModelAlgorithm *model ) +{ + mModel = model; +} + void QgsModelGraphicsScene::setFlag( QgsModelGraphicsScene::Flag flag, bool on ) { if ( on ) @@ -154,6 +164,8 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs const QList< LinkSource > sourceItems = linkSourcesForParameterValue( model, QVariant::fromValue( source ), it.value().childId(), context ); for ( const LinkSource &link : sourceItems ) { + if ( !link.item ) + continue; QgsModelArrowItem *arrow = nullptr; if ( link.linkIndex == -1 ) arrow = new QgsModelArrowItem( link.item, mChildAlgorithmItems.value( it.value().childId() ), parameter->isDestination() ? Qt::BottomEdge : Qt::TopEdge, parameter->isDestination() ? bottomIdx : topIdx ); diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index d39d2f07a59..5c806bba9e7 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -70,6 +70,10 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene */ QgsModelGraphicsScene( QObject *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingModelAlgorithm *model(); + + void setModel( QgsProcessingModelAlgorithm *model ); + /** * Sets the combination of \a flags controlling how the scene is rendered and behaves. * \see setFlag() @@ -210,6 +214,8 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene Flags mFlags = nullptr; + QgsProcessingModelAlgorithm *mModel = nullptr; + QMap< QString, QgsModelComponentGraphicItem * > mParameterItems; QMap< QString, QgsModelChildAlgorithmGraphicItem * > mChildAlgorithmItems; QMap< QString, QMap< QString, QgsModelComponentGraphicItem * > > mOutputItems; diff --git a/src/gui/processing/models/qgsmodelgraphicsview.cpp b/src/gui/processing/models/qgsmodelgraphicsview.cpp index cb7481e530a..22da37e08d8 100644 --- a/src/gui/processing/models/qgsmodelgraphicsview.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsview.cpp @@ -22,9 +22,15 @@ #include "qgsmodelviewtooltemporarykeyzoom.h" #include "qgsmodelcomponentgraphicitem.h" #include "qgsmodelgraphicsscene.h" - +#include "qgsprocessingmodelcomponent.h" +#include "qgsprocessingmodelparameter.h" +#include "qgsprocessingmodelchildalgorithm.h" +#include "qgsxmlutils.h" +#include "qgsprocessingmodelalgorithm.h" #include #include +#include +#include ///@cond NOT_STABLE @@ -458,6 +464,270 @@ void QgsModelGraphicsView::snapSelected() endMacroCommand(); } +void QgsModelGraphicsView::copySelectedItems( QgsModelGraphicsView::ClipboardOperation operation ) +{ + copyItems( modelScene()->selectedComponentItems(), operation ); +} + +void QgsModelGraphicsView::copyItems( const QList &items, QgsModelGraphicsView::ClipboardOperation operation ) +{ + if ( !modelScene() ) + return; + + QgsReadWriteContext context; + QDomDocument doc; + QDomElement documentElement = doc.createElement( QStringLiteral( "ModelComponentClipboard" ) ); + if ( operation == ClipboardCut ) + { + emit macroCommandStarted( tr( "Cut Items" ) ); + emit beginCommand( QString() ); + } + + QList< QVariant > paramComponents; + QList< QVariant > groupBoxComponents; + QList< QVariant > algComponents; + for ( QgsModelComponentGraphicItem *item : items ) + { + if ( QgsProcessingModelParameter *param = dynamic_cast< QgsProcessingModelParameter * >( item->component() ) ) + { + QVariantMap paramDef; + paramDef.insert( QStringLiteral( "component" ), param->toVariant() ); + const QgsProcessingParameterDefinition *def = modelScene()->model()->parameterDefinition( param->parameterName() ); + paramDef.insert( QStringLiteral( "definition" ), def->toVariantMap() ); + + paramComponents << paramDef; + } + else if ( QgsProcessingModelGroupBox *groupBox = dynamic_cast< QgsProcessingModelGroupBox * >( item->component() ) ) + { + groupBoxComponents << groupBox->toVariant(); + } + else if ( QgsProcessingModelChildAlgorithm *alg = dynamic_cast< QgsProcessingModelChildAlgorithm * >( item->component() ) ) + { + algComponents << alg->toVariant(); + } + } + QVariantMap components; + components.insert( QStringLiteral( "parameters" ), paramComponents ); + components.insert( QStringLiteral( "groupboxes" ), groupBoxComponents ); + components.insert( QStringLiteral( "algs" ), algComponents ); + doc.appendChild( QgsXmlUtils::writeVariant( components, doc ) ); + if ( operation == ClipboardCut ) + { + emit deleteSelectedItems(); + emit endCommand(); + emit macroCommandEnded(); + } + + QMimeData *mimeData = new QMimeData; + mimeData->setData( QStringLiteral( "text/xml" ), doc.toByteArray() ); + mimeData->setText( doc.toByteArray() ); + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setMimeData( mimeData ); +} + +void QgsModelGraphicsView::pasteItems( QgsModelGraphicsView::PasteMode mode ) +{ + if ( !modelScene() ) + return; + + QList< QgsModelComponentGraphicItem * > pastedItems; + QDomDocument doc; + QClipboard *clipboard = QApplication::clipboard(); + if ( doc.setContent( clipboard->mimeData()->data( QStringLiteral( "text/xml" ) ) ) ) + { + QDomElement docElem = doc.documentElement(); + QVariantMap res = QgsXmlUtils::readVariant( docElem ).toMap(); + + if ( res.contains( QStringLiteral( "parameters" ) ) && res.contains( QStringLiteral( "algs" ) ) ) + { + QPointF pt; + switch ( mode ) + { + case PasteModeCursor: + case PasteModeInPlace: + { + // place items at cursor position + pt = mapToScene( mapFromGlobal( QCursor::pos() ) ); + break; + } + case PasteModeCenter: + { + // place items in center of viewport + pt = mapToScene( viewport()->rect().center() ); + break; + } + } + + emit beginCommand( tr( "Paste Items" ) ); + + QRectF pastedBounds; + + QList< QgsProcessingModelGroupBox > pastedGroups; + for ( const QVariant &v : res.value( QStringLiteral( "groupboxes" ) ).toList() ) + { + QgsProcessingModelGroupBox box; + // don't restore the uuid -- we need them to be unique in the model + box.loadVariant( v.toMap(), true ); + + pastedGroups << box; + + if ( !pastedBounds.isValid( ) ) + pastedBounds = QRectF( box.position() - QPointF( box.size().width() / 2.0, box.size().height() / 2.0 ), box.size() ); + else + pastedBounds = pastedBounds.united( QRectF( box.position() - QPointF( box.size().width() / 2.0, box.size().height() / 2.0 ), box.size() ) ); + } + + QStringList pastedParameters; + for ( const QVariant &v : res.value( QStringLiteral( "parameters" ) ).toList() ) + { + QVariantMap param = v.toMap(); + QVariantMap componentDef = param.value( QStringLiteral( "component" ) ).toMap(); + QVariantMap paramDef = param.value( QStringLiteral( "definition" ) ).toMap(); + + std::unique_ptr< QgsProcessingParameterDefinition > paramDefinition( QgsProcessingParameters::parameterFromVariantMap( paramDef ) ); + + QgsProcessingModelParameter p; + p.loadVariant( componentDef ); + + // we need a unique name for the parameter + QString name = p.parameterName(); + QString description = paramDefinition->description(); + int next = 1; + while ( modelScene()->model()->parameterDefinition( name ) ) + { + next++; + name = QStringLiteral( "%1 (%2)" ).arg( p.parameterName() ).arg( next ); + description = QStringLiteral( "%1 (%2)" ).arg( paramDefinition->description() ).arg( next ); + } + paramDefinition->setName( name ); + paramDefinition->setDescription( description ); + p.setParameterName( name ); + + modelScene()->model()->addModelParameter( paramDefinition.release(), p ); + pastedParameters << p.parameterName(); + + if ( !pastedBounds.isValid( ) ) + pastedBounds = QRectF( p.position() - QPointF( p.size().width() / 2.0, p.size().height() / 2.0 ), p.size() ); + else + pastedBounds = pastedBounds.united( QRectF( p.position() - QPointF( p.size().width() / 2.0, p.size().height() / 2.0 ), p.size() ) ); + + if ( !p.comment()->description().isEmpty() ) + pastedBounds = pastedBounds.united( QRectF( p.comment()->position() - QPointF( p.comment()->size().width() / 2.0, p.comment()->size().height() / 2.0 ), p.comment()->size() ) ); + } + + QStringList pastedAlgorithms; + for ( const QVariant &v : res.value( QStringLiteral( "algs" ) ).toList() ) + { + QgsProcessingModelChildAlgorithm alg; + alg.loadVariant( v.toMap() ); + + // ensure algorithm id is unique + alg.generateChildId( *modelScene()->model() ); + alg.reattach(); + + pastedAlgorithms << alg.childId(); + + if ( !pastedBounds.isValid( ) ) + pastedBounds = QRectF( alg.position() - QPointF( alg.size().width() / 2.0, alg.size().height() / 2.0 ), alg.size() ); + else + pastedBounds = pastedBounds.united( QRectF( alg.position() - QPointF( alg.size().width() / 2.0, alg.size().height() / 2.0 ), alg.size() ) ); + + if ( !alg.comment()->description().isEmpty() ) + pastedBounds = pastedBounds.united( QRectF( alg.comment()->position() - QPointF( alg.comment()->size().width() / 2.0, alg.comment()->size().height() / 2.0 ), alg.comment()->size() ) ); + + const QMap existingAlgs = modelScene()->model()->childAlgorithms(); + + const QMap outputs = alg.modelOutputs(); + QMap pastedOutputs; + for ( auto it = outputs.constBegin(); it != outputs.constEnd(); ++it ) + { + QString name = it.value().name(); + int next = 1; + bool unique = false; + while ( !unique ) + { + unique = true; + for ( auto algIt = existingAlgs.constBegin(); algIt != existingAlgs.constEnd(); ++algIt ) + { + const QMap algOutputs = algIt->modelOutputs(); + for ( auto outputIt = algOutputs.constBegin(); outputIt != algOutputs.constEnd(); ++outputIt ) + { + if ( outputIt.value().name() == name ) + { + unique = false; + break; + } + } + if ( !unique ) + break; + } + if ( unique ) + break; + next++; + name = QStringLiteral( "%1 (%2)" ).arg( it.value().name() ).arg( next ); + } + + QgsProcessingModelOutput newOutput = it.value(); + newOutput.setName( name ); + newOutput.setDescription( name ); + pastedOutputs.insert( name, newOutput ); + + pastedBounds = pastedBounds.united( QRectF( newOutput.position() - QPointF( newOutput.size().width() / 2.0, newOutput.size().height() / 2.0 ), newOutput.size() ) ); + + if ( !alg.comment()->description().isEmpty() ) + pastedBounds = pastedBounds.united( QRectF( newOutput.comment()->position() - QPointF( newOutput.comment()->size().width() / 2.0, newOutput.comment()->size().height() / 2.0 ), newOutput.comment()->size() ) ); + } + alg.setModelOutputs( pastedOutputs ); + + modelScene()->model()->addChildAlgorithm( alg ); + } + + QPointF offset( 0, 0 ); + switch ( mode ) + { + case PasteModeInPlace: + break; + + case PasteModeCursor: + case PasteModeCenter: + { + offset = pt - pastedBounds.topLeft(); + break; + } + } + + if ( !offset.isNull() ) + { + for ( QgsProcessingModelGroupBox pastedGroup : qgis::as_const( pastedGroups ) ) + { + pastedGroup.setPosition( pastedGroup.position() + offset ); + modelScene()->model()->addGroupBox( pastedGroup ); + } + for ( const QString &pastedParam : qgis::as_const( pastedParameters ) ) + { + modelScene()->model()->parameterComponent( pastedParam ).setPosition( modelScene()->model()->parameterComponent( pastedParam ).position() + offset ); + modelScene()->model()->parameterComponent( pastedParam ).comment()->setPosition( modelScene()->model()->parameterComponent( pastedParam ).comment()->position() + offset ); + } + for ( const QString &pastedAlg : qgis::as_const( pastedAlgorithms ) ) + { + modelScene()->model()->childAlgorithm( pastedAlg ).setPosition( modelScene()->model()->childAlgorithm( pastedAlg ).position() + offset ); + modelScene()->model()->childAlgorithm( pastedAlg ).comment()->setPosition( modelScene()->model()->childAlgorithm( pastedAlg ).comment()->position() + offset ); + + const QMap outputs = modelScene()->model()->childAlgorithm( pastedAlg ).modelOutputs(); + for ( auto it = outputs.begin(); it != outputs.end(); ++it ) + { + modelScene()->model()->childAlgorithm( pastedAlg ).modelOutput( it.key() ).setPosition( modelScene()->model()->childAlgorithm( pastedAlg ).modelOutput( it.key() ).position() + offset ); + modelScene()->model()->childAlgorithm( pastedAlg ).modelOutput( it.key() ).comment()->setPosition( modelScene()->model()->childAlgorithm( pastedAlg ).modelOutput( it.key() ).comment()->position() + offset ); + } + } + } + + emit endCommand(); + } + } + + modelScene()->rebuildRequired(); +} QgsModelViewSnapMarker::QgsModelViewSnapMarker() : QGraphicsRectItem( QRectF( 0, 0, 0, 0 ) ) diff --git a/src/gui/processing/models/qgsmodelgraphicsview.h b/src/gui/processing/models/qgsmodelgraphicsview.h index 5d1dbe802fe..b1f003a2e6d 100644 --- a/src/gui/processing/models/qgsmodelgraphicsview.h +++ b/src/gui/processing/models/qgsmodelgraphicsview.h @@ -110,6 +110,44 @@ class GUI_EXPORT QgsModelGraphicsView : public QGraphicsView */ void endMacroCommand(); + + //! Clipboard operations + enum ClipboardOperation + { + ClipboardCut, //!< Cut items + ClipboardCopy, //!< Copy items + }; + + /** + * Cuts or copies the selected items, respecting the specified \a operation. + * \see copyItems() + * \see pasteItems() + */ + void copySelectedItems( ClipboardOperation operation ); + + /** + * Cuts or copies the a list of \a items, respecting the specified \a operation. + * \see copySelectedItems() + * \see pasteItems() + */ + void copyItems( const QList< QgsModelComponentGraphicItem * > &items, ClipboardOperation operation ); + + //! Paste modes + enum PasteMode + { + PasteModeCursor, //!< Paste items at cursor position + PasteModeCenter, //!< Paste items in center of view + PasteModeInPlace, //!< Paste items in place + }; + + /** + * Pastes items from clipboard, using the specified \a mode. + * + * \see copySelectedItems() + * \see hasItemsInClipboard() + */ + void pasteItems( PasteMode mode ); + public slots: /** @@ -157,6 +195,21 @@ class GUI_EXPORT QgsModelGraphicsView : public QGraphicsView */ void macroCommandEnded(); + /** + * Emitted when an undo command is started in the view. + */ + void beginCommand( const QString &text ); + + /** + * Emitted when an undo command in the view has ended. + */ + void endCommand(); + + /** + * Emitted when the selected items should be deleted; + */ + void deleteSelectedItems(); + private: //! Zoom layout from a mouse wheel event