-
Notifications
You must be signed in to change notification settings - Fork 88
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,10 @@ | |
|
|
||
| #include <QDebug> | ||
| #include <QSet> | ||
| #include <QTimer> | ||
| #include <QDateTime> | ||
|
|
||
| #include "featuredraftstorage.h" | ||
|
|
||
| #include "qgis.h" | ||
| #include "qgsproject.h" | ||
|
|
@@ -42,6 +46,9 @@ AttributeController::AttributeController( QObject *parent ) | |
| : QObject( parent ) | ||
| , mAttributeTabProxyModel( new AttributeTabProxyModel() ) | ||
| { | ||
| mDraftSaveTimer.setSingleShot( true ); | ||
| mDraftSaveTimer.setInterval( 1000 ); | ||
| connect( &mDraftSaveTimer, &QTimer::timeout, this, &AttributeController::saveDraft ); | ||
| } | ||
|
|
||
| void AttributeController::reset() | ||
|
|
@@ -67,8 +74,23 @@ 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(); | ||
|
|
||
| // draft immediately so a crash before the first keystroke still resumes into the form | ||
|
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. We actually do that here :) |
||
| if ( pair.layer() && isNewFeature() ) | ||
| { | ||
| saveDraft(); | ||
| } | ||
| } | ||
| if ( hasLayerChanged ) | ||
| { | ||
| // layer changed! | ||
|
|
@@ -646,6 +668,57 @@ bool AttributeController::isNewFeature() const | |
| return FID_IS_NEW( id ) || FID_IS_NULL( id ); | ||
| } | ||
|
|
||
| FeatureDraftAttribute AttributeController::toDraftAttribute( const QgsFields &fields, const QgsFeature &feature, int fieldIndex ) const | ||
| { | ||
| return { fields.at( fieldIndex ).name(), fields.at( fieldIndex ).typeName(), feature.attribute( fieldIndex ) }; | ||
| } | ||
|
|
||
| void AttributeController::saveDraft() | ||
| { | ||
| if ( !mFeatureLayerPair.layer() ) | ||
| return; | ||
|
|
||
| const QgsFeature feature = mFeatureLayerPair.feature(); | ||
| const QgsFields fields = feature.fields(); | ||
| const bool featureIsNew = isNewFeature(); | ||
|
|
||
| FeatureDraft draft; | ||
| draft.layerId = mFeatureLayerPair.layer()->id(); | ||
| draft.stage = FeatureDraft::AttributeForm; | ||
| draft.timestamp = QDateTime::currentDateTimeUtc(); | ||
|
|
||
| // only touched fields are drafted - an untouched one falls back to its | ||
| // default value expression on resume rather than a stale recorded value | ||
|
Comment on lines
+690
to
+691
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'd prefer if we stored everything, not just selected fields. Imagine there are automatic field values that are changed when a different field changes. They would not get stored. Let's instead try storing everything and when opening the feature again, do not evaluate anything, just present what we stored. |
||
| for ( int fieldIndex : mTouchedFieldIndices ) | ||
| { | ||
| if ( fieldIndex >= 0 && fieldIndex < feature.attributeCount() ) | ||
| { | ||
| draft.attributes.append( toDraftAttribute( fields, feature, fieldIndex ) ); | ||
| } | ||
| } | ||
|
|
||
| if ( featureIsNew ) | ||
| { | ||
| // existing-feature geometry edits are drafted separately, by RecordingMapTool | ||
| draft.geometry = feature.geometry(); | ||
| } | ||
| else | ||
| { | ||
| draft.featureId = feature.id(); | ||
| } | ||
|
|
||
| FeatureDraftStorage::saveDraft( QgsProject::instance()->homePath(), draft ); | ||
|
xkello marked this conversation as resolved.
|
||
| } | ||
|
|
||
| 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 +1276,7 @@ bool AttributeController::deleteFeature() | |
| { | ||
| mFeatureLayerPair = FeatureLayerPair(); | ||
| emit featureLayerPairChanged(); | ||
| clearDraft(); | ||
| emit changesCommited(); | ||
| } | ||
|
|
||
|
|
@@ -1214,6 +1288,8 @@ bool AttributeController::rollback() | |
| if ( !mFeatureLayerPair.layer() ) | ||
| return false; | ||
|
|
||
| clearDraft(); | ||
|
|
||
| if ( !mFeatureLayerPair.layer()->isEditable() ) | ||
| { | ||
| return false; | ||
|
|
@@ -1281,6 +1357,7 @@ bool AttributeController::save() | |
|
|
||
| if ( rv ) | ||
| { | ||
| clearDraft(); | ||
| emit changesCommited(); | ||
| } | ||
| else | ||
|
|
@@ -1509,6 +1586,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,14 +21,17 @@ | |
| #include <QVariant> | ||
| #include <memory> | ||
| #include <QMap> | ||
| #include <QSet> | ||
| #include <QVector> | ||
| #include <QUuid> | ||
| #include <QTimer> | ||
|
|
||
| #include "featurelayerpair.h" | ||
| #include "attributedata.h" | ||
| #include "attributeformproxymodel.h" | ||
| #include "attributetabproxymodel.h" | ||
| #include "rememberattributescontroller.h" | ||
| #include "featuredraft.h" | ||
|
|
||
| #include "qgsfeature.h" | ||
| #include "qgsproject.h" | ||
|
|
@@ -188,6 +191,15 @@ class AttributeController : public QObject | |
|
|
||
| bool isNewFeature() const; | ||
|
|
||
| // Persists touched attributes as a draft, debounced. | ||
| void saveDraft(); | ||
|
|
||
| //! Removes any persisted draft for the current project | ||
| void clearDraft(); | ||
|
|
||
| //! Builds a FeatureDraftAttribute for one attribute, used by saveDraft() | ||
| FeatureDraftAttribute toDraftAttribute( 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. | ||
|
|
@@ -249,5 +261,10 @@ class AttributeController : public QObject | |
|
|
||
| AttributeController *mParentController = nullptr; // not owned | ||
| QgsRelation mLinkedRelation; | ||
|
|
||
| QTimer mDraftSaveTimer; // debounces saveDraft() | ||
|
|
||
| //! Indices of fields the user has actually changed this session, used for drafting | ||
| QSet<int> mTouchedFieldIndices; | ||
|
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. We should not need this. Simply store everything whenever an attribute is changed |
||
| }; | ||
| #endif // ATTRIBUTECONTROLLER_H | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| /*************************************************************************** | ||
| * * | ||
| * 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 FEATUREDRAFT_H | ||
| #define FEATUREDRAFT_H | ||
|
|
||
| #include <QString> | ||
| #include <QDateTime> | ||
| #include <QVariant> | ||
| #include <QVector> | ||
|
|
||
| #include "qgsgeometry.h" | ||
| #include "qgsfeature.h" | ||
|
|
||
| // One touched attribute captured for a draft - the type name lets isDraftValid() | ||
| // detect a field that has since changed shape. | ||
| struct FeatureDraftAttribute | ||
| { | ||
| QString name; | ||
| QString typeName; | ||
| QVariant value; | ||
| }; | ||
|
|
||
| // In-memory shape of an in-progress feature edit. FeatureDraftStorage is the only | ||
| // class that knows how this maps to the on-disk (QSettings/JSON) format. | ||
| struct FeatureDraft | ||
| { | ||
| enum Stage | ||
| { | ||
| GeometryCapture, | ||
| AttributeForm | ||
| }; | ||
|
|
||
| bool isEmpty() const { return layerId.isEmpty(); } | ||
|
|
||
| // Whether this draft belongs to an existing feature being edited, rather than a new one being added | ||
| bool isExistingFeature() const { return !FID_IS_NULL( featureId ) && !FID_IS_NEW( featureId ); } | ||
|
|
||
| QString layerId; | ||
| Stage stage = AttributeForm; | ||
| QDateTime timestamp; | ||
| QgsGeometry geometry; | ||
| QVector<FeatureDraftAttribute> attributes; | ||
| QgsFeatureId featureId = FID_NULL; // FID_NULL means a new (not-yet-existing) feature | ||
| }; | ||
|
|
||
| #endif // FEATUREDRAFT_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 not? Can't we simply store draft immediately? What is the problem with it?