-
Notifications
You must be signed in to change notification settings - Fork 85
Feature/feature drafts #4652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Feature/feature drafts #4652
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -20,6 +20,12 @@ | |||||
|
|
||||||
| #include <QDebug> | ||||||
| #include <QSet> | ||||||
| #include <QTimer> | ||||||
| #include <QDateTime> | ||||||
| #include <QJsonObject> | ||||||
| #include <QJsonArray> | ||||||
|
|
||||||
| #include "featuredraftstorage.h" | ||||||
|
|
||||||
| #include "qgis.h" | ||||||
| #include "qgsproject.h" | ||||||
|
|
@@ -41,7 +47,11 @@ | |||||
| AttributeController::AttributeController( QObject *parent ) | ||||||
| : QObject( parent ) | ||||||
| , mAttributeTabProxyModel( new AttributeTabProxyModel() ) | ||||||
| , mDraftSaveTimer( new QTimer( this ) ) | ||||||
| { | ||||||
| mDraftSaveTimer->setSingleShot( true ); | ||||||
| mDraftSaveTimer->setInterval( 1000 ); | ||||||
| connect( mDraftSaveTimer, &QTimer::timeout, this, &AttributeController::saveDraft ); | ||||||
| } | ||||||
|
|
||||||
| void AttributeController::reset() | ||||||
|
|
@@ -67,8 +77,17 @@ void AttributeController::setFeatureLayerPair( const FeatureLayerPair &pair ) | |||||
| blockSignals( true ); | ||||||
|
|
||||||
| bool hasLayerChanged = mFeatureLayerPair.layer() != pair.layer(); | ||||||
| // geometry edits round-trip back into this same setter (via the live QML | ||||||
| // binding once the geometry-editing map tool hands the feature back) - that | ||||||
| // must not wipe attribute changes already tracked for this same feature | ||||||
| bool isSameFeature = !hasLayerChanged && mFeatureLayerPair.feature().id() == pair.feature().id(); | ||||||
|
|
||||||
| // Set new active pair | ||||||
| mFeatureLayerPair = pair; | ||||||
| if ( !isSameFeature ) | ||||||
| { | ||||||
| mTouchedFieldIndices.clear(); | ||||||
| } | ||||||
| if ( hasLayerChanged ) | ||||||
| { | ||||||
| // layer changed! | ||||||
|
|
@@ -646,6 +665,72 @@ bool AttributeController::isNewFeature() const | |||||
| return FID_IS_NEW( id ) || FID_IS_NULL( id ); | ||||||
| } | ||||||
|
|
||||||
| QJsonObject AttributeController::attributeToJson( const QgsFields &fields, const QgsFeature &feature, int fieldIndex ) const | ||||||
| { | ||||||
| QJsonObject attribute; | ||||||
| attribute[ QStringLiteral( "name" ) ] = fields.at( fieldIndex ).name(); | ||||||
| attribute[ QStringLiteral( "type" ) ] = fields.at( fieldIndex ).typeName(); | ||||||
| attribute[ QStringLiteral( "value" ) ] = QJsonValue::fromVariant( feature.attribute( fieldIndex ) ); | ||||||
| return attribute; | ||||||
| } | ||||||
|
|
||||||
| void AttributeController::saveDraft() | ||||||
| { | ||||||
| if ( !mFeatureLayerPair.layer() ) | ||||||
| return; | ||||||
|
|
||||||
| const QgsFeature feature = mFeatureLayerPair.feature(); | ||||||
| const QgsFields fields = feature.fields(); | ||||||
| const bool featureIsNew = isNewFeature(); | ||||||
|
|
||||||
| QJsonArray attributes; | ||||||
|
|
||||||
| if ( featureIsNew ) | ||||||
| { | ||||||
| for ( int i = 0; i < feature.attributeCount(); ++i ) | ||||||
| { | ||||||
| attributes.append( attributeToJson( fields, feature, i ) ); | ||||||
| } | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| for ( int fieldIndex : mTouchedFieldIndices ) | ||||||
| { | ||||||
| if ( fieldIndex >= 0 && fieldIndex < feature.attributeCount() ) | ||||||
| { | ||||||
| attributes.append( attributeToJson( fields, feature, fieldIndex ) ); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| QJsonObject draft; | ||||||
| draft[ QStringLiteral( "layerId" ) ] = mFeatureLayerPair.layer()->id(); | ||||||
| draft[ QStringLiteral( "stage" ) ] = QStringLiteral( "attributeForm" ); | ||||||
| draft[ QStringLiteral( "timestamp" ) ] = QDateTime::currentDateTimeUtc().toString( Qt::ISODate ); | ||||||
| draft[ QStringLiteral( "attributes" ) ] = attributes; | ||||||
|
|
||||||
| if ( featureIsNew ) | ||||||
| { | ||||||
| // existing-feature geometry edits are drafted separately, by RecordingMapTool | ||||||
| draft[ QStringLiteral( "geometry" ) ] = feature.geometry().asWkt(); | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| draft[ QStringLiteral( "featureId" ) ] = QJsonValue( static_cast<qint64>( feature.id() ) ); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
redundant cast |
||||||
| } | ||||||
|
|
||||||
| FeatureDraftStorage::saveDraft( QgsProject::instance()->homePath(), draft ); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm not so sure about this, we should use project id's if possible |
||||||
| } | ||||||
|
|
||||||
| void AttributeController::clearDraft() | ||||||
| { | ||||||
| // a pending debounced write must not be allowed to resurrect the draft | ||||||
| // after we've just told the storage (and possibly the user) it's gone | ||||||
| mDraftSaveTimer->stop(); | ||||||
|
|
||||||
| FeatureDraftStorage::clearDraft( QgsProject::instance()->homePath() ); | ||||||
| } | ||||||
|
|
||||||
| void AttributeController::acquireId() | ||||||
| { | ||||||
| if ( !mFeatureLayerPair.layer() ) | ||||||
|
|
@@ -1203,6 +1288,7 @@ bool AttributeController::deleteFeature() | |||||
| { | ||||||
| mFeatureLayerPair = FeatureLayerPair(); | ||||||
| emit featureLayerPairChanged(); | ||||||
| clearDraft(); | ||||||
| emit changesCommited(); | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -1214,6 +1300,8 @@ bool AttributeController::rollback() | |||||
| if ( !mFeatureLayerPair.layer() ) | ||||||
| return false; | ||||||
|
|
||||||
| clearDraft(); | ||||||
|
|
||||||
| if ( !mFeatureLayerPair.layer()->isEditable() ) | ||||||
| { | ||||||
| return false; | ||||||
|
|
@@ -1281,6 +1369,7 @@ bool AttributeController::save() | |||||
|
|
||||||
| if ( rv ) | ||||||
| { | ||||||
| clearDraft(); | ||||||
| emit changesCommited(); | ||||||
| } | ||||||
| else | ||||||
|
|
@@ -1509,6 +1598,8 @@ bool AttributeController::setFormValue( const QUuid &id, QVariant value ) | |||||
| { | ||||||
| mFeatureLayerPair.featureRef().setAttribute( item->fieldIndex(), val ); | ||||||
| emit formDataChanged( item->id(), { AttributeFormModel::AttributeValue, AttributeFormModel::RawValueIsNull, AttributeFormModel::HasMixedValues } ); | ||||||
| mTouchedFieldIndices.insert( item->fieldIndex() ); | ||||||
| mDraftSaveTimer->start(); | ||||||
| } | ||||||
| recalculateDerivedItems( true, false ); | ||||||
| return true; | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -21,8 +21,10 @@ | |||||
| #include <QVariant> | ||||||
| #include <memory> | ||||||
| #include <QMap> | ||||||
| #include <QSet> | ||||||
| #include <QVector> | ||||||
| #include <QUuid> | ||||||
| #include <QJsonObject> | ||||||
|
|
||||||
| #include "featurelayerpair.h" | ||||||
| #include "attributedata.h" | ||||||
|
|
@@ -40,6 +42,7 @@ | |||||
| class AttributeFormModel; | ||||||
| class AttributeTabModel; | ||||||
| class QgsVectorLayer; | ||||||
| class QTimer; | ||||||
|
|
||||||
| /** | ||||||
| * This is implementation of the controller between Attribute*Model | ||||||
|
|
@@ -185,6 +188,17 @@ class AttributeController : public QObject | |||||
|
|
||||||
| bool isNewFeature() const; | ||||||
|
|
||||||
| // Persists attribute changes as a draft, debounced. New feature: all attributes. | ||||||
| // Existing feature: only touched fields, so an untouched one is never clobbered | ||||||
| // by a concurrent change made elsewhere via sync. | ||||||
| void saveDraft(); | ||||||
|
|
||||||
| //! Removes any persisted draft for the current project | ||||||
| void clearDraft(); | ||||||
|
|
||||||
| //! Builds the {name, type, value} JSON object for one attribute, used by saveDraft() | ||||||
| QJsonObject attributeToJson( const QgsFields &fields, const QgsFeature &feature, int fieldIndex ) const; | ||||||
|
|
||||||
| /** | ||||||
| * Recalculates visibility & constrains & default values | ||||||
| * Note that reevaluate default values is needed only when an attribnute has changed. | ||||||
|
|
@@ -246,5 +260,10 @@ class AttributeController : public QObject | |||||
|
|
||||||
| AttributeController *mParentController = nullptr; // not owned | ||||||
| QgsRelation mLinkedRelation; | ||||||
|
|
||||||
| QTimer *mDraftSaveTimer = nullptr; // owned by this, debounces saveDraft() | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. raw pointer is unnecessary here |
||||||
|
|
||||||
| //! Indices of fields the user has actually changed this session - only valid for existing (edit-mode) features | ||||||
| QSet<int> mTouchedFieldIndices; | ||||||
| }; | ||||||
| #endif // ATTRIBUTECONTROLLER_H | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't we use this variant in all forms and if the new feature doesn't fill out everything let those missing fields use default value