[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
This commit is contained in:
Nyall Dawson 2020-04-07 14:52:46 +10:00
parent 058a2b8b61
commit 47f96e2466
11 changed files with 456 additions and 5 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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
};

View File

@ -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;
}

View File

@ -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.

View File

@ -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 );

View File

@ -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;

View File

@ -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 );

View File

@ -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;

View File

@ -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 <QDragEnterEvent>
#include <QScrollBar>
#include <QApplication>
#include <QClipboard>
///@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<QgsModelComponentGraphicItem *> &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<QString, QgsProcessingModelChildAlgorithm> existingAlgs = modelScene()->model()->childAlgorithms();
const QMap<QString, QgsProcessingModelOutput> outputs = alg.modelOutputs();
QMap<QString, QgsProcessingModelOutput> 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<QString, QgsProcessingModelOutput> 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<QString, QgsProcessingModelOutput> 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 ) )

View File

@ -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