From 13fc85d740a4a5bd60ccac2dd20515e745430b72 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 4 Mar 2020 15:29:49 +1000 Subject: [PATCH] [FEATURE][processsing] Add support for comments attached to components This allows users to create comments attached to model components (inputs, algorithms or outputs). Comments are shown linked to the associated component, and can be freely moved around the model. --- .../qgsprocessingmodelchildalgorithm.sip.in | 3 + .../models/qgsprocessingmodelcomment.sip.in | 56 +++++++ .../models/qgsprocessingmodelcomponent.sip.in | 16 ++ .../models/qgsprocessingmodeloutput.sip.in | 3 + .../models/qgsprocessingmodelparameter.sip.in | 3 + python/core/core_auto.sip | 1 + .../qgsmodelcomponentgraphicitem.sip.in | 79 ++++++++++ .../models/qgsmodelgraphicsscene.sip.in | 7 + .../processing/modeler/ModelerGraphicItem.py | 60 ++++++- .../ModelerParameterDefinitionDialog.py | 133 +++++++++------- .../modeler/ModelerParametersDialog.py | 42 ++++- .../processing/modeler/ModelerScene.py | 6 +- src/core/CMakeLists.txt | 2 + .../qgsprocessingmodelchildalgorithm.cpp | 4 + .../models/qgsprocessingmodelchildalgorithm.h | 7 + .../models/qgsprocessingmodelcomment.cpp | 47 ++++++ .../models/qgsprocessingmodelcomment.h | 59 +++++++ .../models/qgsprocessingmodelcomponent.cpp | 5 + .../models/qgsprocessingmodelcomponent.h | 20 +++ .../models/qgsprocessingmodeloutput.cpp | 2 + .../models/qgsprocessingmodeloutput.h | 8 + .../models/qgsprocessingmodelparameter.cpp | 2 + .../models/qgsprocessingmodelparameter.h | 7 + src/core/qgsxmlutils.cpp | 23 ++- .../models/qgsmodelcomponentgraphicitem.cpp | 147 ++++++++++++++++-- .../models/qgsmodelcomponentgraphicitem.h | 73 +++++++++ .../models/qgsmodelgraphicsscene.cpp | 34 +++- .../processing/models/qgsmodelgraphicsscene.h | 11 ++ 28 files changed, 779 insertions(+), 81 deletions(-) create mode 100644 python/core/auto_generated/processing/models/qgsprocessingmodelcomment.sip.in create mode 100644 src/core/processing/models/qgsprocessingmodelcomment.cpp create mode 100644 src/core/processing/models/qgsprocessingmodelcomment.h diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelchildalgorithm.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelchildalgorithm.sip.in index 645b9fe7dda..574dbed4d13 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelchildalgorithm.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelchildalgorithm.sip.in @@ -275,6 +275,9 @@ The ``currentIndent`` and ``indentSize`` are used to set the base line indent an The ``friendlyChildNames`` argument gives a map of child id to a friendly algorithm name, to be used in the code to identify that algorithm instead of the raw child id. %End + virtual QgsProcessingModelComment *comment(); + virtual void setComment( const QgsProcessingModelComment &comment ); + }; diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelcomment.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelcomment.sip.in new file mode 100644 index 00000000000..b1c00441f5b --- /dev/null +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelcomment.sip.in @@ -0,0 +1,56 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/processing/models/qgsprocessingmodelcomment.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsProcessingModelComment : QgsProcessingModelComponent +{ +%Docstring +Represents a comment in a model. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsprocessingmodelcomment.h" +%End + public: + + QgsProcessingModelComment( const QString &description = QString() ); +%Docstring +Constructor for QgsProcessingModelComment with the specified ``description``. +%End + + virtual QgsProcessingModelComment *clone() const /Factory/; + + + QVariant toVariant() const; +%Docstring +Saves this comment to a QVariant. + +.. seealso:: :py:func:`loadVariant` +%End + + bool loadVariant( const QVariantMap &map ); +%Docstring +Loads this comment from a QVariantMap. + +.. seealso:: :py:func:`toVariant` +%End +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/processing/models/qgsprocessingmodelcomment.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelcomponent.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelcomponent.sip.in index e1c3385662d..faa9e21577c 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelcomponent.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelcomponent.sip.in @@ -10,6 +10,7 @@ + class QgsProcessingModelComponent { %Docstring @@ -86,6 +87,21 @@ in the graphical modeler. .. seealso:: :py:func:`linksCollapsed` %End + + virtual QgsProcessingModelComment *comment(); +%Docstring +Returns the comment attached to this component (may be ``None``) + +.. seealso:: :py:func:`setComment` +%End + + virtual void setComment( const QgsProcessingModelComment &comment ); +%Docstring +Sets the ``comment`` attached to this component. + +.. seealso:: :py:func:`comment` +%End + virtual QgsProcessingModelComponent *clone() const = 0 /Factory/; %Docstring Clones the component. diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodeloutput.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodeloutput.sip.in index 97c153991f9..dfa373e2591 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodeloutput.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodeloutput.sip.in @@ -127,6 +127,9 @@ Loads this output from a QVariantMap. .. seealso:: :py:func:`toVariant` %End + virtual QgsProcessingModelComment *comment(); + virtual void setComment( const QgsProcessingModelComment &comment ); + }; diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelparameter.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelparameter.sip.in index 2c42438cb1c..cb30079323f 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelparameter.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelparameter.sip.in @@ -63,6 +63,9 @@ Loads this parameter from a QVariantMap. .. seealso:: :py:func:`toVariant` %End + virtual QgsProcessingModelComment *comment(); + virtual void setComment( const QgsProcessingModelComment &comment ); + }; diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index d9dd8e4b463..224ee3552a2 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -415,6 +415,7 @@ %Include auto_generated/processing/models/qgsprocessingmodelalgorithm.sip %Include auto_generated/processing/models/qgsprocessingmodelchildalgorithm.sip %Include auto_generated/processing/models/qgsprocessingmodelchildparametersource.sip +%Include auto_generated/processing/models/qgsprocessingmodelcomment.sip %Include auto_generated/processing/models/qgsprocessingmodelcomponent.sip %Include auto_generated/processing/models/qgsprocessingmodeloutput.sip %Include auto_generated/processing/models/qgsprocessingmodelparameter.sip diff --git a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index 2c2722f52c4..59ccab55a38 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -147,6 +147,13 @@ Returns the best link point to use for a link originating at a specified ``other - edge: item edge for calculated best link point %End + virtual void editComment(); +%Docstring +Called when the comment attached to the item should be edited. + +The default implementation does nothing. +%End + signals: @@ -208,6 +215,11 @@ Returns the stroke color for the item for the specified ``state``. virtual QColor textColor( State state ) const = 0; %Docstring Returns the label text color for the item for the specified ``state``. +%End + + virtual Qt::PenStyle strokeStyle( State state ) const; +%Docstring +Returns the stroke style to use while rendering the outline of the item. %End virtual QPicture iconPicture() const; @@ -218,6 +230,11 @@ Returns a QPicture version of the item's icon, if available. virtual QPixmap iconPixmap() const; %Docstring Returns a QPixmap version of the item's icon, if available. +%End + + virtual void updateStoredComponentPosition( const QPointF &pos ) = 0; +%Docstring +Updates the position stored in the model for the associated comment %End }; @@ -264,6 +281,8 @@ Ownership of ``parameter`` is transferred to the item. virtual QPicture iconPicture() const; + virtual void updateStoredComponentPosition( const QPointF &pos ); + protected slots: @@ -320,6 +339,8 @@ Ownership of ``child`` is transferred to the item. virtual QString linkPointText( Qt::Edge edge, int index ) const; + virtual void updateStoredComponentPosition( const QPointF &pos ); + protected slots: @@ -368,6 +389,8 @@ Ownership of ``output`` is transferred to the item. virtual QPicture iconPicture() const; + virtual void updateStoredComponentPosition( const QPointF &pos ); + protected slots: @@ -376,6 +399,62 @@ Ownership of ``output`` is transferred to the item. }; + + +class QgsModelCommentGraphicItem : QgsModelComponentGraphicItem +{ +%Docstring +A graphic item representing a model comment in the model designer. + +.. warning:: + + Not stable API + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsmodelcomponentgraphicitem.h" +%End + public: + + QgsModelCommentGraphicItem( QgsProcessingModelComment *comment /Transfer/, + QgsModelComponentGraphicItem *parentItem, + QgsProcessingModelAlgorithm *model, + QGraphicsItem *parent /TransferThis/ ); +%Docstring +Constructor for QgsModelCommentGraphicItem for the specified ``comment``, with the specified ``parent`` item. + +The ``model`` argument specifies the associated processing model. Ownership of ``model`` is not transferred, and +it must exist for the lifetime of this object. + +Ownership of ``output`` is transferred to the item. +%End + ~QgsModelCommentGraphicItem(); + virtual void contextMenuEvent( QGraphicsSceneContextMenuEvent *event ); + + + protected: + + virtual QColor fillColor( State state ) const; + + virtual QColor strokeColor( State state ) const; + + virtual QColor textColor( State state ) const; + + virtual Qt::PenStyle strokeStyle( State state ) const; + + virtual void updateStoredComponentPosition( const QPointF &pos ); + + + protected slots: + + virtual void deleteComponent(); + + virtual void editComponent(); + +}; + /************************************************************************ * This file has been generated automatically from * * * diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 1a1dbe602cc..5c38ba7757e 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -110,6 +110,13 @@ Creates a new graphic item for a model child algorithm. Creates a new graphic item for a model output. %End + virtual QgsModelComponentGraphicItem *createCommentGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelComment *comment, + QgsModelComponentGraphicItem *parentItem ) const /Factory/; +%Docstring +Creates a new graphic item for a model comment. +%End + + }; diff --git a/python/plugins/processing/modeler/ModelerGraphicItem.py b/python/plugins/processing/modeler/ModelerGraphicItem.py index 377985f0a82..53d6a8bd916 100644 --- a/python/plugins/processing/modeler/ModelerGraphicItem.py +++ b/python/plugins/processing/modeler/ModelerGraphicItem.py @@ -29,7 +29,8 @@ from qgis.gui import ( QgsProcessingParameterWidgetContext, QgsModelParameterGraphicItem, QgsModelChildAlgorithmGraphicItem, - QgsModelOutputGraphicItem + QgsModelOutputGraphicItem, + QgsModelCommentGraphicItem ) from processing.modeler.ModelerParameterDefinitionDialog import ModelerParameterDefinitionDialog from processing.modeler.ModelerParametersDialog import ModelerParametersDialog @@ -59,15 +60,20 @@ class ModelerInputGraphicItem(QgsModelParameterGraphicItem): widget_context.setModel(self.model()) return widget_context - def editComponent(self): + def edit(self, edit_comment=False): existing_param = self.model().parameterDefinition(self.component().parameterName()) + comment = self.component().comment().description() new_param = None if ModelerParameterDefinitionDialog.use_legacy_dialog(param=existing_param): # boo, old api dlg = ModelerParameterDefinitionDialog(self.model(), param=existing_param) + dlg.setComments(comment) + if edit_comment: + dlg.switchToCommentTab() if dlg.exec_(): new_param = dlg.param + comment = dlg.comments() else: # yay, use new API! context = createContext() @@ -84,11 +90,18 @@ class ModelerInputGraphicItem(QgsModelParameterGraphicItem): self.model().removeModelParameter(self.component().parameterName()) self.component().setParameterName(new_param.name()) self.component().setDescription(new_param.name()) + self.component().comment().setDescription(comment) self.model().addModelParameter(new_param, self.component()) self.setLabel(new_param.description()) self.requestModelRepaint.emit() self.changed.emit() + def editComponent(self): + self.edit() + + def editComment(self): + self.edit(edit_comment=True) + class ModelerChildAlgorithmGraphicItem(QgsModelChildAlgorithmGraphicItem): """ @@ -101,10 +114,13 @@ class ModelerChildAlgorithmGraphicItem(QgsModelChildAlgorithmGraphicItem): def __init__(self, element, model): super().__init__(element, model, None) - def editComponent(self): + def edit(self, edit_comment=False): elemAlg = self.component().algorithm() dlg = ModelerParametersDialog(elemAlg, self.model(), self.component().childId(), self.component().configuration()) + dlg.setComments(self.component().comment().description()) + if edit_comment: + dlg.switchToCommentTab() if dlg.exec_(): alg = dlg.createAlgorithm() alg.setChildId(self.component().childId()) @@ -112,11 +128,18 @@ class ModelerChildAlgorithmGraphicItem(QgsModelChildAlgorithmGraphicItem): self.requestModelRepaint.emit() self.changed.emit() + def editComponent(self): + self.edit() + + def editComment(self): + self.edit(edit_comment=True) + def updateAlgorithm(self, alg): existing_child = self.model().childAlgorithm(alg.childId()) alg.setPosition(existing_child.position()) alg.setLinksCollapsed(Qt.TopEdge, existing_child.linksCollapsed(Qt.TopEdge)) alg.setLinksCollapsed(Qt.BottomEdge, existing_child.linksCollapsed(Qt.BottomEdge)) + alg.comment().setPosition(existing_child.comment().position()) for i, out in enumerate(alg.modelOutputs().keys()): alg.modelOutput(out).setPosition(alg.modelOutput(out).position() or alg.position() + QPointF( @@ -136,15 +159,42 @@ class ModelerOutputGraphicItem(QgsModelOutputGraphicItem): def __init__(self, element, model): super().__init__(element, model, None) - def editComponent(self): + def edit(self, edit_comment=False): child_alg = self.model().childAlgorithm(self.component().childId()) param_name = '{}:{}'.format(self.component().childId(), self.component().name()) dlg = ModelerParameterDefinitionDialog(self.model(), param=self.model().parameterDefinition(param_name)) - if dlg.exec_() and dlg.param is not None: + dlg.setComments(self.component().comment().description()) + if edit_comment: + dlg.switchToCommentTab() + + if dlg.exec_(): model_output = child_alg.modelOutput(self.component().name()) model_output.setDescription(dlg.param.description()) model_output.setDefaultValue(dlg.param.defaultValue()) model_output.setMandatory(not (dlg.param.flags() & QgsProcessingParameterDefinition.FlagOptional)) + model_output.comment().setDescription(dlg.comments()) self.model().updateDestinationParameters() + self.requestModelRepaint.emit() self.changed.emit() + + def editComponent(self): + self.edit() + + def editComment(self): + self.edit(edit_comment=True) + + +class ModelerCommentGraphicItem(QgsModelCommentGraphicItem): + """ + IMPORTANT! This is intentionally a MINIMAL class, only containing code which HAS TO BE HERE + because it contains Python code for compatibility with deprecated methods ONLY. + + Don't add anything here -- edit the c++ base class instead! + """ + + def __init__(self, model, element, parent_component): + super().__init__(element, parent_component, model, None) + + def editComponent(self): + pass diff --git a/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py b/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py index c95a794ba5b..ce102384026 100755 --- a/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py +++ b/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py @@ -33,7 +33,10 @@ from qgis.PyQt.QtWidgets import (QDialog, QComboBox, QCheckBox, QDialogButtonBox, - QMessageBox) + QMessageBox, + QTabWidget, + QWidget, + QTextEdit) from qgis.gui import QgsExpressionLineEdit, QgsProjectionSelectionWidget from qgis.core import (QgsApplication, @@ -124,13 +127,20 @@ class ModelerParameterDefinitionDialog(QDialog): settings.setValue("/Processing/modelParametersDefinitionDialogGeometry", self.saveGeometry()) super(ModelerParameterDefinitionDialog, self).closeEvent(event) + def switchToCommentTab(self): + self.tab.setCurrentIndex(1) + def setupUi(self): type_metadata = QgsApplication.processingRegistry().parameterType(self.param.type() if self.param else self.paramType) self.setWindowTitle(self.tr('{} Parameter Definition').format(type_metadata.name())) + + self.mainLayout = QVBoxLayout() + self.tab = QTabWidget() + self.mainLayout.addWidget(self.tab) + self.setMinimumWidth(300) - self.verticalLayout = QVBoxLayout(self) - self.verticalLayout.setMargin(20) + self.verticalLayout = QVBoxLayout() self.label = QLabel(self.tr('Parameter name')) self.verticalLayout.addWidget(self.label) @@ -214,8 +224,8 @@ class ModelerParameterDefinitionDialog(QDialog): if self.param is not None: self.shapetypeCombo.setCurrentIndex(self.shapetypeCombo.findData(self.param.dataTypes()[0])) self.verticalLayout.addWidget(self.shapetypeCombo) - elif (self.paramType == parameters.PARAMETER_MULTIPLE or - isinstance(self.param, QgsProcessingParameterMultipleLayers)): + elif (self.paramType == parameters.PARAMETER_MULTIPLE + or isinstance(self.param, QgsProcessingParameterMultipleLayers)): self.verticalLayout.addWidget(QLabel(self.tr('Data type'))) self.datatypeCombo = QComboBox() self.datatypeCombo.addItem(self.tr('Any Map Layer'), QgsProcessing.TypeMapLayer) @@ -229,11 +239,11 @@ class ModelerParameterDefinitionDialog(QDialog): if self.param is not None: self.datatypeCombo.setCurrentIndex(self.datatypeCombo.findData(self.param.layerType())) self.verticalLayout.addWidget(self.datatypeCombo) - elif (self.paramType in (parameters.PARAMETER_NUMBER, parameters.PARAMETER_DISTANCE, parameters.PARAMETER_SCALE) or - isinstance(self.param, (QgsProcessingParameterNumber, QgsProcessingParameterDistance, QgsProcessingParameterScale))): + elif (self.paramType in (parameters.PARAMETER_NUMBER, parameters.PARAMETER_DISTANCE, parameters.PARAMETER_SCALE) + or isinstance(self.param, (QgsProcessingParameterNumber, QgsProcessingParameterDistance, QgsProcessingParameterScale))): - if (self.paramType == parameters.PARAMETER_DISTANCE or - isinstance(self.param, QgsProcessingParameterDistance)): + if (self.paramType == parameters.PARAMETER_DISTANCE + or isinstance(self.param, QgsProcessingParameterDistance)): self.verticalLayout.addWidget(QLabel(self.tr('Linked input'))) self.parentCombo = QComboBox() self.parentCombo.addItem('', '') @@ -281,8 +291,8 @@ class ModelerParameterDefinitionDialog(QDialog): if default: self.defaultTextBox.setText(str(default)) self.verticalLayout.addWidget(self.defaultTextBox) - elif (self.paramType == parameters.PARAMETER_EXPRESSION or - isinstance(self.param, QgsProcessingParameterExpression)): + elif (self.paramType == parameters.PARAMETER_EXPRESSION + or isinstance(self.param, QgsProcessingParameterExpression)): self.verticalLayout.addWidget(QLabel(self.tr('Default value'))) self.defaultEdit = QgsExpressionLineEdit() if self.param is not None: @@ -302,15 +312,15 @@ class ModelerParameterDefinitionDialog(QDialog): self.parentCombo.setCurrentIndex(idx) idx += 1 self.verticalLayout.addWidget(self.parentCombo) - elif (self.paramType == parameters.PARAMETER_POINT or - isinstance(self.param, QgsProcessingParameterPoint)): + elif (self.paramType == parameters.PARAMETER_POINT + or isinstance(self.param, QgsProcessingParameterPoint)): self.verticalLayout.addWidget(QLabel(self.tr('Default value'))) self.defaultTextBox = QLineEdit() if self.param is not None: self.defaultTextBox.setText(self.param.defaultValue()) self.verticalLayout.addWidget(self.defaultTextBox) - elif (self.paramType == parameters.PARAMETER_CRS or - isinstance(self.param, QgsProcessingParameterCrs)): + elif (self.paramType == parameters.PARAMETER_CRS + or isinstance(self.param, QgsProcessingParameterCrs)): self.verticalLayout.addWidget(QLabel(self.tr('Default value'))) self.selector = QgsProjectionSelectionWidget() if self.param is not None: @@ -367,18 +377,37 @@ class ModelerParameterDefinitionDialog(QDialog): self.advancedCheck.setEnabled(False) self.advancedCheck.setChecked(False) + self.verticalLayout.addStretch() + + w = QWidget() + w.setLayout(self.verticalLayout) + self.tab.addTab(w, self.tr('Properties')) + + self.commentLayout = QVBoxLayout() + self.commentEdit = QTextEdit() + self.commentEdit.setAcceptRichText(False) + self.commentLayout.addWidget(self.commentEdit) + w2 = QWidget() + w2.setLayout(self.commentLayout) + self.tab.addTab(w2, self.tr('Comments')) + self.buttonBox = QDialogButtonBox(self) self.buttonBox.setOrientation(Qt.Horizontal) - self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | - QDialogButtonBox.Ok) + self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel + | QDialogButtonBox.Ok) self.buttonBox.setObjectName('buttonBox') self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) - self.verticalLayout.addStretch() - self.verticalLayout.addWidget(self.buttonBox) + self.mainLayout.addWidget(self.buttonBox) - self.setLayout(self.verticalLayout) + self.setLayout(self.mainLayout) + + def setComments(self, text): + self.commentEdit.setPlainText(text) + + def comments(self): + return self.commentEdit.toPlainText() def accept(self): description = self.nameTextBox.text() @@ -397,8 +426,8 @@ class ModelerParameterDefinitionDialog(QDialog): i += 1 else: name = self.param.name() - if (self.paramType == parameters.PARAMETER_TABLE_FIELD or - isinstance(self.param, QgsProcessingParameterField)): + if (self.paramType == parameters.PARAMETER_TABLE_FIELD + or isinstance(self.param, QgsProcessingParameterField)): if self.parentCombo.currentIndex() < 0: QMessageBox.warning(self, self.tr('Unable to define parameter'), self.tr('Wrong or missing parameter values')) @@ -411,39 +440,39 @@ class ModelerParameterDefinitionDialog(QDialog): self.param = QgsProcessingParameterField(name, description, defaultValue=default, parentLayerParameterName=parent, type=datatype, allowMultiple=self.multipleCheck.isChecked()) - elif (self.paramType == parameters.PARAMETER_BAND or - isinstance(self.param, QgsProcessingParameterBand)): + elif (self.paramType == parameters.PARAMETER_BAND + or isinstance(self.param, QgsProcessingParameterBand)): if self.parentCombo.currentIndex() < 0: QMessageBox.warning(self, self.tr('Unable to define parameter'), self.tr('Wrong or missing parameter values')) return parent = self.parentCombo.currentData() self.param = QgsProcessingParameterBand(name, description, None, parent) - elif (self.paramType == parameters.PARAMETER_MAP_LAYER or - isinstance(self.param, QgsProcessingParameterMapLayer)): + elif (self.paramType == parameters.PARAMETER_MAP_LAYER + or isinstance(self.param, QgsProcessingParameterMapLayer)): self.param = QgsProcessingParameterMapLayer( name, description) - elif (self.paramType == parameters.PARAMETER_RASTER or - isinstance(self.param, QgsProcessingParameterRasterLayer)): + elif (self.paramType == parameters.PARAMETER_RASTER + or isinstance(self.param, QgsProcessingParameterRasterLayer)): self.param = QgsProcessingParameterRasterLayer( name, description) - elif (self.paramType == parameters.PARAMETER_TABLE or - isinstance(self.param, QgsProcessingParameterVectorLayer)): + elif (self.paramType == parameters.PARAMETER_TABLE + or isinstance(self.param, QgsProcessingParameterVectorLayer)): self.param = QgsProcessingParameterVectorLayer( name, description, [self.shapetypeCombo.currentData()]) - elif (self.paramType == parameters.PARAMETER_VECTOR or - isinstance(self.param, QgsProcessingParameterFeatureSource)): + elif (self.paramType == parameters.PARAMETER_VECTOR + or isinstance(self.param, QgsProcessingParameterFeatureSource)): self.param = QgsProcessingParameterFeatureSource( name, description, [self.shapetypeCombo.currentData()]) - elif (self.paramType == parameters.PARAMETER_MULTIPLE or - isinstance(self.param, QgsProcessingParameterMultipleLayers)): + elif (self.paramType == parameters.PARAMETER_MULTIPLE + or isinstance(self.param, QgsProcessingParameterMultipleLayers)): self.param = QgsProcessingParameterMultipleLayers( name, description, self.datatypeCombo.currentData()) - elif (self.paramType == parameters.PARAMETER_DISTANCE or - isinstance(self.param, QgsProcessingParameterDistance)): + elif (self.paramType == parameters.PARAMETER_DISTANCE + or isinstance(self.param, QgsProcessingParameterDistance)): self.param = QgsProcessingParameterDistance(name, description, self.defaultTextBox.text()) try: @@ -465,12 +494,12 @@ class ModelerParameterDefinitionDialog(QDialog): parent = self.parentCombo.currentData() if parent: self.param.setParentParameterName(parent) - elif (self.paramType == parameters.PARAMETER_SCALE or - isinstance(self.param, QgsProcessingParameterScale)): + elif (self.paramType == parameters.PARAMETER_SCALE + or isinstance(self.param, QgsProcessingParameterScale)): self.param = QgsProcessingParameterScale(name, description, self.defaultTextBox.text()) - elif (self.paramType == parameters.PARAMETER_NUMBER or - isinstance(self.param, QgsProcessingParameterNumber)): + elif (self.paramType == parameters.PARAMETER_NUMBER + or isinstance(self.param, QgsProcessingParameterNumber)): type = self.type_combo.currentData() self.param = QgsProcessingParameterNumber(name, description, type, @@ -486,27 +515,27 @@ class ModelerParameterDefinitionDialog(QDialog): QMessageBox.warning(self, self.tr('Unable to define parameter'), self.tr('Wrong or missing parameter values')) return - elif (self.paramType == parameters.PARAMETER_EXPRESSION or - isinstance(self.param, QgsProcessingParameterExpression)): + elif (self.paramType == parameters.PARAMETER_EXPRESSION + or isinstance(self.param, QgsProcessingParameterExpression)): parent = self.parentCombo.currentData() self.param = QgsProcessingParameterExpression(name, description, str(self.defaultEdit.expression()), parent) - elif (self.paramType == parameters.PARAMETER_EXTENT or - isinstance(self.param, QgsProcessingParameterExtent)): + elif (self.paramType == parameters.PARAMETER_EXTENT + or isinstance(self.param, QgsProcessingParameterExtent)): self.param = QgsProcessingParameterExtent(name, description) - elif (self.paramType == parameters.PARAMETER_POINT or - isinstance(self.param, QgsProcessingParameterPoint)): + elif (self.paramType == parameters.PARAMETER_POINT + or isinstance(self.param, QgsProcessingParameterPoint)): self.param = QgsProcessingParameterPoint(name, description, str(self.defaultTextBox.text())) - elif (self.paramType == parameters.PARAMETER_CRS or - isinstance(self.param, QgsProcessingParameterCrs)): + elif (self.paramType == parameters.PARAMETER_CRS + or isinstance(self.param, QgsProcessingParameterCrs)): self.param = QgsProcessingParameterCrs(name, description, self.selector.crs().authid()) - elif (self.paramType == parameters.PARAMETER_ENUM or - isinstance(self.param, QgsProcessingParameterEnum)): + elif (self.paramType == parameters.PARAMETER_ENUM + or isinstance(self.param, QgsProcessingParameterEnum)): self.param = QgsProcessingParameterEnum(name, description, self.widget.options(), self.widget.allowMultiple(), self.widget.defaultOptions()) - elif (self.paramType == parameters.PARAMETER_MATRIX or - isinstance(self.param, QgsProcessingParameterMatrix)): + elif (self.paramType == parameters.PARAMETER_MATRIX + or isinstance(self.param, QgsProcessingParameterMatrix)): self.param = QgsProcessingParameterMatrix(name, description, hasFixedNumberRows=self.widget.fixedRows(), headers=self.widget.headers(), defaultValue=self.widget.value()) # Destination parameter diff --git a/python/plugins/processing/modeler/ModelerParametersDialog.py b/python/plugins/processing/modeler/ModelerParametersDialog.py index bcea2f5157f..81aaed815da 100644 --- a/python/plugins/processing/modeler/ModelerParametersDialog.py +++ b/python/plugins/processing/modeler/ModelerParametersDialog.py @@ -29,7 +29,7 @@ from qgis.PyQt.QtCore import (Qt, QByteArray) from qgis.PyQt.QtWidgets import (QDialog, QDialogButtonBox, QLabel, QLineEdit, QFrame, QPushButton, QSizePolicy, QVBoxLayout, - QHBoxLayout, QWidget) + QHBoxLayout, QWidget, QTabWidget, QTextEdit) from qgis.core import (Qgis, QgsProject, @@ -102,6 +102,9 @@ class ModelerParametersDialog(QDialog): settings.setValue("/Processing/modelParametersDialogGeometry", self.saveGeometry()) super(ModelerParametersDialog, self).closeEvent(event) + def switchToCommentTab(self): + self.tab.setCurrentIndex(1) + def setupUi(self): self.checkBoxes = {} self.showAdvanced = False @@ -111,6 +114,11 @@ class ModelerParametersDialog(QDialog): self.algorithmItem = None self.resize(650, 450) + + self.mainLayout = QVBoxLayout() + self.tab = QTabWidget() + self.mainLayout.addWidget(self.tab) + self.buttonBox = QDialogButtonBox() self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.Help) @@ -118,7 +126,6 @@ class ModelerParametersDialog(QDialog): QSizePolicy.Expanding) self.verticalLayout = QVBoxLayout() self.verticalLayout.setSpacing(5) - self.verticalLayout.setMargin(20) self.bar = QgsMessageBar() self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) @@ -229,13 +236,33 @@ class ModelerParametersDialog(QDialog): self.scrollArea.setWidgetResizable(True) self.verticalLayout2.addWidget(self.scrollArea) - self.verticalLayout2.addWidget(self.buttonBox) - self.setLayout(self.verticalLayout2) self.buttonBox.accepted.connect(self.okPressed) self.buttonBox.rejected.connect(self.cancelPressed) self.buttonBox.helpRequested.connect(self.openHelp) + + w = QWidget() + w.setLayout(self.verticalLayout2) + self.tab.addTab(w, self.tr('Properties')) + + self.commentLayout = QVBoxLayout() + self.commentEdit = QTextEdit() + self.commentEdit.setAcceptRichText(False) + self.commentLayout.addWidget(self.commentEdit) + w2 = QWidget() + w2.setLayout(self.commentLayout) + self.tab.addTab(w2, self.tr('Comments')) + + self.mainLayout.addWidget(self.buttonBox) + self.setLayout(self.mainLayout) + QMetaObject.connectSlotsByName(self) + def setComments(self, text): + self.commentEdit.setPlainText(text) + + def comments(self): + return self.commentEdit.toPlainText() + def getAvailableDependencies(self): # spellok if self.childId is None: dependent = [] @@ -378,9 +405,9 @@ class ModelerParametersDialog(QDialog): [isinstance(subval, QgsProcessingModelChildParameterSource) for subval in val])): val = [QgsProcessingModelChildParameterSource.fromStaticValue(val)] for subval in val: - if (isinstance(subval, QgsProcessingModelChildParameterSource) and - subval.source() == QgsProcessingModelChildParameterSource.StaticValue and - not param.checkValueIsAcceptable(subval.staticValue())) \ + if (isinstance(subval, QgsProcessingModelChildParameterSource) + and subval.source() == QgsProcessingModelChildParameterSource.StaticValue + and not param.checkValueIsAcceptable(subval.staticValue())) \ or (subval is None and not param.flags() & QgsProcessingParameterDefinition.FlagOptional): self.bar.pushMessage(self.tr("Error"), self.tr("Wrong or missing value for parameter '{}'").format( param.description()), @@ -419,6 +446,7 @@ class ModelerParametersDialog(QDialog): #except: # pass + alg.comment().setDescription(self.comments()) return alg def okPressed(self): diff --git a/python/plugins/processing/modeler/ModelerScene.py b/python/plugins/processing/modeler/ModelerScene.py index dd5b3352289..aaf1acc078f 100644 --- a/python/plugins/processing/modeler/ModelerScene.py +++ b/python/plugins/processing/modeler/ModelerScene.py @@ -25,7 +25,8 @@ from qgis.gui import QgsModelGraphicsScene from processing.modeler.ModelerGraphicItem import ( ModelerInputGraphicItem, ModelerOutputGraphicItem, - ModelerChildAlgorithmGraphicItem + ModelerChildAlgorithmGraphicItem, + ModelerCommentGraphicItem ) @@ -48,3 +49,6 @@ class ModelerScene(QgsModelGraphicsScene): def createOutputGraphicItem(self, model, output): return ModelerOutputGraphicItem(output.clone(), model) + + #def createCommentGraphicItem(self, model, comment, parent): + # return ModelerCommentGraphicItem(model, comment.clone(), parent) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 24f0a88c643..127670642aa 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -152,6 +152,7 @@ SET(QGIS_CORE_SRCS processing/models/qgsprocessingmodelalgorithm.cpp processing/models/qgsprocessingmodelchildalgorithm.cpp processing/models/qgsprocessingmodelchildparametersource.cpp + processing/models/qgsprocessingmodelcomment.cpp processing/models/qgsprocessingmodelcomponent.cpp processing/models/qgsprocessingmodelparameter.cpp processing/models/qgsprocessingmodeloutput.cpp @@ -1169,6 +1170,7 @@ SET(QGIS_CORE_HDRS processing/models/qgsprocessingmodelalgorithm.h processing/models/qgsprocessingmodelchildalgorithm.h processing/models/qgsprocessingmodelchildparametersource.h + processing/models/qgsprocessingmodelcomment.h processing/models/qgsprocessingmodelcomponent.h processing/models/qgsprocessingmodeloutput.h processing/models/qgsprocessingmodelparameter.h diff --git a/src/core/processing/models/qgsprocessingmodelchildalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelchildalgorithm.cpp index 92d27a83db5..f385c606946 100644 --- a/src/core/processing/models/qgsprocessingmodelchildalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelchildalgorithm.cpp @@ -35,6 +35,7 @@ QgsProcessingModelChildAlgorithm::QgsProcessingModelChildAlgorithm( const QgsPro , mModelOutputs( other.mModelOutputs ) , mActive( other.mActive ) , mDependencies( other.mDependencies ) + , mComment( other.mComment ) { setAlgorithmId( other.algorithmId() ); } @@ -49,6 +50,7 @@ QgsProcessingModelChildAlgorithm &QgsProcessingModelChildAlgorithm::operator=( c mModelOutputs = other.mModelOutputs; mActive = other.mActive; mDependencies = other.mDependencies; + mComment = other.mComment; return *this; } @@ -89,6 +91,7 @@ QVariant QgsProcessingModelChildAlgorithm::toVariant() const map.insert( QStringLiteral( "alg_config" ), mConfiguration ); map.insert( QStringLiteral( "active" ), mActive ); map.insert( QStringLiteral( "dependencies" ), mDependencies ); + map.insert( QStringLiteral( "comment" ), mComment.toVariant() ); saveCommonProperties( map ); @@ -126,6 +129,7 @@ bool QgsProcessingModelChildAlgorithm::loadVariant( const QVariant &child ) setAlgorithmId( map.value( QStringLiteral( "alg_id" ) ).toString() ); mActive = map.value( QStringLiteral( "active" ) ).toBool(); mDependencies = map.value( QStringLiteral( "dependencies" ) ).toStringList(); + mComment.loadVariant( map.value( QStringLiteral( "comment" ) ).toMap() ); restoreCommonProperties( map ); diff --git a/src/core/processing/models/qgsprocessingmodelchildalgorithm.h b/src/core/processing/models/qgsprocessingmodelchildalgorithm.h index aa0b17b28ed..63cfbeda68b 100644 --- a/src/core/processing/models/qgsprocessingmodelchildalgorithm.h +++ b/src/core/processing/models/qgsprocessingmodelchildalgorithm.h @@ -23,6 +23,7 @@ #include "qgsprocessingmodelcomponent.h" #include "qgsprocessingmodelchildparametersource.h" #include "qgsprocessingmodeloutput.h" +#include "qgsprocessingmodelcomment.h" #include class QgsProcessingModelAlgorithm; @@ -261,6 +262,10 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithm : public QgsProcessingModelCo QStringList asPythonCode( QgsProcessing::PythonOutputType outputType, const QgsStringMap &extraParameters, int currentIndent, int indentSize, const QMap &friendlyChildNames, const QMap &friendlyOutputNames ) const; + SIP_SKIP const QgsProcessingModelComment *comment() const override { return &mComment; } + QgsProcessingModelComment *comment() override { return &mComment; } + void setComment( const QgsProcessingModelComment &comment ) override { mComment = comment; } + private: QString mId; @@ -281,6 +286,8 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithm : public QgsProcessingModelCo //! List of child algorithms from the parent model on which this algorithm is dependent QStringList mDependencies; + QgsProcessingModelComment mComment; + friend class TestQgsProcessing; }; diff --git a/src/core/processing/models/qgsprocessingmodelcomment.cpp b/src/core/processing/models/qgsprocessingmodelcomment.cpp new file mode 100644 index 00000000000..d003803d056 --- /dev/null +++ b/src/core/processing/models/qgsprocessingmodelcomment.cpp @@ -0,0 +1,47 @@ +/*************************************************************************** + qgsprocessingmodelcomment.cpp + -------------------------- + begin : February 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsprocessingmodelcomment.h" + +///@cond NOT_STABLE + +QgsProcessingModelComment::QgsProcessingModelComment( const QString &description ) + : QgsProcessingModelComponent( description ) +{ + setSize( QSizeF( 100, 60 ) ); +} + +QgsProcessingModelComment *QgsProcessingModelComment::clone() const +{ + return new QgsProcessingModelComment( *this ); +} + +QVariant QgsProcessingModelComment::toVariant() const +{ + QVariantMap map; + saveCommonProperties( map ); + return map; +} + +bool QgsProcessingModelComment::loadVariant( const QVariantMap &map ) +{ + restoreCommonProperties( map ); + return true; +} + + +///@endcond diff --git a/src/core/processing/models/qgsprocessingmodelcomment.h b/src/core/processing/models/qgsprocessingmodelcomment.h new file mode 100644 index 00000000000..f5e2d155668 --- /dev/null +++ b/src/core/processing/models/qgsprocessingmodelcomment.h @@ -0,0 +1,59 @@ +/*************************************************************************** + qgsprocessingmodelcomment.h + -------------------------- + begin : February 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSPROCESSINGMODELCOMMENT_H +#define QGSPROCESSINGMODELCOMMENT_H + +#include "qgis_core.h" +#include "qgis.h" +#include "qgsprocessingmodelcomponent.h" +#include "qgsprocessingparameters.h" + +///@cond NOT_STABLE + +/** + * Represents a comment in a model. + * \ingroup core + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsProcessingModelComment : public QgsProcessingModelComponent +{ + public: + + /** + * Constructor for QgsProcessingModelComment with the specified \a description. + */ + QgsProcessingModelComment( const QString &description = QString() ); + + QgsProcessingModelComment *clone() const override SIP_FACTORY; + + /** + * Saves this comment to a QVariant. + * \see loadVariant() + */ + QVariant toVariant() const; + + /** + * Loads this comment from a QVariantMap. + * \see toVariant() + */ + bool loadVariant( const QVariantMap &map ); +}; + +///@endcond + +#endif // QGSPROCESSINGMODELCOMMENT_H diff --git a/src/core/processing/models/qgsprocessingmodelcomponent.cpp b/src/core/processing/models/qgsprocessingmodelcomponent.cpp index 723261f78e4..f6d00136038 100644 --- a/src/core/processing/models/qgsprocessingmodelcomponent.cpp +++ b/src/core/processing/models/qgsprocessingmodelcomponent.cpp @@ -88,6 +88,11 @@ void QgsProcessingModelComponent::setLinksCollapsed( Qt::Edge edge, bool collaps } } +void QgsProcessingModelComponent::setComment( const QgsProcessingModelComment & ) +{ + +} + void QgsProcessingModelComponent::saveCommonProperties( QVariantMap &map ) const { map.insert( QStringLiteral( "component_pos_x" ), mPosition.x() ); diff --git a/src/core/processing/models/qgsprocessingmodelcomponent.h b/src/core/processing/models/qgsprocessingmodelcomponent.h index 51acdfbd27f..38dc49e23d2 100644 --- a/src/core/processing/models/qgsprocessingmodelcomponent.h +++ b/src/core/processing/models/qgsprocessingmodelcomponent.h @@ -23,6 +23,8 @@ #include #include +class QgsProcessingModelComment; + ///@cond NOT_STABLE /** @@ -87,6 +89,24 @@ class CORE_EXPORT QgsProcessingModelComponent */ void setLinksCollapsed( Qt::Edge edge, bool collapsed ); + /** + * Returns the comment attached to this component (may be NULLPTR) + * \see setComment() + */ + SIP_SKIP virtual const QgsProcessingModelComment *comment() const { return nullptr; } + + /** + * Returns the comment attached to this component (may be NULLPTR) + * \see setComment() + */ + virtual QgsProcessingModelComment *comment() { return nullptr; } + + /** + * Sets the \a comment attached to this component. + * \see comment() + */ + virtual void setComment( const QgsProcessingModelComment &comment ); + /** * Clones the component. * diff --git a/src/core/processing/models/qgsprocessingmodeloutput.cpp b/src/core/processing/models/qgsprocessingmodeloutput.cpp index 18bddc85f35..a5eaa357f33 100644 --- a/src/core/processing/models/qgsprocessingmodeloutput.cpp +++ b/src/core/processing/models/qgsprocessingmodeloutput.cpp @@ -48,6 +48,7 @@ QVariant QgsProcessingModelOutput::toVariant() const map.insert( QStringLiteral( "child_id" ), mChildId ); map.insert( QStringLiteral( "output_name" ), mOutputName ); map.insert( QStringLiteral( "mandatory" ), mMandatory ); + map.insert( QStringLiteral( "comment" ), mComment.toVariant() ); saveCommonProperties( map ); return map; } @@ -79,6 +80,7 @@ bool QgsProcessingModelOutput::loadVariant( const QVariantMap &map ) mChildId = map.value( QStringLiteral( "child_id" ) ).toString(); mOutputName = map.value( QStringLiteral( "output_name" ) ).toString(); mMandatory = map.value( QStringLiteral( "mandatory" ), false ).toBool(); + mComment.loadVariant( map.value( QStringLiteral( "comment" ) ).toMap() ); restoreCommonProperties( map ); return true; } diff --git a/src/core/processing/models/qgsprocessingmodeloutput.h b/src/core/processing/models/qgsprocessingmodeloutput.h index 1e2541d9f87..f64b6905ee1 100644 --- a/src/core/processing/models/qgsprocessingmodeloutput.h +++ b/src/core/processing/models/qgsprocessingmodeloutput.h @@ -22,6 +22,7 @@ #include "qgis.h" #include "qgsprocessingmodelcomponent.h" #include "qgsprocessingparameters.h" +#include "qgsprocessingmodelcomment.h" ///@cond NOT_STABLE @@ -121,6 +122,10 @@ class CORE_EXPORT QgsProcessingModelOutput : public QgsProcessingModelComponent */ bool loadVariant( const QVariantMap &map ); + SIP_SKIP const QgsProcessingModelComment *comment() const override { return &mComment; } + QgsProcessingModelComment *comment() override { return &mComment; } + void setComment( const QgsProcessingModelComment &comment ) override { mComment = comment; } + private: QString mName; @@ -128,6 +133,9 @@ class CORE_EXPORT QgsProcessingModelOutput : public QgsProcessingModelComponent QString mChildId; QString mOutputName; bool mMandatory = false; + + QgsProcessingModelComment mComment; + }; ///@endcond diff --git a/src/core/processing/models/qgsprocessingmodelparameter.cpp b/src/core/processing/models/qgsprocessingmodelparameter.cpp index b721dcfa8b3..4cd52cafa59 100644 --- a/src/core/processing/models/qgsprocessingmodelparameter.cpp +++ b/src/core/processing/models/qgsprocessingmodelparameter.cpp @@ -34,6 +34,7 @@ QVariant QgsProcessingModelParameter::toVariant() const { QVariantMap map; map.insert( QStringLiteral( "name" ), mParameterName ); + map.insert( QStringLiteral( "comment" ), mComment.toVariant() ); saveCommonProperties( map ); return map; } @@ -41,6 +42,7 @@ QVariant QgsProcessingModelParameter::toVariant() const bool QgsProcessingModelParameter::loadVariant( const QVariantMap &map ) { mParameterName = map.value( QStringLiteral( "name" ) ).toString(); + mComment.loadVariant( map.value( QStringLiteral( "comment" ) ).toMap() ); restoreCommonProperties( map ); return true; } diff --git a/src/core/processing/models/qgsprocessingmodelparameter.h b/src/core/processing/models/qgsprocessingmodelparameter.h index 6b54d346be1..f6d63973e35 100644 --- a/src/core/processing/models/qgsprocessingmodelparameter.h +++ b/src/core/processing/models/qgsprocessingmodelparameter.h @@ -21,6 +21,7 @@ #include "qgis_core.h" #include "qgis.h" #include "qgsprocessingmodelcomponent.h" +#include "qgsprocessingmodelcomment.h" ///@cond NOT_STABLE @@ -68,10 +69,16 @@ class CORE_EXPORT QgsProcessingModelParameter : public QgsProcessingModelCompone */ bool loadVariant( const QVariantMap &map ); + SIP_SKIP const QgsProcessingModelComment *comment() const override { return &mComment; } + QgsProcessingModelComment *comment() override { return &mComment; } + void setComment( const QgsProcessingModelComment &comment ) override { mComment = comment; } + private: QString mParameterName; + QgsProcessingModelComment mComment; + }; ///@endcond diff --git a/src/core/qgsxmlutils.cpp b/src/core/qgsxmlutils.cpp index a3e0ce0ef29..13d32b59455 100644 --- a/src/core/qgsxmlutils.cpp +++ b/src/core/qgsxmlutils.cpp @@ -20,6 +20,7 @@ #include "qgsrectangle.h" #include "qgsproperty.h" #include "qgssymbollayerutils.h" +#include "qgsprocessingparameters.h" QgsUnitTypes::DistanceUnit QgsXmlUtils::readMapUnits( const QDomElement &element ) { @@ -213,7 +214,15 @@ QDomElement QgsXmlUtils::writeVariant( const QVariant &value, QDomDocument &doc element.setAttribute( QStringLiteral( "value" ), geom.asWkt() ); break; } - FALLTHROUGH + else if ( value.canConvert< QgsProcessingOutputLayerDefinition >() ) + { + QDomElement valueElement = writeVariant( value.value< QgsProcessingOutputLayerDefinition >().toVariant(), doc ); + element.appendChild( valueElement ); + element.setAttribute( QStringLiteral( "type" ), QStringLiteral( "QgsProcessingOutputLayerDefinition" ) ); + break; + } + Q_ASSERT_X( false, "QgsXmlUtils::writeVariant", QStringLiteral( "unsupported user variant type %1" ).arg( QMetaType::typeName( value.userType() ) ).toLocal8Bit() ); + break; } default: @@ -338,6 +347,18 @@ QVariant QgsXmlUtils::readVariant( const QDomElement &element ) { return QgsGeometry::fromWkt( element.attribute( "value" ) ); } + else if ( type == QLatin1String( "QgsProcessingOutputLayerDefinition" ) ) + { + QgsProcessingOutputLayerDefinition res; + const QDomNodeList values = element.childNodes(); + if ( values.isEmpty() ) + return QVariant(); + + if ( res.loadVariant( QgsXmlUtils::readVariant( values.at( 0 ).toElement() ).toMap() ) ) + return res; + + return QVariant(); + } else { return QVariant(); diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp index 933ff21fdbb..898ddbf39bb 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp @@ -131,14 +131,7 @@ QVariant QgsModelComponentGraphicItem::itemChange( QGraphicsItem::GraphicsItemCh mComponent->setPosition( pos() ); // also need to update the model's stored component's position - // TODO - this is not so nice, consider moving this to model class - if ( QgsProcessingModelChildAlgorithm *child = dynamic_cast< QgsProcessingModelChildAlgorithm * >( mComponent.get() ) ) - mModel->childAlgorithm( child->childId() ).setPosition( pos() ); - else if ( QgsProcessingModelParameter *param = dynamic_cast< QgsProcessingModelParameter * >( mComponent.get() ) ) - mModel->parameterComponent( param->parameterName() ).setPosition( pos() ); - else if ( QgsProcessingModelOutput *output = dynamic_cast< QgsProcessingModelOutput * >( mComponent.get() ) ) - mModel->childAlgorithm( output->childId() ).modelOutput( output->name() ).setPosition( pos() ); - + updateStoredComponentPosition( pos() ); break; } case QGraphicsItem::ItemSelectedChange: @@ -197,7 +190,9 @@ void QgsModelComponentGraphicItem::paint( QPainter *painter, const QStyleOptionG QColor color = fillColor( state() ); QColor stroke = strokeColor( state() ); - painter->setPen( QPen( stroke, 0 ) ); // 0 width "cosmetic" pen + QPen strokePen = QPen( stroke, 0 ) ; // 0 width "cosmetic" pen + strokePen.setStyle( strokeStyle( state() ) ); + painter->setPen( strokePen ); painter->setBrush( QBrush( color, Qt::SolidPattern ) ); painter->drawRect( rect ); painter->setFont( font() ); @@ -291,6 +286,11 @@ QString QgsModelComponentGraphicItem::truncatedTextForItem( const QString &text return t; } +Qt::PenStyle QgsModelComponentGraphicItem::strokeStyle( QgsModelComponentGraphicItem::State ) const +{ + return Qt::SolidLine; +} + QPicture QgsModelComponentGraphicItem::iconPicture() const { return QPicture(); @@ -558,6 +558,12 @@ QPicture QgsModelParameterGraphicItem::iconPicture() const return mPicture; } +void QgsModelParameterGraphicItem::updateStoredComponentPosition( const QPointF &pos ) +{ + if ( QgsProcessingModelParameter *param = dynamic_cast< QgsProcessingModelParameter * >( component() ) ) + model()->parameterComponent( param->parameterName() ).setPosition( pos ); +} + void QgsModelParameterGraphicItem::deleteComponent() { if ( const QgsProcessingModelParameter *param = dynamic_cast< const QgsProcessingModelParameter * >( component() ) ) @@ -733,6 +739,12 @@ QString QgsModelChildAlgorithmGraphicItem::linkPointText( Qt::Edge edge, int ind return QString(); } +void QgsModelChildAlgorithmGraphicItem::updateStoredComponentPosition( const QPointF &pos ) +{ + if ( QgsProcessingModelChildAlgorithm *child = dynamic_cast< QgsProcessingModelChildAlgorithm * >( component() ) ) + model()->childAlgorithm( child->childId() ).setPosition( pos ); +} + void QgsModelChildAlgorithmGraphicItem::deleteComponent() { if ( const QgsProcessingModelChildAlgorithm *child = dynamic_cast< const QgsProcessingModelChildAlgorithm * >( component() ) ) @@ -830,6 +842,12 @@ QPicture QgsModelOutputGraphicItem::iconPicture() const return mPicture; } +void QgsModelOutputGraphicItem::updateStoredComponentPosition( const QPointF &pos ) +{ + if ( QgsProcessingModelOutput *output = dynamic_cast< QgsProcessingModelOutput * >( component() ) ) + model()->childAlgorithm( output->childId() ).modelOutput( output->name() ).setPosition( pos ); +} + void QgsModelOutputGraphicItem::deleteComponent() { if ( const QgsProcessingModelOutput *output = dynamic_cast< const QgsProcessingModelOutput * >( component() ) ) @@ -842,4 +860,115 @@ void QgsModelOutputGraphicItem::deleteComponent() } + + +QgsModelCommentGraphicItem::QgsModelCommentGraphicItem( QgsProcessingModelComment *comment, QgsModelComponentGraphicItem *parentItem, QgsProcessingModelAlgorithm *model, QGraphicsItem *parent ) + : QgsModelComponentGraphicItem( comment, model, parent ) + , mParentComponent( parentItem->component()->clone() ) + , mParentItem( parentItem ) +{ + setLabel( comment->description() ); + + QFont f = font(); + f.setPixelSize( 9 ); + setFont( f ); +} + +void QgsModelCommentGraphicItem::contextMenuEvent( QGraphicsSceneContextMenuEvent *event ) +{ + QMenu *popupmenu = new QMenu( event->widget() ); + QAction *removeAction = popupmenu->addAction( QObject::tr( "Remove" ) ); + connect( removeAction, &QAction::triggered, this, &QgsModelCommentGraphicItem::deleteComponent ); + QAction *editAction = popupmenu->addAction( QObject::tr( "Edit" ) ); + connect( editAction, &QAction::triggered, this, &QgsModelCommentGraphicItem::editComponent ); + popupmenu->exec( event->screenPos() ); +} + +QgsModelCommentGraphicItem::~QgsModelCommentGraphicItem() = default; + +QColor QgsModelCommentGraphicItem::fillColor( QgsModelComponentGraphicItem::State state ) const +{ + QColor c( 230, 230, 230 ); + switch ( state ) + { + case Selected: + c = c.darker( 110 ); + break; + case Hover: + c = c.darker( 105 ); + break; + + case Normal: + break; + } + return c; +} + +QColor QgsModelCommentGraphicItem::strokeColor( QgsModelComponentGraphicItem::State state ) const +{ + switch ( state ) + { + case Selected: + return QColor( 50, 50, 50 ); + case Hover: + case Normal: + return QColor( 150, 150, 150 ); + } + return QColor(); +} + +QColor QgsModelCommentGraphicItem::textColor( QgsModelComponentGraphicItem::State ) const +{ + return QColor( 100, 100, 100 ); +} + +Qt::PenStyle QgsModelCommentGraphicItem::strokeStyle( QgsModelComponentGraphicItem::State ) const +{ + return Qt::DotLine; +} + +void QgsModelCommentGraphicItem::updateStoredComponentPosition( const QPointF &pos ) +{ + if ( QgsProcessingModelComment *comment = modelComponent() ) + { + comment->setPosition( pos ); + } +} + +void QgsModelCommentGraphicItem::deleteComponent() +{ + if ( QgsProcessingModelComment *comment = modelComponent() ) + { + comment->setDescription( QString() ); + emit changed(); + emit requestModelRepaint(); + } +} + +void QgsModelCommentGraphicItem::editComponent() +{ + if ( mParentItem ) + { + mParentItem->editComment(); + } +} + +QgsProcessingModelComment *QgsModelCommentGraphicItem::modelComponent() +{ + if ( const QgsProcessingModelChildAlgorithm *child = dynamic_cast< const QgsProcessingModelChildAlgorithm * >( mParentComponent.get() ) ) + { + return model()->childAlgorithm( child->childId() ).comment(); + } + else if ( const QgsProcessingModelParameter *param = dynamic_cast< const QgsProcessingModelParameter * >( mParentComponent.get() ) ) + { + return model()->parameterComponent( param->parameterName() ).comment(); + } + else if ( const QgsProcessingModelOutput *output = dynamic_cast< const QgsProcessingModelOutput * >( mParentComponent.get() ) ) + { + return model()->childAlgorithm( output->childId() ).modelOutput( output->name() ).comment(); + } + return nullptr; +} + + ///@endcond diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h index a6e5dfdadc6..ab75a3201a3 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h @@ -21,11 +21,13 @@ #include #include #include +#include class QgsProcessingModelComponent; class QgsProcessingModelParameter; class QgsProcessingModelChildAlgorithm; class QgsProcessingModelOutput; +class QgsProcessingModelComment; class QgsProcessingModelAlgorithm; class QgsModelDesignerFlatButtonGraphicItem; class QgsModelDesignerFoldButtonGraphicItem; @@ -158,6 +160,13 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject */ QPointF calculateAutomaticLinkPoint( const QPointF &point, Qt::Edge &edge SIP_OUT ) const; + /** + * Called when the comment attached to the item should be edited. + * + * The default implementation does nothing. + */ + virtual void editComment() {} + signals: // TODO - rework this, should be triggered externally when the model actually changes! @@ -222,6 +231,11 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject */ virtual QColor textColor( State state ) const = 0; + /** + * Returns the stroke style to use while rendering the outline of the item. + */ + virtual Qt::PenStyle strokeStyle( State state ) const; + /** * Returns a QPicture version of the item's icon, if available. */ @@ -232,6 +246,11 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject */ virtual QPixmap iconPixmap() const; + /** + * Updates the position stored in the model for the associated comment + */ + virtual void updateStoredComponentPosition( const QPointF &pos ) = 0; + private: void updateToolTip( const QPointF &pos ); @@ -292,6 +311,7 @@ class GUI_EXPORT QgsModelParameterGraphicItem : public QgsModelComponentGraphicI QColor strokeColor( State state ) const override; QColor textColor( State state ) const override; QPicture iconPicture() const override; + void updateStoredComponentPosition( const QPointF &pos ) override; protected slots: @@ -337,6 +357,7 @@ class GUI_EXPORT QgsModelChildAlgorithmGraphicItem : public QgsModelComponentGra int linkPointCount( Qt::Edge edge ) const override; QString linkPointText( Qt::Edge edge, int index ) const override; + void updateStoredComponentPosition( const QPointF &pos ) override; protected slots: @@ -382,6 +403,7 @@ class GUI_EXPORT QgsModelOutputGraphicItem : public QgsModelComponentGraphicItem QColor strokeColor( State state ) const override; QColor textColor( State state ) const override; QPicture iconPicture() const override; + void updateStoredComponentPosition( const QPointF &pos ) override; protected slots: @@ -391,6 +413,57 @@ class GUI_EXPORT QgsModelOutputGraphicItem : public QgsModelComponentGraphicItem QPicture mPicture; }; + + + +/** + * \ingroup gui + * \brief A graphic item representing a model comment in the model designer. + * \warning Not stable API + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsModelCommentGraphicItem : public QgsModelComponentGraphicItem +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsModelCommentGraphicItem for the specified \a comment, with the specified \a parent item. + * + * The \a model argument specifies the associated processing model. Ownership of \a model is not transferred, and + * it must exist for the lifetime of this object. + * + * Ownership of \a output is transferred to the item. + */ + QgsModelCommentGraphicItem( QgsProcessingModelComment *comment SIP_TRANSFER, + QgsModelComponentGraphicItem *parentItem, + QgsProcessingModelAlgorithm *model, + QGraphicsItem *parent SIP_TRANSFERTHIS ); + ~QgsModelCommentGraphicItem() override; + void contextMenuEvent( QGraphicsSceneContextMenuEvent *event ) override; + + protected: + + QColor fillColor( State state ) const override; + QColor strokeColor( State state ) const override; + QColor textColor( State state ) const override; + Qt::PenStyle strokeStyle( State state ) const override; + void updateStoredComponentPosition( const QPointF &pos ) override; + + protected slots: + + void deleteComponent() override; + void editComponent() override; + private: + + QgsProcessingModelComment *modelComponent(); + + std::unique_ptr< QgsProcessingModelComponent > mParentComponent; + QPointer< QgsModelComponentGraphicItem > mParentItem; + + +}; ///@endcond #endif // QGSMODELCOMPONENTGRAPHICITEM_H diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index c4d72ee35d7..4e2e0263c67 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -58,6 +58,11 @@ QgsModelComponentGraphicItem *QgsModelGraphicsScene::createOutputGraphicItem( Qg return new QgsModelOutputGraphicItem( output, model, nullptr ); } +QgsModelComponentGraphicItem *QgsModelGraphicsScene::createCommentGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelComment *comment, QgsModelComponentGraphicItem *parentItem ) const +{ + return new QgsModelCommentGraphicItem( comment, parentItem, model, nullptr ); +} + void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, QgsProcessingContext &context ) { // model input parameters @@ -70,6 +75,8 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs mParameterItems.insert( it.value().parameterName(), item ); connect( item, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); + + addCommentItemForComponent( model, it.value(), item ); } // input dependency arrows @@ -98,6 +105,8 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs mChildAlgorithmItems.insert( it.value().childId(), item ); connect( item, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); + + addCommentItemForComponent( model, it.value(), item ); } // arrows linking child algorithms @@ -148,6 +157,8 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs connect( item, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); + addCommentItemForComponent( model, outputIt.value(), item ); + QPointF pos = outputIt.value().position(); // find the actual index of the linked output from the child algorithm it comes from @@ -164,12 +175,7 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs i++; } -#if 0 - if pos is None: - pos = ( alg.position() + QPointF( alg.size().width(), 0 ) - + self.algItems[alg.childId()].linkPoint( Qt.BottomEdge, idx ) ) -#endif - item->setPos( pos ); + item->setPos( pos ); outputItems.insert( outputIt.key(), item ); addItem( new QgsModelArrowItem( mChildAlgorithmItems[it.value().childId()], Qt::BottomEdge, idx, item ) ); } @@ -249,5 +255,21 @@ QList QgsModelGraphicsScene::linkSourcesForPa return res; } +void QgsModelGraphicsScene::addCommentItemForComponent( QgsProcessingModelAlgorithm *model, const QgsProcessingModelComponent &component, QgsModelComponentGraphicItem *parentItem ) +{ + if ( !component.comment() || component.comment()->description().isEmpty() ) + return; + + QgsModelComponentGraphicItem *commentItem = createCommentGraphicItem( model, component.comment()->clone(), parentItem ); + commentItem->setPos( component.comment()->position().x(), component.comment()->position().y() ); + addItem( commentItem ); + connect( commentItem, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); + connect( commentItem, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); + + std::unique_ptr< QgsModelArrowItem > arrow = qgis::make_unique< QgsModelArrowItem >( parentItem, commentItem ); + arrow->setPenStyle( Qt::DotLine ); + addItem( arrow.release() ); +} + ///@endcond diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index 5830db8ca2c..b35a25d5376 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -26,6 +26,8 @@ class QgsModelComponentGraphicItem; class QgsProcessingModelParameter; class QgsProcessingModelChildAlgorithm; class QgsProcessingModelOutput; +class QgsProcessingModelComponent; +class QgsProcessingModelComment; ///@cond NOT_STABLE @@ -118,6 +120,13 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene */ virtual QgsModelComponentGraphicItem *createOutputGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelOutput *output ) const SIP_FACTORY; + /** + * Creates a new graphic item for a model comment. + */ + virtual QgsModelComponentGraphicItem *createCommentGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelComment *comment, + QgsModelComponentGraphicItem *parentItem ) const SIP_FACTORY; + + private: struct LinkSource @@ -128,6 +137,8 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene }; QList< LinkSource > linkSourcesForParameterValue( QgsProcessingModelAlgorithm *model, const QVariant &value, const QString &childId, QgsProcessingContext &context ) const; + void addCommentItemForComponent( QgsProcessingModelAlgorithm *model, const QgsProcessingModelComponent &component, QgsModelComponentGraphicItem *parentItem ); + Flags mFlags = nullptr; QMap< QString, QgsModelComponentGraphicItem * > mParameterItems;