Skip to content

Commit

Permalink
[FEATURE][processing] Add undo/redo support to model designer
Browse files Browse the repository at this point in the history
Makes QGIS more forgiving for users!

Sponsored by NRCan
  • Loading branch information
nyalldawson committed Mar 10, 2020
1 parent 6a597e9 commit e7b5c26
Show file tree
Hide file tree
Showing 17 changed files with 373 additions and 8 deletions.
Expand Up @@ -87,6 +87,8 @@ Sets the ``font`` used to render text in the item.

virtual void mouseDoubleClickEvent( QGraphicsSceneMouseEvent *event );

virtual void mouseReleaseEvent( QGraphicsSceneMouseEvent *event );

virtual void hoverEnterEvent( QGraphicsSceneHoverEvent *event );

virtual void hoverMoveEvent( QGraphicsSceneHoverEvent *event );
Expand Down Expand Up @@ -172,6 +174,15 @@ The default implementation does nothing.
void requestModelRepaint();
%Docstring
Emitted by the item to request a repaint of the parent model scene.
%End

void aboutToChange( const QString &text, int id = 0 );
%Docstring
Emitted when the definition of the associated component is about to be changed
by the item.

The ``text`` argument gives the translated text describing the change about to occur, and the
optional ``id`` can be used to group the associated undo commands.
%End

void changed();
Expand Down
Expand Up @@ -30,10 +30,21 @@ Model designer dialog base class
public:

QgsModelDesignerDialog( QWidget *parent /TransferThis/ = 0, Qt::WindowFlags flags = 0 );
~QgsModelDesignerDialog();

virtual void closeEvent( QCloseEvent *event );


void beginUndoCommand( const QString &text, int id = 0 );
%Docstring
Starts an undo command. This should be called before any changes are made to the model.
%End

void endUndoCommand();
%Docstring
Ends the current undo command. This should be called after changes are made to the model.
%End

protected:

virtual void repaintModel( bool showControls = true ) = 0;
Expand Down
Expand Up @@ -87,6 +87,14 @@ Populates the scene by creating items representing the specified ``model``.
void rebuildRequired();
%Docstring
Emitted when a change in the model requires a full rebuild of the scene.
%End

void componentAboutToChange( const QString &text, int id = 0 );
%Docstring
Emitted whenever a component of the model is about to be changed.

The ``text`` argument gives the translated text describing the change about to occur, and the
optional ``id`` can be used to group the associated undo commands.
%End

void componentChanged();
Expand Down
16 changes: 12 additions & 4 deletions python/plugins/processing/modeler/ModelerDialog.py
Expand Up @@ -91,6 +91,7 @@ def __init__(self, model=None, parent=None):
self.scene = ModelerScene(self)
self.scene.setSceneRect(QRectF(0, 0, self.CANVAS_SIZE, self.CANVAS_SIZE))
self.scene.rebuildRequired.connect(self.repaintModel)
self.scene.componentAboutToChange.connect(self.componentAboutToChange)
self.scene.componentChanged.connect(self.componentChanged)

self.view().setScene(self.scene)
Expand Down Expand Up @@ -125,8 +126,9 @@ def editHelp(self):
dlg = HelpEditionDialog(alg)
dlg.exec_()
if dlg.descriptions:
self.beginUndoCommand(self.tr('Edit Model Help'))
self.model().setHelpContent(dlg.descriptions)
self.setDirty(True)
self.endUndoCommand()

def runModel(self):
if len(self.model().childAlgorithms()) == 0:
Expand Down Expand Up @@ -257,10 +259,14 @@ def repaintModel(self, showControls=True):
self.view().setScene(self.scene)

self.scene.rebuildRequired.connect(self.repaintModel)
self.scene.componentAboutToChange.connect(self.componentAboutToChange)
self.scene.componentChanged.connect(self.componentChanged)

def componentAboutToChange(self, description, id):
self.beginUndoCommand(description, id)

def componentChanged(self):
self.setDirty(True)
self.endUndoCommand()

def create_widget_context(self):
"""
Expand Down Expand Up @@ -326,10 +332,11 @@ def addInput(self, paramType, pos=None):
component.size().width(),
-1.5 * component.size().height()))

self.beginUndoCommand(self.tr('Add Model Input'))
self.model().addModelParameter(new_param, component)
self.repaintModel()
# self.view().ensureVisible(self.scene.getLastParameterItem())
self.setDirty(True)
self.endUndoCommand()

def getPositionForParameterItem(self):
MARGIN = 20
Expand Down Expand Up @@ -365,9 +372,10 @@ def addAlgorithm(self, alg_id, pos=None):
alg.modelOutput(out).setPosition(alg.position() + QPointF(output_offset_x, output_offset_y))
output_offset_y += 1.5 * alg.modelOutput(out).size().height()

self.beginUndoCommand(self.tr('Add Algorithm'))
self.model().addChildAlgorithm(alg)
self.repaintModel()
self.setDirty(True)
self.endUndoCommand()

def getPositionForAlgorithmItem(self):
MARGIN = 20
Expand Down
3 changes: 3 additions & 0 deletions python/plugins/processing/modeler/ModelerGraphicItem.py
Expand Up @@ -90,6 +90,7 @@ def edit(self, edit_comment=False):
comment = dlg.comments()

if new_param is not None:
self.aboutToChange.emit(self.tr('Edit {}').format(new_param.description()))
self.model().removeModelParameter(self.component().parameterName())
self.component().setParameterName(new_param.name())
self.component().setDescription(new_param.name())
Expand Down Expand Up @@ -128,6 +129,7 @@ def edit(self, edit_comment=False):
alg = dlg.createAlgorithm()
alg.setChildId(self.component().childId())
alg.copyNonDefinitionPropertiesFromModel(self.model())
self.aboutToChange.emit(self.tr('Edit {}').format(alg.description()))
self.model().setChildAlgorithm(alg)
self.requestModelRepaint.emit()
self.changed.emit()
Expand Down Expand Up @@ -165,6 +167,7 @@ def edit(self, edit_comment=False):
model_output.setDefaultValue(dlg.param.defaultValue())
model_output.setMandatory(not (dlg.param.flags() & QgsProcessingParameterDefinition.FlagOptional))
model_output.comment().setDescription(dlg.comments())
self.aboutToChange.emit(self.tr('Edit {}').format(model_output.description()))
self.model().updateDestinationParameters()
self.requestModelRepaint.emit()
self.changed.emit()
Expand Down
2 changes: 2 additions & 0 deletions src/gui/CMakeLists.txt
Expand Up @@ -282,6 +282,7 @@ SET(QGIS_GUI_SRCS
processing/models/qgsmodelgraphicitem.cpp
processing/models/qgsmodelgraphicsscene.cpp
processing/models/qgsmodelgraphicsview.cpp
processing/models/qgsmodelundocommand.cpp

providers/gdal/qgsgdalsourceselect.cpp
providers/gdal/qgsgdalguiprovider.cpp
Expand Down Expand Up @@ -942,6 +943,7 @@ SET(QGIS_GUI_HDRS
processing/models/qgsmodelgraphicitem.h
processing/models/qgsmodelgraphicsscene.h
processing/models/qgsmodelgraphicsview.h
processing/models/qgsmodelundocommand.h

providers/gdal/qgsgdalguiprovider.h
providers/gdal/qgsgdalsourceselect.h
Expand Down
30 changes: 30 additions & 0 deletions src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp
Expand Up @@ -105,6 +105,16 @@ void QgsModelComponentGraphicItem::mouseDoubleClickEvent( QGraphicsSceneMouseEve
editComponent();
}

void QgsModelComponentGraphicItem::mouseReleaseEvent( QGraphicsSceneMouseEvent *event )
{
if ( mIsMoving && event->button() == Qt::LeftButton )
{
// released the move drag button, so we've finalised the move operation
emit changed();
mIsMoving = false;
}
}

void QgsModelComponentGraphicItem::hoverEnterEvent( QGraphicsSceneHoverEvent *event )
{
updateToolTip( event->pos() );
Expand Down Expand Up @@ -133,10 +143,23 @@ QVariant QgsModelComponentGraphicItem::itemChange( QGraphicsItem::GraphicsItemCh
case QGraphicsItem::ItemPositionHasChanged:
{
emit updateArrowPaths();

if ( !mInitialized || pos() == mComponent->position() )
break;

mComponent->setPosition( pos() );

// also need to update the model's stored component's position
if ( !mIsMoving )
{
// we are just starting a move operation - so create the undo command using the existing state
emit aboutToChange( tr( "Move %1" ).arg( mComponent->description() ) );
}
updateStoredComponentPosition( pos() );

// we don't emit the changed signal to trigger the end of the undo command until the whole move operation finishes
// (i.e. we do that in mouseReleaseEvent)
mIsMoving = true;
break;
}
case QGraphicsItem::ItemSelectedChange:
Expand Down Expand Up @@ -164,6 +187,7 @@ QVariant QgsModelComponentGraphicItem::itemChange( QGraphicsItem::GraphicsItemCh
mExpandBottomButton = new QgsModelDesignerFoldButtonGraphicItem( this, mComponent->linksCollapsed( Qt::BottomEdge ), pt );
connect( mExpandBottomButton, &QgsModelDesignerFoldButtonGraphicItem::folded, this, [ = ]( bool folded ) { fold( Qt::BottomEdge, folded ); } );
}
mInitialized = true;
}
break;
}
Expand Down Expand Up @@ -340,6 +364,7 @@ void QgsModelComponentGraphicItem::updateToolTip( const QPointF &pos )

void QgsModelComponentGraphicItem::fold( Qt::Edge edge, bool folded )
{
emit aboutToChange( !folded ? tr( "Expand Item" ) : tr( "Collapse Item" ) );
mComponent->setLinksCollapsed( edge, folded );
// also need to update the model's stored component

Expand All @@ -353,6 +378,7 @@ void QgsModelComponentGraphicItem::fold( Qt::Edge edge, bool folded )

prepareGeometryChange();
emit updateArrowPaths();
emit changed();
update();
}

Expand Down Expand Up @@ -602,6 +628,7 @@ void QgsModelParameterGraphicItem::deleteComponent()
}
else
{
emit aboutToChange( tr( "Delete Input %1" ).arg( param->description() ) );
model()->removeModelParameter( param->parameterName() );
emit changed();
emit requestModelRepaint();
Expand Down Expand Up @@ -772,6 +799,7 @@ void QgsModelChildAlgorithmGraphicItem::deleteComponent()
{
if ( const QgsProcessingModelChildAlgorithm *child = dynamic_cast< const QgsProcessingModelChildAlgorithm * >( component() ) )
{
emit aboutToChange( tr( "Remove %1" ).arg( child->algorithm() ? child->algorithm()->displayName() : tr( "Algorithm" ) ) );
if ( !model()->removeChildAlgorithm( child->childId() ) )
{
QMessageBox::warning( nullptr, QObject::tr( "Could not remove algorithm" ),
Expand Down Expand Up @@ -875,6 +903,7 @@ void QgsModelOutputGraphicItem::deleteComponent()
{
if ( const QgsProcessingModelOutput *output = dynamic_cast< const QgsProcessingModelOutput * >( component() ) )
{
emit aboutToChange( tr( "Delete Output %1" ).arg( output->description() ) );
model()->childAlgorithm( output->childId() ).removeModelOutput( output->name() );
model()->updateDestinationParameters();
emit changed();
Expand Down Expand Up @@ -967,6 +996,7 @@ void QgsModelCommentGraphicItem::deleteComponent()
{
if ( QgsProcessingModelComment *comment = modelComponent() )
{
emit aboutToChange( tr( "Delete Comment" ) );
comment->setDescription( QString() );
emit changed();
emit requestModelRepaint();
Expand Down
11 changes: 11 additions & 0 deletions src/gui/processing/models/qgsmodelcomponentgraphicitem.h
Expand Up @@ -108,6 +108,7 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject
void setFont( const QFont &font );

void mouseDoubleClickEvent( QGraphicsSceneMouseEvent *event ) override;
void mouseReleaseEvent( QGraphicsSceneMouseEvent *event ) override;
void hoverEnterEvent( QGraphicsSceneHoverEvent *event ) override;
void hoverMoveEvent( QGraphicsSceneHoverEvent *event ) override;
void hoverLeaveEvent( QGraphicsSceneHoverEvent *event ) override;
Expand Down Expand Up @@ -188,6 +189,15 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject
*/
void requestModelRepaint();

/**
* Emitted when the definition of the associated component is about to be changed
* by the item.
*
* The \a text argument gives the translated text describing the change about to occur, and the
* optional \a id can be used to group the associated undo commands.
*/
void aboutToChange( const QString &text, int id = 0 );

/**
* Emitted when the definition of the associated component is changed
* by the item.
Expand Down Expand Up @@ -288,6 +298,7 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject
QFont mFont;

bool mIsHovering = false;
bool mIsMoving = false;

};
Q_DECLARE_OPERATORS_FOR_FLAGS( QgsModelComponentGraphicItem::Flags )
Expand Down

0 comments on commit e7b5c26

Please sign in to comment.