From 8b6000bc1f5cb36994473042b0af99fbfd600a8d Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 29 Dec 2025 21:15:32 +0100 Subject: [PATCH 1/7] Fix autosync for related features --- app/autosynccontroller.cpp | 26 +++++++------------------- app/autosynccontroller.h | 3 +++ app/qml/form/MMFormController.qml | 2 +- app/qml/form/MMFormPage.qml | 16 +++++++++++----- app/qml/form/MMFormStackController.qml | 2 +- 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/app/autosynccontroller.cpp b/app/autosynccontroller.cpp index 6416d39e2..9e44a844d 100644 --- a/app/autosynccontroller.cpp +++ b/app/autosynccontroller.cpp @@ -33,25 +33,7 @@ AutosyncController::AutosyncController( return; } - // Register for data change of project's vector layers - const QMap layers = mQgsProject->mapLayers( true ); - for ( const QgsMapLayer *layer : layers ) - { - const QgsVectorLayer *vecLayer = qobject_cast( layer ); - if ( vecLayer ) - { - if ( !vecLayer->readOnly() ) - { - connect( vecLayer, &QgsVectorLayer::afterCommitChanges, this, [&] - { - mLastUpdateTime = QDateTime::currentDateTime(); - emit projectSyncRequested( SyncOptions::RequestOrigin::AutomaticRequest ); - } ); - } - } - } - - //every 10 seconds check if last sync was a 60 seconds or more ago and sync if it's true + //every 10 seconds check if last sync was 60 seconds or more ago and sync if it's true mTimer = std::make_unique( this ); connect( mTimer.get(), &QTimer::timeout, this, [&] { @@ -69,6 +51,12 @@ void AutosyncController::updateLastUpdateTime() mLastUpdateTime = QDateTime::currentDateTime(); } +void AutosyncController::syncLayerChange() +{ + mLastUpdateTime = QDateTime::currentDateTime(); + emit projectSyncRequested( SyncOptions::RequestOrigin::AutomaticRequest ); +} + void AutosyncController::checkSyncRequiredAfterAppStateChange( const Qt::ApplicationState state ) { if ( state != Qt::ApplicationState::ApplicationActive ) diff --git a/app/autosynccontroller.h b/app/autosynccontroller.h index fc1f3f32a..6b341b70f 100644 --- a/app/autosynccontroller.h +++ b/app/autosynccontroller.h @@ -29,6 +29,9 @@ class AutosyncController : public QObject // Set mLastUpdateTime to "now", triggered by manual sync void updateLastUpdateTime(); + // This triggers sync after a change has been saved to layer via attributeController + Q_INVOKABLE void syncLayerChange(); + signals: void projectSyncRequested( SyncOptions::RequestOrigin origin ); diff --git a/app/qml/form/MMFormController.qml b/app/qml/form/MMFormController.qml index b736770cb..a7cdfd730 100644 --- a/app/qml/form/MMFormController.qml +++ b/app/qml/form/MMFormController.qml @@ -31,7 +31,7 @@ Item { property var relationToApply property var controllerToApply - property alias formState: featureForm.state // add, edit, readOnly or multiEdit + property alias formState: featureForm.state // add, addChild, edit, readOnly or multiEdit property alias panelState: statesManager.state property bool layerIsReadOnly: featureLayerPair?.layer?.readOnly ?? false diff --git a/app/qml/form/MMFormPage.qml b/app/qml/form/MMFormPage.qml index 0d804e61b..e14b883e1 100644 --- a/app/qml/form/MMFormPage.qml +++ b/app/qml/form/MMFormPage.qml @@ -71,6 +71,9 @@ Page { }, State { name: "add" + }, + State { + name: "addChild" } ] @@ -106,7 +109,7 @@ Page { title: { - if ( root.state === "add" ) return qsTr( "New feature" ) + if ( root.state === "add" || root.state === "addChild" ) return qsTr( "New feature" ) else if ( root.state === "edit" ) return qsTr( "Edit feature" ) else if ( root.state === "multiEdit" ) return qsTr( "Edit selected features" ) return __inputUtils.featureTitle( root.controller.featureLayerPair, __activeProject.qgsProject ) @@ -116,7 +119,7 @@ Page { anchors.verticalCenter: parent.verticalCenter - visible: root.state === "add" || root.state === "edit" || root.state === "multiEdit" + visible: root.state === "add" || root.state === "edit" || root.state === "multiEdit" || root.state === "addChild" iconSource: __style.checkmarkIcon iconColor: controller.hasValidationErrors ? __style.grapeColor : __style.forestColor @@ -316,7 +319,7 @@ Page { property var fieldFeatureLayerPair: root.controller.featureLayerPair property string fieldHomePath: root.project ? root.project.homePath : "" // for photo editor - property bool fieldRememberValueSupported: root.controller.rememberAttributesController.rememberValuesAllowed && root.state === "add" && model.EditorWidget !== "Hidden" && Type === MM.FormItem.Field + property bool fieldRememberValueSupported: root.controller.rememberAttributesController.rememberValuesAllowed && ( root.state === "add" || root.state === "addChild" ) && model.EditorWidget !== "Hidden" && Type === MM.FormItem.Field property bool fieldRememberValueState: model.RememberValue ? true : false active: fieldWidget !== 'Hidden' @@ -433,6 +436,9 @@ Page { function onChangesCommited() { root.saved() + if ( root.state !== "addChild" ) { + __activeProject.autosyncController?.syncLayerChange() + } } function onCommitFailed() { @@ -458,10 +464,10 @@ Page { } function rollbackAndClose() { - // remove feature if we are in "add" mode and it already has valid ID + // remove feature if we are in "add" or "addChild" mode and it already has valid ID // it was saved to prefill relation reference field in child layer let featureId = root.controller.featureLayerPair.feature.id - let shouldRemoveFeature = root.state === "add" && __inputUtils.isFeatureIdValid( featureId ) + let shouldRemoveFeature = ( root.state === "add" || root.state === "addChild" ) && __inputUtils.isFeatureIdValid( featureId ) if ( shouldRemoveFeature ) { root.controller.deleteFeature() diff --git a/app/qml/form/MMFormStackController.qml b/app/qml/form/MMFormStackController.qml index 79ddd8f84..2c50c9336 100644 --- a/app/qml/form/MMFormStackController.qml +++ b/app/qml/form/MMFormStackController.qml @@ -167,7 +167,7 @@ Item { function addLinkedFeature( newPair, parentController, relation ) { let props = { featureLayerPair: newPair, - formState: "add", + formState: "addChild", panelState: "form", parentController: parentController, linkedRelation: relation From f57e709dddb9f999a5a6b0d3a8d672e563f85fe8 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 31 Dec 2025 13:09:15 +0100 Subject: [PATCH 2/7] Fix autosync test --- app/test/testmerginapi.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index b066163d5..f38cc9ba0 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -2365,25 +2365,25 @@ void TestMerginApi::testAutosync() // 2. allow autosync controller // 3. load the project // 4. make some changes in the project - // 5. make sure autosync controller triggers that data has changed + // 5. mock UI form done trigger for autosync // - QString projectname = QStringLiteral( "testAutosync" ); - QString projectdir = QDir::tempPath() + "/" + projectname; - QString projectfilename = "quickapp_project.qgs"; + QString projectName = QStringLiteral( "testAutosync" ); + QString projectDir = QDir::tempPath() + "/" + projectName; + QString projectFilename = QStringLiteral( "quickapp_project.qgs" ); - InputUtils::cpDir( TestUtils::testDataDir() + "/planes", projectdir ); + InputUtils::cpDir( TestUtils::testDataDir() + QStringLiteral( "/planes" ), projectDir ); MapThemesModel mtm; AppSettings as; ActiveLayer al; ActiveProject activeProject( as, al, mApi->localProjectsManager() ); - mApi->localProjectsManager().addLocalProject( projectdir, projectname ); + mApi->localProjectsManager().addLocalProject( projectDir, projectName ); as.setAutosyncAllowed( true ); - QVERIFY( activeProject.load( projectdir + "/" + projectfilename ) ); + QVERIFY( activeProject.load( projectDir + QStringLiteral( "/" ) + projectFilename ) ); QVERIFY( activeProject.localProject().isValid() ); QSignalSpy syncSpy( &activeProject, &ActiveProject::syncActiveProject ); @@ -2403,6 +2403,7 @@ void TestMerginApi::testAutosync() QSignalSpy changesSpy( autosyncController, &AutosyncController::projectSyncRequested ); planes->commitChanges(); + autosyncController->syncLayerChange(); QVERIFY( changesSpy.count() ); QVERIFY( syncSpy.count() ); From 503b9fac383aa58b5244c99e79434eb8f9a8eb61 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 7 Jan 2026 12:11:11 +0100 Subject: [PATCH 3/7] Revert "Fix autosync test" This reverts commit f57e709dddb9f999a5a6b0d3a8d672e563f85fe8, with a slight change to keep improvements. --- app/test/testmerginapi.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index f38cc9ba0..db56d812a 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -2365,7 +2365,7 @@ void TestMerginApi::testAutosync() // 2. allow autosync controller // 3. load the project // 4. make some changes in the project - // 5. mock UI form done trigger for autosync + // 5. make sure autosync controller triggers that data has changed // QString projectName = QStringLiteral( "testAutosync" ); @@ -2403,7 +2403,6 @@ void TestMerginApi::testAutosync() QSignalSpy changesSpy( autosyncController, &AutosyncController::projectSyncRequested ); planes->commitChanges(); - autosyncController->syncLayerChange(); QVERIFY( changesSpy.count() ); QVERIFY( syncSpy.count() ); From 9fea201d11a3ad02d91f59dd95395d80016b0cda Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Thu, 8 Jan 2026 09:59:00 +0100 Subject: [PATCH 4/7] Change autosync logic back and pause it on form open This partly reverts commit 8b6000bc1f5cb36994473042b0af99fbfd600a8d. --- app/autosynccontroller.cpp | 24 ++++++++++++++++++++---- app/autosynccontroller.h | 10 ++++++++-- app/qml/form/MMFormController.qml | 2 +- app/qml/form/MMFormPage.qml | 23 +++++++++++------------ app/qml/form/MMFormStackController.qml | 2 +- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/app/autosynccontroller.cpp b/app/autosynccontroller.cpp index 9e44a844d..63d011d49 100644 --- a/app/autosynccontroller.cpp +++ b/app/autosynccontroller.cpp @@ -33,14 +33,27 @@ AutosyncController::AutosyncController( return; } + // Register for data change of project's vector layers + const QMap layers = mQgsProject->mapLayers( true ); + for ( const QgsMapLayer *layer : layers ) + { + const QgsVectorLayer *vecLayer = qobject_cast( layer ); + if ( vecLayer ) + { + if ( !vecLayer->readOnly() ) + { + connect( vecLayer, &QgsVectorLayer::afterCommitChanges, this, &AutosyncController::syncLayerChange ); + } + } + } + //every 10 seconds check if last sync was 60 seconds or more ago and sync if it's true mTimer = std::make_unique( this ); connect( mTimer.get(), &QTimer::timeout, this, [&] { if ( QDateTime::currentDateTime() - mLastUpdateTime >= std::chrono::milliseconds( SYNC_INTERVAL ) ) { - mLastUpdateTime = QDateTime::currentDateTime(); - emit projectSyncRequested( SyncOptions::RequestOrigin::AutomaticRequest ); + syncLayerChange(); } } ); mTimer->start( SYNC_CHECK_TIMEOUT ); @@ -53,8 +66,11 @@ void AutosyncController::updateLastUpdateTime() void AutosyncController::syncLayerChange() { - mLastUpdateTime = QDateTime::currentDateTime(); - emit projectSyncRequested( SyncOptions::RequestOrigin::AutomaticRequest ); + if ( !mIsSyncPaused ) + { + mLastUpdateTime = QDateTime::currentDateTime(); + emit projectSyncRequested( SyncOptions::RequestOrigin::AutomaticRequest ); + } } void AutosyncController::checkSyncRequiredAfterAppStateChange( const Qt::ApplicationState state ) diff --git a/app/autosynccontroller.h b/app/autosynccontroller.h index 6b341b70f..df4cce93b 100644 --- a/app/autosynccontroller.h +++ b/app/autosynccontroller.h @@ -29,20 +29,26 @@ class AutosyncController : public QObject // Set mLastUpdateTime to "now", triggered by manual sync void updateLastUpdateTime(); - // This triggers sync after a change has been saved to layer via attributeController - Q_INVOKABLE void syncLayerChange(); + Q_INVOKABLE void setIsSyncPaused( const bool isSyncPaused ) + { + mIsSyncPaused = isSyncPaused; + } + signals: void projectSyncRequested( SyncOptions::RequestOrigin origin ); public slots: void checkSyncRequiredAfterAppStateChange( Qt::ApplicationState state ); + // This triggers sync after a change has been saved to layer + void syncLayerChange(); private: QgsProject *mQgsProject = nullptr; // not owned QDateTime mLastUpdateTime; std::unique_ptr mTimer = nullptr; + bool mIsSyncPaused = false; }; #endif // AUTOSYNCCONTROLLER_H diff --git a/app/qml/form/MMFormController.qml b/app/qml/form/MMFormController.qml index a7cdfd730..b736770cb 100644 --- a/app/qml/form/MMFormController.qml +++ b/app/qml/form/MMFormController.qml @@ -31,7 +31,7 @@ Item { property var relationToApply property var controllerToApply - property alias formState: featureForm.state // add, addChild, edit, readOnly or multiEdit + property alias formState: featureForm.state // add, edit, readOnly or multiEdit property alias panelState: statesManager.state property bool layerIsReadOnly: featureLayerPair?.layer?.readOnly ?? false diff --git a/app/qml/form/MMFormPage.qml b/app/qml/form/MMFormPage.qml index e14b883e1..ac72ca0d9 100644 --- a/app/qml/form/MMFormPage.qml +++ b/app/qml/form/MMFormPage.qml @@ -71,9 +71,6 @@ Page { }, State { name: "add" - }, - State { - name: "addChild" } ] @@ -89,6 +86,10 @@ Page { } } + onVisibleChanged: { + __activeProject.autosyncController?.setIsSyncPaused(visible) + } + property bool layerIsReadOnly: true property bool layerIsSpatial: true @@ -109,7 +110,7 @@ Page { title: { - if ( root.state === "add" || root.state === "addChild" ) return qsTr( "New feature" ) + if ( root.state === "add" ) return qsTr( "New feature" ) else if ( root.state === "edit" ) return qsTr( "Edit feature" ) else if ( root.state === "multiEdit" ) return qsTr( "Edit selected features" ) return __inputUtils.featureTitle( root.controller.featureLayerPair, __activeProject.qgsProject ) @@ -119,7 +120,7 @@ Page { anchors.verticalCenter: parent.verticalCenter - visible: root.state === "add" || root.state === "edit" || root.state === "multiEdit" || root.state === "addChild" + visible: root.state === "add" || root.state === "edit" || root.state === "multiEdit" iconSource: __style.checkmarkIcon iconColor: controller.hasValidationErrors ? __style.grapeColor : __style.forestColor @@ -319,7 +320,7 @@ Page { property var fieldFeatureLayerPair: root.controller.featureLayerPair property string fieldHomePath: root.project ? root.project.homePath : "" // for photo editor - property bool fieldRememberValueSupported: root.controller.rememberAttributesController.rememberValuesAllowed && ( root.state === "add" || root.state === "addChild" ) && model.EditorWidget !== "Hidden" && Type === MM.FormItem.Field + property bool fieldRememberValueSupported: root.controller.rememberAttributesController.rememberValuesAllowed && root.state === "add" && model.EditorWidget !== "Hidden" && Type === MM.FormItem.Field property bool fieldRememberValueState: model.RememberValue ? true : false active: fieldWidget !== 'Hidden' @@ -436,9 +437,6 @@ Page { function onChangesCommited() { root.saved() - if ( root.state !== "addChild" ) { - __activeProject.autosyncController?.syncLayerChange() - } } function onCommitFailed() { @@ -459,15 +457,16 @@ Page { return } + __activeProject.autosyncController?.setIsSyncPaused(false) parent.focus = true controller.save() } function rollbackAndClose() { - // remove feature if we are in "add" or "addChild" mode and it already has valid ID + // remove feature if we are in "add" mode and it already has valid ID // it was saved to prefill relation reference field in child layer let featureId = root.controller.featureLayerPair.feature.id - let shouldRemoveFeature = ( root.state === "add" || root.state === "addChild" ) && __inputUtils.isFeatureIdValid( featureId ) + let shouldRemoveFeature = root.state === "add" && __inputUtils.isFeatureIdValid( featureId ) if ( shouldRemoveFeature ) { root.controller.deleteFeature() @@ -477,7 +476,7 @@ Page { // rollback all changes if the layer is still editable root.controller.rollback() - + __activeProject.autosyncController?.setIsSyncPaused(false) root.canceled() } diff --git a/app/qml/form/MMFormStackController.qml b/app/qml/form/MMFormStackController.qml index 2c50c9336..79ddd8f84 100644 --- a/app/qml/form/MMFormStackController.qml +++ b/app/qml/form/MMFormStackController.qml @@ -167,7 +167,7 @@ Item { function addLinkedFeature( newPair, parentController, relation ) { let props = { featureLayerPair: newPair, - formState: "addChild", + formState: "add", panelState: "form", parentController: parentController, linkedRelation: relation From 9caf6255937b2a91d06a77633efcbc8af00d51c2 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Thu, 8 Jan 2026 13:32:13 +0100 Subject: [PATCH 5/7] Fix autosync to work as intended --- app/qml/form/MMFormController.qml | 1 + app/qml/form/MMFormPage.qml | 6 ------ app/qml/form/MMFormStackController.qml | 6 ++++++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/qml/form/MMFormController.qml b/app/qml/form/MMFormController.qml index b736770cb..e6f267011 100644 --- a/app/qml/form/MMFormController.qml +++ b/app/qml/form/MMFormController.qml @@ -79,6 +79,7 @@ Item { StateChangeScript { script: { featureForm.forceActiveFocus() + __activeProject.autosyncController?.setIsSyncPaused(true) } } }, diff --git a/app/qml/form/MMFormPage.qml b/app/qml/form/MMFormPage.qml index ac72ca0d9..fe13097ba 100644 --- a/app/qml/form/MMFormPage.qml +++ b/app/qml/form/MMFormPage.qml @@ -86,10 +86,6 @@ Page { } } - onVisibleChanged: { - __activeProject.autosyncController?.setIsSyncPaused(visible) - } - property bool layerIsReadOnly: true property bool layerIsSpatial: true @@ -457,7 +453,6 @@ Page { return } - __activeProject.autosyncController?.setIsSyncPaused(false) parent.focus = true controller.save() } @@ -476,7 +471,6 @@ Page { // rollback all changes if the layer is still editable root.controller.rollback() - __activeProject.autosyncController?.setIsSyncPaused(false) root.canceled() } diff --git a/app/qml/form/MMFormStackController.qml b/app/qml/form/MMFormStackController.qml index 79ddd8f84..1fa137a16 100644 --- a/app/qml/form/MMFormStackController.qml +++ b/app/qml/form/MMFormStackController.qml @@ -272,6 +272,12 @@ Item { focus: true anchors.fill: parent + + onDepthChanged: { + if (depth === 0) { + __activeProject.autosyncController?.setIsSyncPaused(false) + } + } } Component { From 007f19ae7e9b01c2147362ba268187c4c2f1a207 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 12 Jan 2026 11:07:27 +0100 Subject: [PATCH 6/7] Trigger autosync on forms closed after changes were commited --- app/autosynccontroller.h | 2 +- app/qml/form/MMFormController.qml | 6 +++++- app/qml/form/MMFormStackController.qml | 10 ++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/autosynccontroller.h b/app/autosynccontroller.h index df4cce93b..6d7e8c9c2 100644 --- a/app/autosynccontroller.h +++ b/app/autosynccontroller.h @@ -41,7 +41,7 @@ class AutosyncController : public QObject public slots: void checkSyncRequiredAfterAppStateChange( Qt::ApplicationState state ); // This triggers sync after a change has been saved to layer - void syncLayerChange(); + Q_INVOKABLE void syncLayerChange(); private: diff --git a/app/qml/form/MMFormController.qml b/app/qml/form/MMFormController.qml index e6f267011..c0abccd6d 100644 --- a/app/qml/form/MMFormController.qml +++ b/app/qml/form/MMFormController.qml @@ -40,6 +40,7 @@ Item { property real drawerHeight: drawer.height signal closed() + signal saveRequested() signal editGeometry( var pair ) signal openLinkedFeature( var linkedFeature ) signal createLinkedFeature( var targetLayer, var parentPair ) @@ -199,7 +200,10 @@ Item { layerIsReadOnly: root.layerIsReadOnly layerIsSpatial: root.layerIsSpatial - onSaved: drawer.close() + onSaved: { + root.saveRequested() + drawer.close() + } onCanceled: drawer.close() onEditGeometryRequested: function( pair ) { diff --git a/app/qml/form/MMFormStackController.qml b/app/qml/form/MMFormStackController.qml index 1fa137a16..bf79856c7 100644 --- a/app/qml/form/MMFormStackController.qml +++ b/app/qml/form/MMFormStackController.qml @@ -255,6 +255,8 @@ Item { StackView { id: formsStack + property bool saveRequested: false + function popOneOrClose() { if ( formsStack.depth > 1 ) { formsStack.pop() @@ -276,6 +278,10 @@ Item { onDepthChanged: { if (depth === 0) { __activeProject.autosyncController?.setIsSyncPaused(false) + if (saveRequested) { + __activeProject.autosyncController?.syncLayerChange() + saveRequested = false + } } } } @@ -294,6 +300,10 @@ Item { } } + onSaveRequested: { + formsStack.saveRequested = true + } + onPreviewPanelChanged: function( panelHeight ) { root.previewPanelChanged( panelHeight ) } From 3c7ac66ff0a2cab0af1caf44ab1eae81c0fa57db Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Fri, 16 Jan 2026 15:11:26 +0100 Subject: [PATCH 7/7] Add explanation & change property name --- app/qml/form/MMFormStackController.qml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/qml/form/MMFormStackController.qml b/app/qml/form/MMFormStackController.qml index bf79856c7..9d8c4d1f3 100644 --- a/app/qml/form/MMFormStackController.qml +++ b/app/qml/form/MMFormStackController.qml @@ -255,7 +255,7 @@ Item { StackView { id: formsStack - property bool saveRequested: false + property bool syncWhenFormCloses: false function popOneOrClose() { if ( formsStack.depth > 1 ) { @@ -275,12 +275,14 @@ Item { anchors.fill: parent + // While the feature forms are open we block autosync triggers, so child features don't get synced before the parent + // is saved. After the last (parent) form is closed we enable autosync again and trigger it. onDepthChanged: { if (depth === 0) { __activeProject.autosyncController?.setIsSyncPaused(false) - if (saveRequested) { + if (syncWhenFormCloses) { __activeProject.autosyncController?.syncLayerChange() - saveRequested = false + syncWhenFormCloses = false } } } @@ -301,7 +303,7 @@ Item { } onSaveRequested: { - formsStack.saveRequested = true + formsStack.syncWhenFormCloses = true } onPreviewPanelChanged: function( panelHeight ) {