diff --git a/images/images.qrc b/images/images.qrc
index f3bedccef5..d44bab7425 100644
--- a/images/images.qrc
+++ b/images/images.qrc
@@ -531,6 +531,8 @@
themes/qfield/nodpi/ic_hide_green_48dp.svg
themes/qfield/nodpi/ic_chevron_down.svg
themes/qfield/nodpi/ic_chevron_up.svg
+ themes/qfield/nodpi/ic_undo.svg
+ themes/qfield/nodpi/ic_redo.svg
themes/qfield/nodpi/ic_arrow_left_white_24dp.svg
themes/qfield/nodpi/ic_opacity_black_24dp.svg
themes/qfield/nodpi/ic_common_angle_white_24dp.svg
diff --git a/images/themes/qfield/nodpi/ic_redo.svg b/images/themes/qfield/nodpi/ic_redo.svg
new file mode 100644
index 0000000000..b3602b3b50
--- /dev/null
+++ b/images/themes/qfield/nodpi/ic_redo.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/images/themes/qfield/nodpi/ic_undo.svg b/images/themes/qfield/nodpi/ic_undo.svg
new file mode 100644
index 0000000000..dffb70e59f
--- /dev/null
+++ b/images/themes/qfield/nodpi/ic_undo.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 95c7b490a7..41d73a0322 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -57,6 +57,7 @@ set(QFIELD_CORE_SRCS
geometryeditorsmodel.cpp
identifytool.cpp
layerobserver.cpp
+ featurehistory.cpp
layerresolver.cpp
layertreemapcanvasbridge.cpp
layertreemodel.cpp
@@ -164,6 +165,7 @@ set(QFIELD_CORE_HDRS
geometryeditorsmodel.h
identifytool.h
layerobserver.h
+ featurehistory.h
layerresolver.h
layertreemapcanvasbridge.h
layertreemodel.h
diff --git a/src/core/featurehistory.cpp b/src/core/featurehistory.cpp
new file mode 100644
index 0000000000..610df5caa8
--- /dev/null
+++ b/src/core/featurehistory.cpp
@@ -0,0 +1,448 @@
+#include "featurehistory.h"
+
+#include
+#include
+#include
+
+
+FeatureHistory::FeatureHistory( const QgsProject *project )
+ : mProject( project )
+{
+ connect( mProject, &QgsProject::homePathChanged, this, &FeatureHistory::onHomePathChanged );
+ connect( mProject, &QgsProject::layersAdded, this, &FeatureHistory::onLayersAdded );
+ connect( &mTimer, &QTimer::timeout, this, &FeatureHistory::onTimerTimeout );
+}
+
+
+void FeatureHistory::onHomePathChanged()
+{
+ if ( mProject->homePath().isNull() )
+ return;
+
+ mObservedLayerIds.clear();
+
+ addLayerListeners();
+}
+
+void FeatureHistory::addLayerListeners()
+{
+ const QList layers = mProject->mapLayers().values();
+
+ for ( QgsMapLayer *layer : layers )
+ {
+ QgsVectorLayer *vl = qobject_cast( layer );
+
+ if ( !vl )
+ {
+ continue;
+ }
+
+ if ( mObservedLayerIds.contains( vl->id() ) )
+ {
+ continue;
+ }
+
+ // make sure we disconnect vector layer signal slots for these events if they are already present
+ disconnect( vl, &QgsVectorLayer::beforeCommitChanges, this, &FeatureHistory::onBeforeCommitChanges );
+ disconnect( vl, &QgsVectorLayer::afterCommitChanges, this, &FeatureHistory::onAfterCommitChanges );
+ disconnect( vl, &QgsVectorLayer::committedFeaturesAdded, this, &FeatureHistory::onCommittedFeaturesAdded );
+ disconnect( vl, &QgsVectorLayer::committedFeaturesRemoved, this, &FeatureHistory::onCommittedFeaturesRemoved );
+
+ connect( vl, &QgsVectorLayer::beforeCommitChanges, this, &FeatureHistory::onBeforeCommitChanges );
+ connect( vl, &QgsVectorLayer::afterCommitChanges, this, &FeatureHistory::onAfterCommitChanges );
+ connect( vl, &QgsVectorLayer::committedFeaturesAdded, this, &FeatureHistory::onCommittedFeaturesAdded );
+ connect( vl, &QgsVectorLayer::committedFeaturesRemoved, this, &FeatureHistory::onCommittedFeaturesRemoved );
+
+ mObservedLayerIds.insert( vl->id() );
+ }
+}
+
+
+void FeatureHistory::onBeforeCommitChanges()
+{
+ if ( mIsApplyingModifications )
+ {
+ return;
+ }
+
+ QgsVectorLayer *vl = qobject_cast( sender() );
+
+ if ( !vl )
+ {
+ return;
+ }
+
+ QgsVectorLayerEditBuffer *eb = vl->editBuffer();
+
+ if ( !eb )
+ {
+ return;
+ }
+
+ const QgsFeatureIds deletedFids = eb->deletedFeatureIds();
+ const QgsFeatureIds changedGeometriesFids = qgis::listToSet( eb->changedGeometries().keys() );
+ const QgsFeatureIds changedAttributesFids = qgis::listToSet( eb->changedAttributeValues().keys() );
+ // NOTE QgsFeatureIds underlying implementation is QSet, so no need to check if the QgsFeatureId already exists
+ QgsFeatureIds changedFids;
+
+ for ( const QgsFeatureId fid : deletedFids )
+ changedFids.insert( fid );
+
+ for ( const QgsFeatureId fid : changedGeometriesFids )
+ changedFids.insert( fid );
+
+ for ( const QgsFeatureId fid : changedAttributesFids )
+ changedFids.insert( fid );
+
+ // NOTE we read the features from the dataProvider directly as we want to access the old values.
+ // If we use the layer, we get the values from the edit buffer.
+ QgsFeatureIterator featuresIt = vl->dataProvider()->getFeatures( QgsFeatureRequest( changedFids ) );
+ QMap modifiedFeatures;
+ QgsFeature f;
+
+ // ? is it possible to use the iterator in a less ugly way? something like normal `for ( QgsFeature &f : it ) {}`
+ while ( featuresIt.nextFeature( f ) )
+ modifiedFeatures.insert( f.id(), f );
+
+ qInfo() << "FeatureHistory::onBeforeCommitChanges: vl->id()=" << vl->id() << "sourcePkAttrPair=" << changedFids;
+
+ // NOTE no need to keep track of added features, as they are always present in the layer after commit
+ mTempModifiedFeaturesByLayerId.insert( vl->id(), modifiedFeatures );
+}
+
+
+void FeatureHistory::onCommittedFeaturesAdded( const QString &localLayerId, const QgsFeatureList &addedFeatures )
+{
+ if ( mIsApplyingModifications )
+ {
+ return;
+ }
+
+ const QgsVectorLayer *vl = qobject_cast( sender() );
+
+ if ( !vl )
+ {
+ return;
+ }
+
+ qInfo() << "FeatureHistory::onCommittedFeaturesAdded: adding create committed features";
+
+ FeatureModifications modifications = mTempHistoryStep.take( vl->id() );
+
+ for ( const QgsFeature &f : addedFeatures )
+ {
+ modifications.createdFeatures.append( OldNewFeaturePair( QgsFeature(), f ) );
+ }
+
+ mTempHistoryStep.insert( vl->id(), modifications );
+}
+
+void FeatureHistory::onCommittedFeaturesRemoved( const QString &layerId, const QgsFeatureIds &deletedFeatureIds )
+{
+ if ( mIsApplyingModifications )
+ {
+ return;
+ }
+
+ mTempDeletedFeatureIdsByLayerId.insert( layerId, deletedFeatureIds );
+}
+
+void FeatureHistory::onAfterCommitChanges()
+{
+ if ( mIsApplyingModifications )
+ {
+ return;
+ }
+
+ QgsVectorLayer *vl = qobject_cast( sender() );
+
+ if ( !vl )
+ {
+ return;
+ }
+
+ const QString layerId = vl->id();
+ FeatureModifications modifications = mTempHistoryStep.take( layerId );
+ QMap modifiedFeaturesOld = mTempModifiedFeaturesByLayerId.take( layerId );
+ const QgsFeatureIds deletedFids = mTempDeletedFeatureIdsByLayerId.take( layerId );
+
+ for ( const QgsFeatureId &deletedFid : deletedFids )
+ {
+ OldNewFeaturePair oldNewFeaturePair( modifiedFeaturesOld.take( deletedFid ), QgsFeature() );
+ modifications.deletedFeatures.append( oldNewFeaturePair );
+ }
+
+ const QgsFeatureIds modifiedFids = qgis::listToSet( modifiedFeaturesOld.keys() );
+ QgsFeatureIterator featuresIt = vl->getFeatures( QgsFeatureRequest( modifiedFids ) );
+ QgsFeature f;
+
+ while ( featuresIt.nextFeature( f ) )
+ {
+ OldNewFeaturePair oldNewFeaturePair( modifiedFeaturesOld.take( f.id() ), f );
+ modifications.updatedFeatures.append( oldNewFeaturePair );
+ }
+
+ if ( !modifications.createdFeatures.isEmpty() || !modifications.updatedFeatures.isEmpty() || !modifications.deletedFeatures.isEmpty() )
+ {
+ mTempHistoryStep.insert( vl->id(), modifications );
+ mTimer.start( sTimeoutMs );
+ }
+}
+
+void FeatureHistory::onLayersAdded( const QList &layers )
+{
+ Q_UNUSED( layers );
+ addLayerListeners();
+}
+
+
+void FeatureHistory::onTimerTimeout()
+{
+ mTimer.stop();
+ mUndoHistory.append( mTempHistoryStep );
+ mTempHistoryStep.clear();
+ mRedoHistory.clear();
+
+ emit isUndoEnabledChanged();
+ emit isRedoEnabledChanged();
+}
+
+QMap FeatureHistory::reverseModifications( QMap &modificationsByLayerId )
+{
+ QMap reversedModificationsByLayerId;
+
+ const QStringList layerIds = modificationsByLayerId.keys();
+ for ( const QString &layerId : layerIds )
+ {
+ QgsVectorLayer *vl = qobject_cast( mProject->mapLayer( layerId ) );
+
+ if ( !vl )
+ {
+ continue;
+ }
+
+ QString displayString;
+ FeatureModifications modifications = modificationsByLayerId.value( layerId );
+ FeatureModifications reversedModifications;
+
+ for ( const OldNewFeaturePair &pair : modifications.deletedFeatures )
+ {
+ reversedModifications.createdFeatures.append( OldNewFeaturePair( pair.second, pair.first ) );
+ }
+
+ for ( const OldNewFeaturePair &pair : modifications.createdFeatures )
+ {
+ reversedModifications.deletedFeatures.append( OldNewFeaturePair( pair.second, pair.first ) );
+ }
+
+ for ( const OldNewFeaturePair &pair : modifications.updatedFeatures )
+ {
+ reversedModifications.updatedFeatures.append( OldNewFeaturePair( pair.second, pair.first ) );
+ }
+
+ reversedModificationsByLayerId.insert( layerId, reversedModifications );
+ }
+
+ return reversedModificationsByLayerId;
+}
+
+
+bool FeatureHistory::applyModifications( QMap &modificationsByLayerId )
+{
+ mIsApplyingModifications = true;
+
+ const QStringList layerIds = modificationsByLayerId.keys();
+
+ for ( const QString &layerId : layerIds )
+ {
+ QgsVectorLayer *vl = qobject_cast( mProject->mapLayer( layerId ) );
+
+ if ( !vl )
+ {
+ continue;
+ }
+
+ FeatureModifications undoFeatureModifications = modificationsByLayerId.value( layerId );
+
+ if ( !vl->startEditing() )
+ {
+ return false;
+ }
+
+ // created features
+ QgsFeatureIds fidsToDelete;
+ for ( const OldNewFeaturePair &pair : undoFeatureModifications.createdFeatures )
+ {
+ fidsToDelete << pair.second.id();
+ }
+
+ if ( !undoFeatureModifications.createdFeatures.isEmpty() && !vl->deleteFeatures( fidsToDelete ) )
+ {
+ QgsMessageLog::logMessage( tr( "Failed to undo created features in layer \"%1\"" ).arg( vl->name() ) );
+ return false;
+ }
+
+ // deleted features
+ QgsFeatureList featuresToAdd;
+ for ( const OldNewFeaturePair &pair : undoFeatureModifications.deletedFeatures )
+ {
+ featuresToAdd.append( pair.first );
+ }
+
+ if ( !undoFeatureModifications.deletedFeatures.isEmpty() && !vl->addFeatures( featuresToAdd ) )
+ {
+ QgsMessageLog::logMessage( tr( "Failed to undo deleted features in layer \"%1\"" ).arg( vl->name() ) );
+ return false;
+ }
+
+ // update features
+ QgsFeatureIds featureIds;
+ for ( const OldNewFeaturePair &pair : undoFeatureModifications.updatedFeatures )
+ {
+ featureIds.insert( pair.first.id() );
+ }
+
+ for ( OldNewFeaturePair &pair : undoFeatureModifications.updatedFeatures )
+ {
+ if ( !vl->updateFeature( pair.first, true ) )
+ {
+ QgsMessageLog::logMessage( tr( "Failed to undo update features in layer \"%1\"" ).arg( vl->name() ) );
+ return false;
+ }
+ }
+
+ if ( !vl->commitChanges() )
+ {
+ QgsMessageLog::logMessage( tr( "Failed to commit undo feature modification in layer \"%1\"" ).arg( vl->name() ) );
+
+ if ( !vl->rollBack() )
+ {
+ QgsMessageLog::logMessage( tr( "Failed to rollback undo featurue modifications in layer \"%1\"" ).arg( vl->name() ) );
+ }
+
+ // OGR error creating feature -4: failed to execute insert : UNIQUE constraint failed: area.fid
+
+ return false;
+ }
+ }
+
+ mIsApplyingModifications = false;
+
+ return true;
+}
+
+bool FeatureHistory::undo()
+{
+ if ( mUndoHistory.isEmpty() )
+ {
+ return false;
+ }
+
+ QMap modifications = mUndoHistory.takeLast();
+ QMap reversedModifications = reverseModifications( modifications );
+
+ if ( !applyModifications( modifications ) )
+ {
+ return false;
+ }
+
+ mRedoHistory.append( reversedModifications );
+
+ emit isUndoEnabledChanged();
+ emit isRedoEnabledChanged();
+
+ return true;
+}
+
+
+bool FeatureHistory::redo()
+{
+ if ( mRedoHistory.isEmpty() )
+ {
+ return false;
+ }
+
+ QMap modifications = mRedoHistory.takeLast();
+ QMap reversedModifications = reverseModifications( modifications );
+
+ if ( !applyModifications( modifications ) )
+ {
+ return false;
+ }
+
+ mUndoHistory.append( reversedModifications );
+
+ emit isUndoEnabledChanged();
+ emit isRedoEnabledChanged();
+
+ return true;
+}
+
+
+bool FeatureHistory::isUndoEnabled()
+{
+ return !mUndoHistory.isEmpty();
+}
+
+
+bool FeatureHistory::isRedoEnabled()
+{
+ return !mRedoHistory.isEmpty();
+}
+
+const QString FeatureHistory::undoMessage()
+{
+ if ( mUndoHistory.isEmpty() )
+ {
+ return QString();
+ }
+
+ int totalChanges = 0;
+ QMap modifiedFeaturesByLayerId = mUndoHistory.last();
+
+ for ( const FeatureModifications &modifiedFeatures : modifiedFeaturesByLayerId.values() )
+ {
+ totalChanges += modifiedFeatures.createdFeatures.count()
+ + modifiedFeatures.updatedFeatures.count()
+ + modifiedFeatures.deletedFeatures.count();
+ }
+
+ if ( totalChanges == 0 )
+ {
+ return QString();
+ }
+ else
+ {
+ // TODO show the display string of the first feature and say something like "Changed and N more"
+ return QStringLiteral( "Undo modifications on %1 feature(s)." ).arg( totalChanges );
+ }
+}
+
+
+const QString FeatureHistory::redoMessage()
+{
+ if ( mRedoHistory.isEmpty() )
+ {
+ return QString();
+ }
+
+ int totalChanges = 0;
+ QMap modifiedFeaturesByLayerId = mRedoHistory.last();
+
+ for ( const FeatureModifications &modifiedFeatures : modifiedFeaturesByLayerId.values() )
+ {
+ totalChanges += modifiedFeatures.createdFeatures.count()
+ + modifiedFeatures.updatedFeatures.count()
+ + modifiedFeatures.deletedFeatures.count();
+ }
+
+ if ( totalChanges == 0 )
+ {
+ return QString();
+ }
+ else
+ {
+ // TODO show the display string of the first feature and say something like "Changed and N more"
+ return QStringLiteral( "Redo modifications on %1 feature(s)." ).arg( totalChanges );
+ }
+}
diff --git a/src/core/featurehistory.h b/src/core/featurehistory.h
new file mode 100644
index 0000000000..2ec28eefb3
--- /dev/null
+++ b/src/core/featurehistory.h
@@ -0,0 +1,124 @@
+#ifndef FEATUREHISTORY_H
+#define FEATUREHISTORY_H
+
+#include
+#include
+#include
+
+
+typedef QPair OldNewFeaturePair;
+
+class FeatureHistory : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY( bool isUndoEnabled READ isUndoEnabled NOTIFY isUndoEnabledChanged )
+ Q_PROPERTY( bool isRedoEnabled READ isRedoEnabled NOTIFY isRedoEnabledChanged )
+
+ public:
+ /**
+ * Stores the created, updated and deleted features on each undo/redo step.
+ */
+ struct FeatureModifications
+ {
+ FeatureModifications()
+ {}
+
+ QList createdFeatures;
+ QList updatedFeatures;
+ QList deletedFeatures;
+ };
+
+ /**
+ * Construct a new Feature history object
+ *
+ * @param project
+ */
+ explicit FeatureHistory( const QgsProject *project );
+
+ //! Perform undo of the most recent modification step
+ Q_INVOKABLE bool undo();
+
+ //! Perform redo of the most recent modification step
+ Q_INVOKABLE bool redo();
+
+ //! Get the undo message to be show in the UI. NOTE should be called before calling \a undo.
+ Q_INVOKABLE const QString undoMessage();
+
+ //! Get the redo message to be show in the UI. NOTE should be called before calling \a redo.
+ Q_INVOKABLE const QString redoMessage();
+
+ bool isUndoEnabled();
+ bool isRedoEnabled();
+
+ private:
+ static const int sTimeoutMs = 50;
+
+ //! Add the needed event listeners to monitor for changes.
+ void addLayerListeners();
+
+ //! Apply given modifications on all layers in the current project. Used both by undo and redo operations.
+ bool applyModifications( QMap &modificationsByLayerId );
+
+ //! Reverse the modification. Used to make undo modifications into redo modifications.
+ QMap reverseModifications( QMap &modificationsByLayerId );
+
+ //! The current project instance.
+ const QgsProject *mProject = nullptr;
+
+ //! If currently applying undo or redo feature modifications.
+ bool mIsApplyingModifications = false;
+
+ //! Timer to wait for short time before all features (and child features) are saved. When timeouts, all temporary modifications are added as a new undo step.
+ QTimer mTimer;
+
+ //! Temporary storage of all modifications before creating a new undo step.
+ QMap mTempHistoryStep;
+
+ //! Temporary storage of All features that have been modified before creating a new undo step.
+ QMap> mTempModifiedFeaturesByLayerId;
+
+ //! Temporary storage of the deleted feature ids before creating a new undo step.
+ QMap mTempDeletedFeatureIdsByLayerId;
+
+ //! Undo history records
+ QList> mUndoHistory;
+
+ //! Redo history records
+ QList> mRedoHistory;
+
+ //! Layer ids being observed for changes. Should reset when the project is changed. Used to prevent double event listeners.
+ QSet mObservedLayerIds;
+
+ signals:
+ void isUndoEnabledChanged();
+ void isRedoEnabledChanged();
+
+ private slots:
+ /**
+ * Monitors the current project for new layers.
+ *
+ * @param layers layers added
+ */
+ void onLayersAdded( const QList &layers );
+
+ //! The project file has been changed
+ void onHomePathChanged();
+
+ //! Called when features are added on the layer
+ void onCommittedFeaturesAdded( const QString &localLayerId, const QgsFeatureList &addedFeatures );
+
+ //! Called after features are committed for deletion.
+ void onCommittedFeaturesRemoved( const QString &layerId, const QgsFeatureIds &deletedFeatureIds );
+
+ //! Called before features are committed. Used to prepare the old state of the features and store it in \a mHistry.
+ void onBeforeCommitChanges();
+
+ //! Called after features are committed. Used because the added features do not have FID before they are committed.
+ void onAfterCommitChanges();
+
+ //! Timer's timeout slot. Used to collect multiple feature changes (calls of \a onBeforeCommitChanges and \a onAfterCommitChanges) into one undo step.
+ void onTimerTimeout();
+};
+
+#endif // FEATUREHISTORY_H
diff --git a/src/core/qgismobileapp.cpp b/src/core/qgismobileapp.cpp
index b18cf577d8..f021c66215 100644
--- a/src/core/qgismobileapp.cpp
+++ b/src/core/qgismobileapp.cpp
@@ -53,6 +53,7 @@
#include "expressionevaluator.h"
#include "expressionvariablemodel.h"
#include "featurechecklistmodel.h"
+#include "featurehistory.h"
#include "featurelistextentcontroller.h"
#include "featurelistmodel.h"
#include "featurelistmodelselection.h"
@@ -268,6 +269,7 @@ QgisMobileapp::QgisMobileapp( QgsApplication *app, QObject *parent )
mTrackingModel = new TrackingModel();
mGpkgFlusher = std::make_unique( mProject );
mLayerObserver = std::make_unique( mProject );
+ mFeatureHistory = std::make_unique( mProject );
mFlatLayerTree = new FlatLayerTreeModel( mProject->layerTreeRoot(), mProject, this );
mLegendImageProvider = new LegendImageProvider( mFlatLayerTree->layerTreeModel() );
mLocalFilesImageProvider = new LocalFilesImageProvider();
@@ -545,6 +547,7 @@ void QgisMobileapp::initDeclarative()
rootContext()->setContextProperty( "LocatorModelNoGroup", QgsLocatorModel::NoGroup );
rootContext()->setContextProperty( "gpkgFlusher", mGpkgFlusher.get() );
rootContext()->setContextProperty( "layerObserver", mLayerObserver.get() );
+ rootContext()->setContextProperty( "featureHistory", mFeatureHistory.get() );
rootContext()->setContextProperty( "messageLogModel", mMessageLogModel.get() );
rootContext()->setContextProperty( "qfieldAuthRequestHandler", mAuthRequestHandler );
diff --git a/src/core/qgismobileapp.h b/src/core/qgismobileapp.h
index e8a0530a44..e904b66022 100644
--- a/src/core/qgismobileapp.h
+++ b/src/core/qgismobileapp.h
@@ -51,6 +51,7 @@ class TrackingModel;
class LocatorFiltersModel;
class QgsProject;
class LayerObserver;
+class FeatureHistory;
class MessageLogModel;
class QgsPrintLayout;
@@ -208,6 +209,7 @@ class QFIELD_CORE_EXPORT QgisMobileapp : public QQmlApplicationEngine
std::unique_ptr mGpkgFlusher;
std::unique_ptr mLayerObserver;
+ std::unique_ptr mFeatureHistory;
QFieldAppAuthRequestHandler *mAuthRequestHandler = nullptr;
std::unique_ptr mBookmarkModel;
diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml
index b7aba02da6..1f974fdb01 100644
--- a/src/qml/qgismobileapp.qml
+++ b/src/qml/qgismobileapp.qml
@@ -1249,6 +1249,7 @@ ApplicationWindow {
spacing: 4
QfToolButtonDrawer {
+ id: mainToolbarDrawer
name: "digitizingDrawer"
size: 48
round: true
@@ -2116,6 +2117,48 @@ ApplicationWindow {
return result + padding * 2;
}
+ Row {
+ bottomPadding: parent.topMargin
+
+ QfToolButton {
+ icon.source: Theme.getThemeVectorIcon( "ic_undo" )
+ height: 48
+ width: 48
+ enabled: featureHistory.isUndoEnabled
+ opacity: enabled ? 1 : 0.5
+
+ onClicked: {
+ const msg = featureHistory.undoMessage();
+
+ if ( featureHistory.undo() ) {
+ displayToast( msg );
+ }
+
+ dashBoard.close();
+ mainMenu.close();
+ }
+ }
+
+ QfToolButton {
+ icon.source: Theme.getThemeVectorIcon( "ic_redo" )
+ height: 48
+ width: 48
+ enabled: featureHistory.isRedoEnabled
+ opacity: enabled ? 1 : 0.5
+
+ onClicked: {
+ const msg = featureHistory.redoMessage();
+
+ if ( featureHistory.redo() ) {
+ displayToast( msg );
+ }
+
+ dashBoard.close();
+ mainMenu.close();
+ }
+ }
+ }
+
MenuItem {
text: qsTr( 'Measure Tool' )