From 9ddabca4fdc114c0c86b3a92e7d48ca2055dd9f0 Mon Sep 17 00:00:00 2001 From: Richard Kello Date: Thu, 30 Jul 2026 13:35:10 +0200 Subject: [PATCH 1/5] Add rename local project option --- app/projectsmodel.cpp | 5 + app/projectsmodel.h | 3 + app/qml/CMakeLists.txt | 1 + app/qml/dialogs/MMRenameProjectDialog.qml | 96 +++++++++++++++++++ app/qml/project/MMProjectList.qml | 37 ++++++- .../project/components/MMProjectDelegate.qml | 8 +- core/localprojectsmanager.cpp | 60 ++++++++++++ core/localprojectsmanager.h | 2 + gallery/qml/pages/DrawerPage.qml | 14 +++ gallery/qml/pages/ProjectItemsPage.qml | 3 +- 10 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 app/qml/dialogs/MMRenameProjectDialog.qml diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 26a58519a..c98613b20 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -380,6 +380,11 @@ void ProjectsModel::removeLocalProject( const QString &projectId ) mLocalProjectsManager->removeLocalProject( projectId ); } +QString ProjectsModel::renameLocalProject( const QString &projectId, const QString &newName ) +{ + return mLocalProjectsManager->renameLocalProject( projectId, newName ); +} + void ProjectsModel::migrateProject( const QString &projectId ) { int ix = projectIndexFromId( projectId ); diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 900bec57e..2502fa537 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -127,6 +127,9 @@ class ProjectsModel : public QAbstractListModel //! Forwards call to LocalProjectsManager to remove local project Q_INVOKABLE void removeLocalProject( const QString &projectId ); + //! Forwards call to LocalProjectsManager to rename local project + Q_INVOKABLE QString renameLocalProject( const QString &projectId, const QString &newName ); + //! Migrates local project to mergin Q_INVOKABLE void migrateProject( const QString &projectId ); diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 83ef85a09..82ddae1c7 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -82,6 +82,7 @@ set(MM_QML dialogs/MMPositionTrackingDialog.qml dialogs/MMProjectLimitDialog.qml dialogs/MMRemoveProjectDialog.qml + dialogs/MMRenameProjectDialog.qml dialogs/MMWelcomeToNewDesignDialog.qml dialogs/MMSplittingFailedDialog.qml dialogs/MMStorageLimitDialog.qml diff --git a/app/qml/dialogs/MMRenameProjectDialog.qml b/app/qml/dialogs/MMRenameProjectDialog.qml new file mode 100644 index 000000000..47ca58ee9 --- /dev/null +++ b/app/qml/dialogs/MMRenameProjectDialog.qml @@ -0,0 +1,96 @@ +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +import QtQuick + +import "../components" +import "../inputs" + +MMDrawer { + id: root + + property string relatedProjectId: "" + property string newProjectName: "" + property string renameErrorText: "" + + signal renameClicked( string newName ) + + drawerHeader.title: qsTr( "Rename the project" ) + drawerHeader.titleFont: __style.t2 + + drawerContent: Column { + width: parent.width + spacing: __style.spacing10 + + MMTextInput { + id: newNameField + + width: parent.width + textFieldBackground.color: root.renameErrorText === "" ? __style.lightGreenColor : __style.negativeUltraLightColor + textFieldBackground.border.width: root.renameErrorText === "" ? 0 : __style.width2 + textFieldBackground.border.color: root.renameErrorText === "" ? __style.polarColor : __style.negativeColor + + placeholderText: qsTr( "Enter the new name" ) + text: root.newProjectName + + onTextEdited: root.clearRenameError() + } + + Item { + width: parent.width + height: __style.row24 + + visible: true + + Row { + width: parent.width + height: parent.height + anchors.verticalCenter: parent.verticalCenter + visible: root.renameErrorText !== "" + spacing: __style.margin4 + + MMIcon { + y: parent.height / 2 - height / 2 + source: __style.errorCircleIcon + color: __style.negativeColor + size: __style.icon16 + } + + MMText { + width: parent.width - __style.icon16 - parent.spacing + text: root.renameErrorText + color: __style.grapeColor + font: __style.t4 + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + } + + } + + MMButton { + width: parent.width + + text: qsTr( "Confirm" ) + + onClicked: { + root.renameClicked( newNameField.text ) + } + } + } + + function clearRenameError() { + root.renameErrorText = "" + } + + function showRenameError( message ) { + root.renameErrorText = message + } + +} \ No newline at end of file diff --git a/app/qml/project/MMProjectList.qml b/app/qml/project/MMProjectList.qml index 7636434b9..449589aa0 100644 --- a/app/qml/project/MMProjectList.qml +++ b/app/qml/project/MMProjectList.qml @@ -97,7 +97,7 @@ Item { width: ListView.view.width height: visible ? implicitHeight : 0 - projectDisplayName: root.projectModelType === MM.ProjectsModel.WorkspaceProjectsModel ? model.ProjectName : model.ProjectFullName + projectDisplayName: model.ProjectName projectId: model.ProjectId ? model.ProjectId : "" projectDescription: model.ProjectDescription ? model.ProjectDescription : "" projectIsInSync: model.ProjectSyncPending ? model.ProjectSyncPending : false @@ -139,7 +139,7 @@ Item { return ["changes", "remove"] } else if ( !model.ProjectIsMergin && model.ProjectIsLocal ) { - return ["upload", "remove"] + return ["upload", "remove", "rename"] } return ["download"] } @@ -176,6 +176,12 @@ Item { } onStopSyncRequested: controllerModel.stopProjectSync( projectId ) onShowChangesRequested: root.showLocalChangesRequested( projectId ) + onRenameRequested: { + renameDialog.relatedProjectId = projectId + renameDialog.newProjectName = model.ProjectName + renameDialog.clearRenameError() + renameDialog.open() + } } } @@ -306,6 +312,33 @@ Item { } } + MMRenameProjectDialog { + id: renameDialog + + onRenameClicked: function( newName ) { + if (relatedProjectId === "") { + return + } + + __inputUtils.log( + "Rename project", + "Project " + __localProjectsManager.projectName( relatedProjectId ) + " renamed to " + newName + " by " + + ( __merginApi.userInfo ? __merginApi.userInfo.username : "unknown" ) + " (" + __localProjectsManager.projectChanges( relatedProjectId ) + ")" ) + + let renameResult = controllerModel.renameLocalProject( relatedProjectId, newName ) + + if ( renameResult === "" ) { + renameDialog.relatedProjectId = "" + renameDialog.clearRenameError() + renameDialog.close() + } + else { + renameDialog.newProjectName = "" + renameDialog.showRenameError( renameResult ) + } + } + } + MMDownloadProjectDialog { id: downloadProjectDialog diff --git a/app/qml/project/components/MMProjectDelegate.qml b/app/qml/project/components/MMProjectDelegate.qml index b858b0666..c945807a7 100644 --- a/app/qml/project/components/MMProjectDelegate.qml +++ b/app/qml/project/components/MMProjectDelegate.qml @@ -25,7 +25,7 @@ Control { property bool projectIsInSync: false property real projectSyncProgress: 0.0 - property var projectActionButtons: [] // possible values: upload, changes, sync, download, remove + property var projectActionButtons: [] // possible values: upload, changes, sync, download, remove, rename property bool projectIsOpened: false @@ -35,6 +35,7 @@ Control { signal removeRequested() signal stopSyncRequested() signal showChangesRequested() + signal renameRequested() height: implicitHeight @@ -307,6 +308,11 @@ Control { "name": qsTr("Upload"), "iconSource": __style.uploadIcon, "callback": () => root.migrateRequested() + }, + "rename": { + "name": qsTr("Rename the local project"), + "iconSource": __style.editIcon, + "callback": () => root.renameRequested() } } } diff --git a/core/localprojectsmanager.cpp b/core/localprojectsmanager.cpp index f3d134fc8..4a898855c 100644 --- a/core/localprojectsmanager.cpp +++ b/core/localprojectsmanager.cpp @@ -15,6 +15,12 @@ #include #include +#include + +// Local (never synced to server) override for a project's display name, keyed by its +// stable projectDir. Stored outside of the Mergin metadata cache since that file is +// wholesale overwritten with the server's response on every sync (see MerginApi::writeData). +static const QString sLocalProjectNameSettingsGroup = QStringLiteral( "localProjectNames" ); LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) : mDataDir( dataDir ) @@ -25,6 +31,10 @@ LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) void LocalProjectsManager::reloadDataDir() { mProjects.clear(); + + QSettings settings; + settings.beginGroup( CoreUtils::QSETTINGS_APP_GROUP_NAME ); + QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); for ( const QString &folderName : entryList ) { @@ -44,9 +54,18 @@ void LocalProjectsManager::reloadDataDir() info.projectName = folderName; } + // A local rename overrides whatever name we just resolved above. + const QString customName = settings.value( sLocalProjectNameSettingsGroup + "/" + info.projectDir ).toString(); + if ( !customName.isEmpty() ) + { + info.projectName = customName; + } + mProjects << info; } + settings.endGroup(); + QString msg = QString( "Found %1 local projects in %2" ).arg( mProjects.size() ).arg( mDataDir ); CoreUtils::log( "Local projects", msg ); emit dataDirReloaded(); @@ -115,6 +134,11 @@ void LocalProjectsManager::removeLocalProject( const QString &projectId ) { emit aboutToRemoveLocalProject( mProjects[i] ); + QSettings settings; + settings.beginGroup( CoreUtils::QSETTINGS_APP_GROUP_NAME ); + settings.remove( sLocalProjectNameSettingsGroup + "/" + mProjects[i].projectDir ); + settings.endGroup(); + CoreUtils::removeDir( mProjects[i].projectDir ); mProjects.removeAt( i ); @@ -249,3 +273,39 @@ void LocalProjectsManager::addProject( const QString &projectDir, const QString mProjects << project; emit localProjectAdded( project ); } + +QString LocalProjectsManager::renameLocalProject( const QString &projectId, const QString &newName ) +{ + if ( newName.trimmed().isEmpty() ) + { + return tr( "The project name cannot be empty" ); + } + + if ( !CoreUtils::isValidName( newName ) ) + { + return tr( "The project name contains invalid characters" ); + } + + int projectIndex = -1; + for ( int i = 0; i < mProjects.count(); ++i ) + { + if ( mProjects[i].id() == projectId ) + { + projectIndex = i; + } + + if ( i != projectIndex && mProjects[i].projectName == newName ) + { + return tr( "A project name is already taken" ); + } + } + + QSettings settings; + settings.beginGroup( CoreUtils::QSETTINGS_APP_GROUP_NAME ); + settings.setValue( sLocalProjectNameSettingsGroup + "/" + mProjects[projectIndex].projectDir, newName ); + settings.endGroup(); + + mProjects[projectIndex].projectName = newName; + emit localProjectDataChanged( mProjects[projectIndex] ); + return QString(); +} diff --git a/core/localprojectsmanager.h b/core/localprojectsmanager.h index f90290861..30282ec93 100644 --- a/core/localprojectsmanager.h +++ b/core/localprojectsmanager.h @@ -53,6 +53,8 @@ class LocalProjectsManager : public QObject Q_INVOKABLE QString projectName( const QString &projectId ) const; + Q_INVOKABLE QString renameLocalProject( const QString &projectId, const QString &newName ); + /** * Returns changes of a project specified by projectId in the form : * (pending changes, features in layer survey: 10 addition, 3 updates, 1 deletion. 10 new files) diff --git a/gallery/qml/pages/DrawerPage.qml b/gallery/qml/pages/DrawerPage.qml index 47a0aab1f..ff0fa1ab4 100644 --- a/gallery/qml/pages/DrawerPage.qml +++ b/gallery/qml/pages/DrawerPage.qml @@ -48,6 +48,11 @@ Page { onClicked: removeProjectDialog.open() } + Button { + text: "MMRenameProjectDialog" + onClicked: renameProjectDialog.open() + } + Button { text: "MMDownloadProjectDialog" onClicked: downloadProjectDialog.open() @@ -251,6 +256,15 @@ Page { onRemoveClicked: console.log("on remove clicked") } + MMRenameProjectDialog { + id: renameProjectDialog + + relatedProjectId: "Cool Project" + newProjectName: "Even cooler Project" + + onRenameClicked: console.log("on rename clicked") + } + MMProjectLimitDialog { id: projectLimitDialog diff --git a/gallery/qml/pages/ProjectItemsPage.qml b/gallery/qml/pages/ProjectItemsPage.qml index 49ff70f19..473ef734d 100644 --- a/gallery/qml/pages/ProjectItemsPage.qml +++ b/gallery/qml/pages/ProjectItemsPage.qml @@ -48,7 +48,7 @@ MMPage { projectIsInSync: true projectSyncProgress: 1/4 - projectActionButtons: ["download", "sync", "remove"] + projectActionButtons: ["download", "sync", "remove", "rename"] state: "NeedsSync" @@ -61,6 +61,7 @@ MMPage { } onRemoveRequested: console.log("onRemoveRequested") onMigrateRequested: console.log("onMigrateRequested") + onRenameRequested: console.log("onRenameRequested") Timer { id: syncAnimator From 689e3bee8299d6f7644a7077e74c023c9cd01f20 Mon Sep 17 00:00:00 2001 From: Richard Kello Date: Mon, 24 Aug 2026 20:20:43 +0200 Subject: [PATCH 2/5] Update rename feature based on review --- app/projectsmodel.cpp | 27 ++++++ app/projectsmodel.h | 1 + app/qml/CMakeLists.txt | 2 +- app/qml/project/MMProjectList.qml | 25 ++---- .../components}/MMRenameProjectDialog.qml | 50 +++++------ core/localprojectsmanager.cpp | 85 ++++++++++++------- core/localprojectsmanager.h | 3 +- gallery/qml/pages/DrawerPage.qml | 14 --- 8 files changed, 112 insertions(+), 95 deletions(-) rename app/qml/{dialogs => project/components}/MMRenameProjectDialog.qml (64%) diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index c98613b20..920e33417 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -53,6 +53,7 @@ void ProjectsModel::initializeProjectsModel() QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel::onProjectAdded ); QObject::connect( mLocalProjectsManager, &LocalProjectsManager::aboutToRemoveLocalProject, this, &ProjectsModel::onAboutToRemoveProject ); QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel::onProjectDataChanged ); + QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectRenamed, this, &ProjectsModel::onProjectRenamed ); QObject::connect( mLocalProjectsManager, &LocalProjectsManager::dataDirReloaded, this, &ProjectsModel::loadLocalProjects ); emit modelInitialized(); @@ -385,6 +386,32 @@ QString ProjectsModel::renameLocalProject( const QString &projectId, const QStri return mLocalProjectsManager->renameLocalProject( projectId, newName ); } +void ProjectsModel::onProjectRenamed( const QString &oldProjectId, const LocalProject &localProject ) +{ + int ix = projectIndexFromId( oldProjectId ); + + if ( ix < 0 ) + return; + + Project &project = mProjects[ix]; + + project.local = localProject; + + if ( project.isMergin() ) + { + project.mergin.status = ProjectStatus::projectStatus( project, mBackend->supportsSelectiveSync() ); + } + + QModelIndex editIndex = index( ix ); + emit dataChanged( editIndex, editIndex ); + + if ( mActiveProjectId == oldProjectId ) + { + mActiveProjectId = localProject.id(); + emit activeProjectIdChanged( mActiveProjectId ); + } +} + void ProjectsModel::migrateProject( const QString &projectId ) { int ix = projectIndexFromId( projectId ); diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 2502fa537..691387940 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -173,6 +173,7 @@ class ProjectsModel : public QAbstractListModel void onProjectAdded( const LocalProject &project ); void onAboutToRemoveProject( const LocalProject &project ); void onProjectDataChanged( const LocalProject &project ); + void onProjectRenamed( const QString &oldProjectId, const LocalProject &project ); void onAuthChanged(); diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 82ddae1c7..607d0875c 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -82,7 +82,6 @@ set(MM_QML dialogs/MMPositionTrackingDialog.qml dialogs/MMProjectLimitDialog.qml dialogs/MMRemoveProjectDialog.qml - dialogs/MMRenameProjectDialog.qml dialogs/MMWelcomeToNewDesignDialog.qml dialogs/MMSplittingFailedDialog.qml dialogs/MMStorageLimitDialog.qml @@ -185,6 +184,7 @@ set(MM_QML project/components/MMProjectStatusItem.qml project/components/MMProjectWizardDelegate.qml project/components/MMProjectDelegate.qml + project/components/MMRenameProjectDialog.qml settings/MMAboutPage.qml settings/MMChangelogPage.qml settings/MMLogPage.qml diff --git a/app/qml/project/MMProjectList.qml b/app/qml/project/MMProjectList.qml index 449589aa0..768a35b24 100644 --- a/app/qml/project/MMProjectList.qml +++ b/app/qml/project/MMProjectList.qml @@ -26,6 +26,7 @@ Item { property string searchText: "" property int spacing: 0 property bool activeProjectAlwaysFirst: false + property string projectIdToRename: "" property alias projectsProxyModel: viewModel property alias projectsModel: controllerModel property alias listHeader: listview.header @@ -97,7 +98,7 @@ Item { width: ListView.view.width height: visible ? implicitHeight : 0 - projectDisplayName: model.ProjectName + projectDisplayName: root.projectModelType === MM.ProjectsModel.WorkspaceProjectsModel ? model.ProjectName : model.ProjectFullName projectId: model.ProjectId ? model.ProjectId : "" projectDescription: model.ProjectDescription ? model.ProjectDescription : "" projectIsInSync: model.ProjectSyncPending ? model.ProjectSyncPending : false @@ -177,9 +178,7 @@ Item { onStopSyncRequested: controllerModel.stopProjectSync( projectId ) onShowChangesRequested: root.showLocalChangesRequested( projectId ) onRenameRequested: { - renameDialog.relatedProjectId = projectId - renameDialog.newProjectName = model.ProjectName - renameDialog.clearRenameError() + root.projectIdToRename = projectId renameDialog.open() } } @@ -312,29 +311,21 @@ Item { } } - MMRenameProjectDialog { + MMProjectComponents.MMRenameProjectDialog { id: renameDialog onRenameClicked: function( newName ) { - if (relatedProjectId === "") { + if ( !root.projectIdToRename ) { return } - __inputUtils.log( - "Rename project", - "Project " + __localProjectsManager.projectName( relatedProjectId ) + " renamed to " + newName + " by " + - ( __merginApi.userInfo ? __merginApi.userInfo.username : "unknown" ) + " (" + __localProjectsManager.projectChanges( relatedProjectId ) + ")" ) - - let renameResult = controllerModel.renameLocalProject( relatedProjectId, newName ) + const renameResult = controllerModel.renameLocalProject( root.projectIdToRename, newName ) - if ( renameResult === "" ) { - renameDialog.relatedProjectId = "" - renameDialog.clearRenameError() + if ( !renameResult ) { renameDialog.close() } else { - renameDialog.newProjectName = "" - renameDialog.showRenameError( renameResult ) + renameDialog.errorText = renameResult } } } diff --git a/app/qml/dialogs/MMRenameProjectDialog.qml b/app/qml/project/components/MMRenameProjectDialog.qml similarity index 64% rename from app/qml/dialogs/MMRenameProjectDialog.qml rename to app/qml/project/components/MMRenameProjectDialog.qml index 47ca58ee9..ff37b8974 100644 --- a/app/qml/dialogs/MMRenameProjectDialog.qml +++ b/app/qml/project/components/MMRenameProjectDialog.qml @@ -9,52 +9,54 @@ import QtQuick -import "../components" -import "../inputs" +import "../../components" +import "../../inputs" MMDrawer { id: root - property string relatedProjectId: "" - property string newProjectName: "" - property string renameErrorText: "" + property string errorText: "" signal renameClicked( string newName ) - drawerHeader.title: qsTr( "Rename the project" ) + drawerHeader.title: qsTr( "Rename project" ) drawerHeader.titleFont: __style.t2 + onOpened: { + root.errorText = "" + newNameField.text = "" + } + drawerContent: Column { width: parent.width - spacing: __style.spacing10 + spacing: 0 MMTextInput { id: newNameField width: parent.width - textFieldBackground.color: root.renameErrorText === "" ? __style.lightGreenColor : __style.negativeUltraLightColor - textFieldBackground.border.width: root.renameErrorText === "" ? 0 : __style.width2 - textFieldBackground.border.color: root.renameErrorText === "" ? __style.polarColor : __style.negativeColor + textFieldBackground.color: root.errorText === "" ? __style.lightGreenColor : __style.negativeUltraLightColor + textFieldBackground.border.width: root.errorText === "" ? 0 : __style.width2 + textFieldBackground.border.color: root.errorText === "" ? __style.polarColor : __style.negativeColor placeholderText: qsTr( "Enter the new name" ) - text: root.newProjectName - onTextEdited: root.clearRenameError() + onTextEdited: root.errorText = "" } + // Fixed-height slot so the drawer does not grow/shrink when the error message appears. Item { width: parent.width - height: __style.row24 - - visible: true + height: __style.spacing40 Row { - width: parent.width - height: parent.height anchors.verticalCenter: parent.verticalCenter - visible: root.renameErrorText !== "" + + width: parent.width spacing: __style.margin4 + visible: root.errorText !== "" + MMIcon { y: parent.height / 2 - height / 2 source: __style.errorCircleIcon @@ -64,14 +66,13 @@ MMDrawer { MMText { width: parent.width - __style.icon16 - parent.spacing - text: root.renameErrorText + text: root.errorText color: __style.grapeColor font: __style.t4 verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } } - } MMButton { @@ -84,13 +85,4 @@ MMDrawer { } } } - - function clearRenameError() { - root.renameErrorText = "" - } - - function showRenameError( message ) { - root.renameErrorText = message - } - } \ No newline at end of file diff --git a/core/localprojectsmanager.cpp b/core/localprojectsmanager.cpp index 4a898855c..7b501292b 100644 --- a/core/localprojectsmanager.cpp +++ b/core/localprojectsmanager.cpp @@ -15,12 +15,8 @@ #include #include -#include - -// Local (never synced to server) override for a project's display name, keyed by its -// stable projectDir. Stored outside of the Mergin metadata cache since that file is -// wholesale overwritten with the server's response on every sync (see MerginApi::writeData). -static const QString sLocalProjectNameSettingsGroup = QStringLiteral( "localProjectNames" ); +#include +#include LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) : mDataDir( dataDir ) @@ -32,9 +28,6 @@ void LocalProjectsManager::reloadDataDir() { mProjects.clear(); - QSettings settings; - settings.beginGroup( CoreUtils::QSETTINGS_APP_GROUP_NAME ); - QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); for ( const QString &folderName : entryList ) { @@ -54,18 +47,9 @@ void LocalProjectsManager::reloadDataDir() info.projectName = folderName; } - // A local rename overrides whatever name we just resolved above. - const QString customName = settings.value( sLocalProjectNameSettingsGroup + "/" + info.projectDir ).toString(); - if ( !customName.isEmpty() ) - { - info.projectName = customName; - } - mProjects << info; } - settings.endGroup(); - QString msg = QString( "Found %1 local projects in %2" ).arg( mProjects.size() ).arg( mDataDir ); CoreUtils::log( "Local projects", msg ); emit dataDirReloaded(); @@ -134,11 +118,6 @@ void LocalProjectsManager::removeLocalProject( const QString &projectId ) { emit aboutToRemoveLocalProject( mProjects[i] ); - QSettings settings; - settings.beginGroup( CoreUtils::QSETTINGS_APP_GROUP_NAME ); - settings.remove( sLocalProjectNameSettingsGroup + "/" + mProjects[i].projectDir ); - settings.endGroup(); - CoreUtils::removeDir( mProjects[i].projectDir ); mProjects.removeAt( i ); @@ -276,12 +255,14 @@ void LocalProjectsManager::addProject( const QString &projectDir, const QString QString LocalProjectsManager::renameLocalProject( const QString &projectId, const QString &newName ) { - if ( newName.trimmed().isEmpty() ) + const QString trimmedName = newName.trimmed(); + + if ( trimmedName.isEmpty() ) { return tr( "The project name cannot be empty" ); } - if ( !CoreUtils::isValidName( newName ) ) + if ( !CoreUtils::isValidName( trimmedName ) ) { return tr( "The project name contains invalid characters" ); } @@ -294,18 +275,56 @@ QString LocalProjectsManager::renameLocalProject( const QString &projectId, cons projectIndex = i; } - if ( i != projectIndex && mProjects[i].projectName == newName ) + if ( i != projectIndex && mProjects[i].projectName == trimmedName ) { return tr( "A project name is already taken" ); } } - QSettings settings; - settings.beginGroup( CoreUtils::QSETTINGS_APP_GROUP_NAME ); - settings.setValue( sLocalProjectNameSettingsGroup + "/" + mProjects[projectIndex].projectDir, newName ); - settings.endGroup(); + if ( projectIndex == -1 ) + { + return tr( "Project not found" ); + } - mProjects[projectIndex].projectName = newName; - emit localProjectDataChanged( mProjects[projectIndex] ); - return QString(); + LocalProject &project = mProjects[projectIndex]; + + if ( project.projectName == trimmedName ) + { + return {}; // name did not change, nothing to rename + } + + const QString oldProjectId = project.id(); + + const QString parentDir = QFileInfo( project.projectDir ).dir().absolutePath(); + const QString newProjectDir = CoreUtils::findUniquePath( parentDir + "/" + trimmedName ); + + if ( !QDir().rename( project.projectDir, newProjectDir ) ) + { + CoreUtils::log( "Rename project", QStringLiteral( "Failed to rename directory %1 to %2" ).arg( project.projectDir, newProjectDir ) ); + return tr( "Failed to rename the project directory" ); + } + + if ( !project.qgisProjectFilePath.isEmpty() ) + { + const QString relativeFilePath = QDir( project.projectDir ).relativeFilePath( project.qgisProjectFilePath ); + const QString oldFilePath = newProjectDir + "/" + relativeFilePath; + + QFileInfo oldFileInfo( oldFilePath ); + const QString newFilePath = oldFileInfo.dir().absoluteFilePath( trimmedName + "." + oldFileInfo.suffix() ); + + if ( oldFilePath != newFilePath && QFile::rename( oldFilePath, newFilePath ) ) + { + project.qgisProjectFilePath = newFilePath; + } + else + { + project.qgisProjectFilePath = oldFilePath; + } + } + + project.projectDir = newProjectDir; + project.projectName = trimmedName; + + emit localProjectRenamed( oldProjectId, project ); + return {}; } diff --git a/core/localprojectsmanager.h b/core/localprojectsmanager.h index 30282ec93..ca02b0054 100644 --- a/core/localprojectsmanager.h +++ b/core/localprojectsmanager.h @@ -53,7 +53,7 @@ class LocalProjectsManager : public QObject Q_INVOKABLE QString projectName( const QString &projectId ) const; - Q_INVOKABLE QString renameLocalProject( const QString &projectId, const QString &newName ); + QString renameLocalProject( const QString &projectId, const QString &newName ); /** * Returns changes of a project specified by projectId in the form : @@ -73,6 +73,7 @@ class LocalProjectsManager : public QObject signals: void localProjectAdded( const LocalProject &project ); void localProjectDataChanged( const LocalProject &project ); + void localProjectRenamed( const QString &oldProjectId, const LocalProject &project ); void aboutToRemoveLocalProject( const LocalProject &project ); void dataDirReloaded(); diff --git a/gallery/qml/pages/DrawerPage.qml b/gallery/qml/pages/DrawerPage.qml index ff0fa1ab4..47a0aab1f 100644 --- a/gallery/qml/pages/DrawerPage.qml +++ b/gallery/qml/pages/DrawerPage.qml @@ -48,11 +48,6 @@ Page { onClicked: removeProjectDialog.open() } - Button { - text: "MMRenameProjectDialog" - onClicked: renameProjectDialog.open() - } - Button { text: "MMDownloadProjectDialog" onClicked: downloadProjectDialog.open() @@ -256,15 +251,6 @@ Page { onRemoveClicked: console.log("on remove clicked") } - MMRenameProjectDialog { - id: renameProjectDialog - - relatedProjectId: "Cool Project" - newProjectName: "Even cooler Project" - - onRenameClicked: console.log("on rename clicked") - } - MMProjectLimitDialog { id: projectLimitDialog From d0a0be1e397c48baa14d8960a754b1b6c03901d7 Mon Sep 17 00:00:00 2001 From: Richard Kello Date: Thu, 3 Sep 2026 18:01:56 +0200 Subject: [PATCH 3/5] Add unit tests and update code --- app/CMakeLists.txt | 2 + app/projectsmodel.cpp | 5 - app/qml/project/MMProjectList.qml | 15 +- .../components/MMRenameProjectDialog.qml | 51 +---- app/test/inputtests.cpp | 6 + app/test/testlocalprojectsmanager.cpp | 191 ++++++++++++++++++ app/test/testlocalprojectsmanager.h | 36 ++++ app/test/testmodels.cpp | 94 +++++++++ app/test/testmodels.h | 2 + core/localprojectsmanager.h | 2 + test/CMakeLists.txt | 1 + 11 files changed, 354 insertions(+), 51 deletions(-) create mode 100644 app/test/testlocalprojectsmanager.cpp create mode 100644 app/test/testlocalprojectsmanager.h diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index ed910b431..a794b0f03 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -241,6 +241,7 @@ if (ENABLE_TESTS) test/testimageutils.cpp test/testlayertree.cpp test/testlinks.cpp + test/testlocalprojectsmanager.cpp test/testmaptools.cpp test/testmerginapi.cpp test/testmodels.cpp @@ -268,6 +269,7 @@ if (ENABLE_TESTS) test/testimageutils.h test/testlayertree.h test/testlinks.h + test/testlocalprojectsmanager.h test/testmaptools.h test/testmerginapi.h test/testmodels.h diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 920e33417..044f12e71 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -397,11 +397,6 @@ void ProjectsModel::onProjectRenamed( const QString &oldProjectId, const LocalPr project.local = localProject; - if ( project.isMergin() ) - { - project.mergin.status = ProjectStatus::projectStatus( project, mBackend->supportsSelectiveSync() ); - } - QModelIndex editIndex = index( ix ); emit dataChanged( editIndex, editIndex ); diff --git a/app/qml/project/MMProjectList.qml b/app/qml/project/MMProjectList.qml index 768a35b24..9db3a3e4a 100644 --- a/app/qml/project/MMProjectList.qml +++ b/app/qml/project/MMProjectList.qml @@ -26,7 +26,6 @@ Item { property string searchText: "" property int spacing: 0 property bool activeProjectAlwaysFirst: false - property string projectIdToRename: "" property alias projectsProxyModel: viewModel property alias projectsModel: controllerModel property alias listHeader: listview.header @@ -177,8 +176,8 @@ Item { } onStopSyncRequested: controllerModel.stopProjectSync( projectId ) onShowChangesRequested: root.showLocalChangesRequested( projectId ) - onRenameRequested: { - root.projectIdToRename = projectId + onRenameRequested: () => { + internal.projectIdToRename = projectId renameDialog.open() } } @@ -315,11 +314,11 @@ Item { id: renameDialog onRenameClicked: function( newName ) { - if ( !root.projectIdToRename ) { + if ( !internal.projectIdToRename ) { return } - const renameResult = controllerModel.renameLocalProject( root.projectIdToRename, newName ) + const renameResult = controllerModel.renameLocalProject( internal.projectIdToRename, newName ) if ( !renameResult ) { renameDialog.close() @@ -338,4 +337,10 @@ Item { downloadProjectDialog.relatedProjectId = "" } } + + QtObject { + id: internal + + property string projectIdToRename: "" + } } diff --git a/app/qml/project/components/MMRenameProjectDialog.qml b/app/qml/project/components/MMRenameProjectDialog.qml index ff37b8974..662d5e0c4 100644 --- a/app/qml/project/components/MMRenameProjectDialog.qml +++ b/app/qml/project/components/MMRenameProjectDialog.qml @@ -15,68 +15,37 @@ import "../../inputs" MMDrawer { id: root - property string errorText: "" + property alias errorText: newNameField.errorMsg signal renameClicked( string newName ) drawerHeader.title: qsTr( "Rename project" ) drawerHeader.titleFont: __style.t2 - onOpened: { - root.errorText = "" + onAboutToShow: () => { + newNameField.errorMsg = "" newNameField.text = "" } drawerContent: Column { + id: contentColumn + width: parent.width - spacing: 0 + spacing: newNameField.errorMsg ? __style.margin12 : __style.spacing40 MMTextInput { id: newNameField - width: parent.width - textFieldBackground.color: root.errorText === "" ? __style.lightGreenColor : __style.negativeUltraLightColor - textFieldBackground.border.width: root.errorText === "" ? 0 : __style.width2 - textFieldBackground.border.color: root.errorText === "" ? __style.polarColor : __style.negativeColor + width: contentColumn.width + textFieldBackground.color: __style.lightGreenColor placeholderText: qsTr( "Enter the new name" ) - onTextEdited: root.errorText = "" - } - - // Fixed-height slot so the drawer does not grow/shrink when the error message appears. - Item { - width: parent.width - height: __style.spacing40 - - Row { - anchors.verticalCenter: parent.verticalCenter - - width: parent.width - spacing: __style.margin4 - - visible: root.errorText !== "" - - MMIcon { - y: parent.height / 2 - height / 2 - source: __style.errorCircleIcon - color: __style.negativeColor - size: __style.icon16 - } - - MMText { - width: parent.width - __style.icon16 - parent.spacing - text: root.errorText - color: __style.grapeColor - font: __style.t4 - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - } + onTextEdited: () => newNameField.errorMsg = "" } MMButton { - width: parent.width + width: contentColumn.width text: qsTr( "Confirm" ) diff --git a/app/test/inputtests.cpp b/app/test/inputtests.cpp index 5fe25939e..6b359bb87 100644 --- a/app/test/inputtests.cpp +++ b/app/test/inputtests.cpp @@ -27,6 +27,7 @@ #include "testimageutils.h" #include "testmaptools.h" #include "testlayertree.h" +#include "testlocalprojectsmanager.h" #include "testactiveproject.h" #include "testprojectchecksumcache.h" #include "testmultieditmanager.h" @@ -172,6 +173,11 @@ int InputTests::runTest() const TestLayerTree layerTreeTest; nFailed = QTest::qExec( &layerTreeTest, mTestArgs ); } + else if ( mTestRequested == "--testLocalProjectsManager" ) + { + TestLocalProjectsManager localProjectsManagerTest; + nFailed = QTest::qExec( &localProjectsManagerTest, mTestArgs ); + } else if ( mTestRequested == "--testActiveProject" ) { TestActiveProject activeProjectTest( mApi ); diff --git a/app/test/testlocalprojectsmanager.cpp b/app/test/testlocalprojectsmanager.cpp new file mode 100644 index 000000000..6ae163816 --- /dev/null +++ b/app/test/testlocalprojectsmanager.cpp @@ -0,0 +1,191 @@ +/*************************************************************************** + * * + * 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 "testlocalprojectsmanager.h" +#include "localprojectsmanager.h" + +#include +#include +#include + +namespace +{ + //! Creates a fake local project directory containing a dummy .qgz file, so + //! LocalProjectsManager::findQgisProjectFile() has something to discover. + QString createFakeProject( const QString &dataDir, const QString &name ) + { + QString projectDir = dataDir + "/" + name; + QDir().mkpath( projectDir ); + + QFile qgzFile( projectDir + "/" + name + ".qgz" ); + qgzFile.open( QIODevice::WriteOnly ); + qgzFile.close(); + + return projectDir; + } +} + +void TestLocalProjectsManager::init() +{ + mDataDir = QDir::tempPath() + QStringLiteral( "/testLocalProjectsManager" ); + + QDir dir( mDataDir ); + if ( dir.exists() ) + dir.removeRecursively(); + + QDir().mkpath( mDataDir ); +} + +void TestLocalProjectsManager::cleanup() +{ + QDir( mDataDir ).removeRecursively(); +} + +void TestLocalProjectsManager::testRenameSuccess() +{ + createFakeProject( mDataDir, "OriginalName" ); + + LocalProjectsManager manager( mDataDir ); + QCOMPARE( manager.projects().size(), 1 ); + + QString projectId = manager.projects().first().id(); + QCOMPARE( projectId, QStringLiteral( "OriginalName" ) ); + + QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + + QString result = manager.renameLocalProject( projectId, QStringLiteral( "NewName" ) ); + + QCOMPARE( result, QString() ); + QCOMPARE( renamedSpy.count(), 1 ); + QCOMPARE( renamedSpy.at( 0 ).at( 0 ).toString(), projectId ); + + QVERIFY( !QDir( mDataDir + "/OriginalName" ).exists() ); + QVERIFY( QDir( mDataDir + "/NewName" ).exists() ); + QVERIFY( QFile::exists( mDataDir + "/NewName/NewName.qgz" ) ); + + LocalProject updated = manager.projectFromProjectId( QStringLiteral( "NewName" ) ); + QVERIFY( updated.isValid() ); + QCOMPARE( updated.projectName, QStringLiteral( "NewName" ) ); + QCOMPARE( updated.projectDir, mDataDir + "/NewName" ); + QCOMPARE( updated.qgisProjectFilePath, mDataDir + "/NewName/NewName.qgz" ); +} + +void TestLocalProjectsManager::testRenameEmptyName() +{ + createFakeProject( mDataDir, "OriginalName" ); + + LocalProjectsManager manager( mDataDir ); + QString projectId = manager.projects().first().id(); + + QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + + QString result = manager.renameLocalProject( projectId, QStringLiteral( " " ) ); + + QVERIFY( !result.isEmpty() ); + QCOMPARE( renamedSpy.count(), 0 ); + QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); +} + +void TestLocalProjectsManager::testRenameInvalidCharacters() +{ + createFakeProject( mDataDir, "OriginalName" ); + + LocalProjectsManager manager( mDataDir ); + QString projectId = manager.projects().first().id(); + + QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + + QString result = manager.renameLocalProject( projectId, QStringLiteral( "Bad/Name" ) ); + + QVERIFY( !result.isEmpty() ); + QCOMPARE( renamedSpy.count(), 0 ); + QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); +} + +void TestLocalProjectsManager::testRenameNameAlreadyTaken() +{ + createFakeProject( mDataDir, "ProjectA" ); + createFakeProject( mDataDir, "ProjectB" ); + + LocalProjectsManager manager( mDataDir ); + QCOMPARE( manager.projects().size(), 2 ); + + QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + + QString result = manager.renameLocalProject( QStringLiteral( "ProjectA" ), QStringLiteral( "ProjectB" ) ); + + QVERIFY( !result.isEmpty() ); + QCOMPARE( renamedSpy.count(), 0 ); + QVERIFY( QDir( mDataDir + "/ProjectA" ).exists() ); + QVERIFY( QDir( mDataDir + "/ProjectB" ).exists() ); +} + +void TestLocalProjectsManager::testRenameSameName() +{ + createFakeProject( mDataDir, "OriginalName" ); + + LocalProjectsManager manager( mDataDir ); + QString projectId = manager.projects().first().id(); + + QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + + QString result = manager.renameLocalProject( projectId, QStringLiteral( "OriginalName" ) ); + + QCOMPARE( result, QString() ); + QCOMPARE( renamedSpy.count(), 0 ); // no-op - nothing should have been touched + QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); +} + +void TestLocalProjectsManager::testRenameUnknownProject() +{ + LocalProjectsManager manager( mDataDir ); + QCOMPARE( manager.projects().size(), 0 ); + + QString result = manager.renameLocalProject( QStringLiteral( "does-not-exist" ), QStringLiteral( "NewName" ) ); + + QVERIFY( !result.isEmpty() ); +} + +void TestLocalProjectsManager::testRenameDirectoryCollision() +{ + createFakeProject( mDataDir, "OriginalName" ); + + LocalProjectsManager manager( mDataDir ); + QString projectId = manager.projects().first().id(); + + // A folder that already exists on disk at the rename target path, but that the + // manager does not know about (created after it last scanned mDataDir). + QDir().mkpath( mDataDir + "/NewName" ); + + QString result = manager.renameLocalProject( projectId, QStringLiteral( "NewName" ) ); + + QCOMPARE( result, QString() ); + QVERIFY( QDir( mDataDir + "/NewName (1)" ).exists() ); + + LocalProject updated = manager.projectFromProjectId( QStringLiteral( "NewName (1)" ) ); + QVERIFY( updated.isValid() ); + QCOMPARE( updated.projectDir, mDataDir + "/NewName (1)" ); +} + +void TestLocalProjectsManager::testRenameTrimsWhitespace() +{ + createFakeProject( mDataDir, "OriginalName" ); + + LocalProjectsManager manager( mDataDir ); + QString projectId = manager.projects().first().id(); + + QString result = manager.renameLocalProject( projectId, QStringLiteral( " NewName " ) ); + + QCOMPARE( result, QString() ); + QVERIFY( QDir( mDataDir + "/NewName" ).exists() ); + + LocalProject updated = manager.projectFromProjectId( QStringLiteral( "NewName" ) ); + QVERIFY( updated.isValid() ); + QCOMPARE( updated.projectName, QStringLiteral( "NewName" ) ); +} diff --git a/app/test/testlocalprojectsmanager.h b/app/test/testlocalprojectsmanager.h new file mode 100644 index 000000000..994d85352 --- /dev/null +++ b/app/test/testlocalprojectsmanager.h @@ -0,0 +1,36 @@ +/*************************************************************************** + * * + * 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 TESTLOCALPROJECTSMANAGER_H +#define TESTLOCALPROJECTSMANAGER_H + +#include + +class TestLocalProjectsManager : public QObject +{ + Q_OBJECT + + private slots: + void init(); // will be called before each testfunction is executed. + void cleanup(); // will be called after every testfunction. + + void testRenameSuccess(); + void testRenameEmptyName(); + void testRenameInvalidCharacters(); + void testRenameNameAlreadyTaken(); + void testRenameSameName(); + void testRenameUnknownProject(); + void testRenameDirectoryCollision(); + void testRenameTrimsWhitespace(); + + private: + QString mDataDir; +}; + +#endif // TESTLOCALPROJECTSMANAGER_H diff --git a/app/test/testmodels.cpp b/app/test/testmodels.cpp index 48682ace1..ecd56cf90 100644 --- a/app/test/testmodels.cpp +++ b/app/test/testmodels.cpp @@ -527,3 +527,97 @@ void TestModels::testProjectsProxyModel() QCOMPARE( proxy.data( proxy.index( 1, 0 ), ProjectsModel::Roles::ProjectId ).toString(), p1.id() ); QCOMPARE( proxy.data( proxy.index( 2, 0 ), ProjectsModel::Roles::ProjectId ).toString(), p0.id() ); } + +void TestModels::testProjectsModelOnProjectRenamed() +{ + // A purely local project - no namespace, so its id() is derived from its directory name, + // exactly like LocalProjectsManager::renameLocalProject() produces after renaming on disk. + Project p0; + p0.local.projectName = QStringLiteral( "OriginalName" ); + p0.local.projectDir = QStringLiteral( "/tmp/OriginalName" ); + + Project p1; + p1.local.projectNamespace = QStringLiteral( "namespace" ); + p1.local.projectName = QStringLiteral( "project_A" ); + p1.local.projectDir = QStringLiteral( "project_A_dir" ); + + ProjectsModel model; + model.setModelType( ProjectsModel::LocalProjectsModel ); + model.mProjects << p0 << p1; + + QCOMPARE( model.rowCount(), 2 ); + + QString oldId = p0.local.id(); + + LocalProject renamed = p0.local; + renamed.projectName = QStringLiteral( "NewName" ); + renamed.projectDir = QStringLiteral( "/tmp/NewName" ); + + QVERIFY( oldId != renamed.id() ); // renaming a local-only project does change its id + + QSignalSpy dataChangedSpy( &model, &QAbstractItemModel::dataChanged ); + + model.onProjectRenamed( oldId, renamed ); + + // this is an in-place update of an existing row, not an add/remove + QCOMPARE( model.rowCount(), 2 ); + QCOMPARE( dataChangedSpy.count(), 1 ); + + // the row can be found again via its NEW id - this is exactly the lookup that used to be + // broken, since the row was previously only searchable under its OLD (pre-rename) id + QModelIndex newIndex = model.projectModelIndexFromId( renamed.id() ); + QVERIFY( newIndex.isValid() ); + QCOMPARE( model.data( newIndex, ProjectsModel::Roles::ProjectName ).toString(), QStringLiteral( "NewName" ) ); + QCOMPARE( model.data( newIndex, ProjectsModel::Roles::ProjectId ).toString(), renamed.id() ); + + // the row is no longer reachable under its old id + QVERIFY( !model.projectModelIndexFromId( oldId ).isValid() ); + + // the other project in the model is untouched + QModelIndex otherIndex = model.projectModelIndexFromId( p1.id() ); + QCOMPARE( model.data( otherIndex, ProjectsModel::Roles::ProjectName ).toString(), QStringLiteral( "project_A" ) ); +} + +void TestModels::testProjectsModelOnProjectRenamedActiveProject() +{ + Project p0; + p0.local.projectName = QStringLiteral( "OriginalName" ); + p0.local.projectDir = QStringLiteral( "/tmp/OriginalName" ); + + Project p1; + p1.local.projectNamespace = QStringLiteral( "namespace" ); + p1.local.projectName = QStringLiteral( "project_A" ); + p1.local.projectDir = QStringLiteral( "project_A_dir" ); + + ProjectsModel model; + model.setModelType( ProjectsModel::LocalProjectsModel ); + model.mProjects << p0 << p1; + + QString oldId = p0.local.id(); + + LocalProject renamed = p0.local; + renamed.projectName = QStringLiteral( "NewName" ); + renamed.projectDir = QStringLiteral( "/tmp/NewName" ); + + // Renaming a project that is NOT the active one must leave activeProjectId untouched + model.setActiveProjectId( p1.id() ); + QSignalSpy inactiveRenameSpy( &model, &ProjectsModel::activeProjectIdChanged ); + + model.onProjectRenamed( oldId, renamed ); + + QCOMPARE( model.activeProjectId(), p1.id() ); + QCOMPARE( inactiveRenameSpy.count(), 0 ); + + // Renaming the ACTIVE project must update activeProjectId to follow it + model.setActiveProjectId( renamed.id() ); // simulate that the just-renamed project is now active + QSignalSpy activeRenameSpy( &model, &ProjectsModel::activeProjectIdChanged ); + + LocalProject renamedAgain = renamed; + renamedAgain.projectName = QStringLiteral( "NewerName" ); + renamedAgain.projectDir = QStringLiteral( "/tmp/NewerName" ); + + model.onProjectRenamed( renamed.id(), renamedAgain ); + + QCOMPARE( model.activeProjectId(), renamedAgain.id() ); + QCOMPARE( activeRenameSpy.count(), 1 ); +} diff --git a/app/test/testmodels.h b/app/test/testmodels.h index a6ea309cf..dd88434e9 100644 --- a/app/test/testmodels.h +++ b/app/test/testmodels.h @@ -28,6 +28,8 @@ class TestModels : public QObject void testValueRelationHotreload(); void testProjectsModel(); void testProjectsProxyModel(); + void testProjectsModelOnProjectRenamed(); + void testProjectsModelOnProjectRenamedActiveProject(); }; diff --git a/core/localprojectsmanager.h b/core/localprojectsmanager.h index ca02b0054..e41f2b396 100644 --- a/core/localprojectsmanager.h +++ b/core/localprojectsmanager.h @@ -53,6 +53,8 @@ class LocalProjectsManager : public QObject Q_INVOKABLE QString projectName( const QString &projectId ) const; + //! Renames the local project's directory and main QGIS project file to newName. + //! Returns an empty string on success, or a user-facing error message on failure. QString renameLocalProject( const QString &projectId, const QString &newName ); /** diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 19c5cdbf2..20998e3bb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -22,6 +22,7 @@ set(MM_TESTS testImageUtils testMapTools testLayerTree + testLocalProjectsManager testActiveProject testProjectChecksumCache testVariablesManager From 269c309484bda08a1984033826334a84c1ef6939 Mon Sep 17 00:00:00 2001 From: Richard Kello Date: Mon, 21 Sep 2026 17:56:23 +0200 Subject: [PATCH 4/5] Update code --- app/activeproject.cpp | 12 +++ app/projectsmodel.cpp | 16 ++-- app/projectsmodel.h | 7 +- app/qml/project/MMProjectList.qml | 65 +++++++++---- .../project/components/MMProjectDelegate.qml | 2 +- .../components/MMRenameProjectDialog.qml | 7 +- app/test/testlocalprojectsmanager.cpp | 94 +++++++++++++++---- app/test/testlocalprojectsmanager.h | 3 + app/test/testmodels.cpp | 6 +- core/localprojectsmanager.cpp | 78 ++++++++++----- core/localprojectsmanager.h | 14 ++- 11 files changed, 227 insertions(+), 77 deletions(-) diff --git a/app/activeproject.cpp b/app/activeproject.cpp index 77060f0f6..4329b9bcc 100644 --- a/app/activeproject.cpp +++ b/app/activeproject.cpp @@ -59,6 +59,18 @@ ActiveProject::ActiveProject( AppSettings &appSettings } } ); + // listen to local project rename event to unload mProject before it is renamed on disk + QObject::connect( + &mLocalProjectsManager, + &LocalProjectsManager::aboutToRenameLocalProject, + this, [this]( const QString & projectId ) + { + if ( projectId == mLocalProject.id() ) + { + load( QLatin1String() ); + } + } ); + // listen to metadata changes of opened LocalProject (e.g. local version update or namespace update) QObject::connect( &mLocalProjectsManager, diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 044f12e71..68b21ec11 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -54,6 +54,7 @@ void ProjectsModel::initializeProjectsModel() QObject::connect( mLocalProjectsManager, &LocalProjectsManager::aboutToRemoveLocalProject, this, &ProjectsModel::onAboutToRemoveProject ); QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel::onProjectDataChanged ); QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectRenamed, this, &ProjectsModel::onProjectRenamed ); + QObject::connect( mLocalProjectsManager, &LocalProjectsManager::renameLocalProjectFinished, this, &ProjectsModel::renameLocalProjectFinished ); QObject::connect( mLocalProjectsManager, &LocalProjectsManager::dataDirReloaded, this, &ProjectsModel::loadLocalProjects ); emit modelInitialized(); @@ -381,9 +382,14 @@ void ProjectsModel::removeLocalProject( const QString &projectId ) mLocalProjectsManager->removeLocalProject( projectId ); } -QString ProjectsModel::renameLocalProject( const QString &projectId, const QString &newName ) +QString ProjectsModel::canRenameProject( const QString &projectId, const QString &newName ) const { - return mLocalProjectsManager->renameLocalProject( projectId, newName ); + return mLocalProjectsManager->canRenameProject( projectId, newName ); +} + +void ProjectsModel::renameLocalProject( const QString &projectId, const QString &newName ) +{ + mLocalProjectsManager->renameLocalProject( projectId, newName ); } void ProjectsModel::onProjectRenamed( const QString &oldProjectId, const LocalProject &localProject ) @@ -399,12 +405,6 @@ void ProjectsModel::onProjectRenamed( const QString &oldProjectId, const LocalPr QModelIndex editIndex = index( ix ); emit dataChanged( editIndex, editIndex ); - - if ( mActiveProjectId == oldProjectId ) - { - mActiveProjectId = localProject.id(); - emit activeProjectIdChanged( mActiveProjectId ); - } } void ProjectsModel::migrateProject( const QString &projectId ) diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 691387940..9cc3ac27c 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -127,8 +127,11 @@ class ProjectsModel : public QAbstractListModel //! Forwards call to LocalProjectsManager to remove local project Q_INVOKABLE void removeLocalProject( const QString &projectId ); + //! Forwards call to LocalProjectsManager to check whether a rename is valid + Q_INVOKABLE QString canRenameProject( const QString &projectId, const QString &newName ) const; + //! Forwards call to LocalProjectsManager to rename local project - Q_INVOKABLE QString renameLocalProject( const QString &projectId, const QString &newName ); + Q_INVOKABLE void renameLocalProject( const QString &projectId, const QString &newName ); //! Migrates local project to mergin Q_INVOKABLE void migrateProject( const QString &projectId ); @@ -197,6 +200,8 @@ class ProjectsModel : public QAbstractListModel void activeProjectIdChanged( QString projectId ); + void renameLocalProjectFinished( bool success ); + private: int projectIndexFromId( const QString &projectId ) const; diff --git a/app/qml/project/MMProjectList.qml b/app/qml/project/MMProjectList.qml index 9db3a3e4a..93500d4e7 100644 --- a/app/qml/project/MMProjectList.qml +++ b/app/qml/project/MMProjectList.qml @@ -177,8 +177,8 @@ Item { onStopSyncRequested: controllerModel.stopProjectSync( projectId ) onShowChangesRequested: root.showLocalChangesRequested( projectId ) onRenameRequested: () => { - internal.projectIdToRename = projectId - renameDialog.open() + renameDialogLoader.projectIdToRename = projectId + renameDialogLoader.active = true } } } @@ -310,21 +310,56 @@ Item { } } - MMProjectComponents.MMRenameProjectDialog { - id: renameDialog + Loader { + id: renameDialogLoader - onRenameClicked: function( newName ) { - if ( !internal.projectIdToRename ) { - return + property string projectIdToRename: "" + + active: false + asynchronous: true + + sourceComponent: MMProjectComponents.MMRenameProjectDialog { + id: renameDialog + + projectId: renameDialogLoader.projectIdToRename + + onRenameClicked: function( newName ) { + if ( !renameDialog.projectId ) { + return + } + + controllerModel.renameLocalProject( renameDialog.projectId, newName ) + } + + onTextEdited: function( text ) { + canRenameCheckTimer.pendingText = text + canRenameCheckTimer.restart() } - const renameResult = controllerModel.renameLocalProject( internal.projectIdToRename, newName ) + onClosed: renameDialogLoader.active = false + + Component.onCompleted: open() + + Connections { + target: controllerModel - if ( !renameResult ) { - renameDialog.close() + function onRenameLocalProjectFinished( success ) { + if ( success ) { + renameDialog.close() + } + } } - else { - renameDialog.errorText = renameResult + + Timer { + id: canRenameCheckTimer + + property string pendingText: "" + + interval: 300 + + onTriggered: { + renameDialog.errorText = controllerModel.canRenameProject( renameDialog.projectId, pendingText ) + } } } } @@ -337,10 +372,4 @@ Item { downloadProjectDialog.relatedProjectId = "" } } - - QtObject { - id: internal - - property string projectIdToRename: "" - } } diff --git a/app/qml/project/components/MMProjectDelegate.qml b/app/qml/project/components/MMProjectDelegate.qml index c945807a7..9e692462c 100644 --- a/app/qml/project/components/MMProjectDelegate.qml +++ b/app/qml/project/components/MMProjectDelegate.qml @@ -310,7 +310,7 @@ Control { "callback": () => root.migrateRequested() }, "rename": { - "name": qsTr("Rename the local project"), + "name": qsTr("Rename project"), "iconSource": __style.editIcon, "callback": () => root.renameRequested() } diff --git a/app/qml/project/components/MMRenameProjectDialog.qml b/app/qml/project/components/MMRenameProjectDialog.qml index 662d5e0c4..5a371d227 100644 --- a/app/qml/project/components/MMRenameProjectDialog.qml +++ b/app/qml/project/components/MMRenameProjectDialog.qml @@ -15,9 +15,11 @@ import "../../inputs" MMDrawer { id: root + property string projectId: "" property alias errorText: newNameField.errorMsg signal renameClicked( string newName ) + signal textEdited( string text ) drawerHeader.title: qsTr( "Rename project" ) drawerHeader.titleFont: __style.t2 @@ -41,7 +43,10 @@ MMDrawer { placeholderText: qsTr( "Enter the new name" ) - onTextEdited: () => newNameField.errorMsg = "" + onTextEdited: ( text ) => { + newNameField.errorMsg = "" + root.textEdited( text ) + } } MMButton { diff --git a/app/test/testlocalprojectsmanager.cpp b/app/test/testlocalprojectsmanager.cpp index 6ae163816..25e6883e8 100644 --- a/app/test/testlocalprojectsmanager.cpp +++ b/app/test/testlocalprojectsmanager.cpp @@ -58,10 +58,12 @@ void TestLocalProjectsManager::testRenameSuccess() QCOMPARE( projectId, QStringLiteral( "OriginalName" ) ); QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); - QString result = manager.renameLocalProject( projectId, QStringLiteral( "NewName" ) ); + manager.renameLocalProject( projectId, QStringLiteral( "NewName" ) ); - QCOMPARE( result, QString() ); + QCOMPARE( finishedSpy.count(), 1 ); + QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), true ); QCOMPARE( renamedSpy.count(), 1 ); QCOMPARE( renamedSpy.at( 0 ).at( 0 ).toString(), projectId ); @@ -84,10 +86,12 @@ void TestLocalProjectsManager::testRenameEmptyName() QString projectId = manager.projects().first().id(); QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); - QString result = manager.renameLocalProject( projectId, QStringLiteral( " " ) ); + manager.renameLocalProject( projectId, QStringLiteral( " " ) ); - QVERIFY( !result.isEmpty() ); + QCOMPARE( finishedSpy.count(), 1 ); + QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), false ); QCOMPARE( renamedSpy.count(), 0 ); QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); } @@ -100,10 +104,12 @@ void TestLocalProjectsManager::testRenameInvalidCharacters() QString projectId = manager.projects().first().id(); QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); - QString result = manager.renameLocalProject( projectId, QStringLiteral( "Bad/Name" ) ); + manager.renameLocalProject( projectId, QStringLiteral( "Bad/Name" ) ); - QVERIFY( !result.isEmpty() ); + QCOMPARE( finishedSpy.count(), 1 ); + QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), false ); QCOMPARE( renamedSpy.count(), 0 ); QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); } @@ -117,10 +123,12 @@ void TestLocalProjectsManager::testRenameNameAlreadyTaken() QCOMPARE( manager.projects().size(), 2 ); QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); - QString result = manager.renameLocalProject( QStringLiteral( "ProjectA" ), QStringLiteral( "ProjectB" ) ); + manager.renameLocalProject( QStringLiteral( "ProjectA" ), QStringLiteral( "ProjectB" ) ); - QVERIFY( !result.isEmpty() ); + QCOMPARE( finishedSpy.count(), 1 ); + QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), false ); QCOMPARE( renamedSpy.count(), 0 ); QVERIFY( QDir( mDataDir + "/ProjectA" ).exists() ); QVERIFY( QDir( mDataDir + "/ProjectB" ).exists() ); @@ -134,11 +142,14 @@ void TestLocalProjectsManager::testRenameSameName() QString projectId = manager.projects().first().id(); QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); - QString result = manager.renameLocalProject( projectId, QStringLiteral( "OriginalName" ) ); + manager.renameLocalProject( projectId, QStringLiteral( "OriginalName" ) ); - QCOMPARE( result, QString() ); - QCOMPARE( renamedSpy.count(), 0 ); // no-op - nothing should have been touched + // a no-op rename still reports success, but nothing on disk is touched + QCOMPARE( finishedSpy.count(), 1 ); + QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), true ); + QCOMPARE( renamedSpy.count(), 1 ); QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); } @@ -147,9 +158,14 @@ void TestLocalProjectsManager::testRenameUnknownProject() LocalProjectsManager manager( mDataDir ); QCOMPARE( manager.projects().size(), 0 ); - QString result = manager.renameLocalProject( QStringLiteral( "does-not-exist" ), QStringLiteral( "NewName" ) ); + QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); + + manager.renameLocalProject( QStringLiteral( "does-not-exist" ), QStringLiteral( "NewName" ) ); - QVERIFY( !result.isEmpty() ); + QCOMPARE( finishedSpy.count(), 1 ); + QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), false ); + QCOMPARE( renamedSpy.count(), 0 ); } void TestLocalProjectsManager::testRenameDirectoryCollision() @@ -163,9 +179,14 @@ void TestLocalProjectsManager::testRenameDirectoryCollision() // manager does not know about (created after it last scanned mDataDir). QDir().mkpath( mDataDir + "/NewName" ); - QString result = manager.renameLocalProject( projectId, QStringLiteral( "NewName" ) ); + QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); + + manager.renameLocalProject( projectId, QStringLiteral( "NewName" ) ); - QCOMPARE( result, QString() ); + QCOMPARE( finishedSpy.count(), 1 ); + QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), true ); + QCOMPARE( renamedSpy.count(), 1 ); QVERIFY( QDir( mDataDir + "/NewName (1)" ).exists() ); LocalProject updated = manager.projectFromProjectId( QStringLiteral( "NewName (1)" ) ); @@ -180,12 +201,51 @@ void TestLocalProjectsManager::testRenameTrimsWhitespace() LocalProjectsManager manager( mDataDir ); QString projectId = manager.projects().first().id(); - QString result = manager.renameLocalProject( projectId, QStringLiteral( " NewName " ) ); + QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); + + manager.renameLocalProject( projectId, QStringLiteral( " NewName " ) ); - QCOMPARE( result, QString() ); + QCOMPARE( finishedSpy.count(), 1 ); + QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), true ); + QCOMPARE( renamedSpy.count(), 1 ); QVERIFY( QDir( mDataDir + "/NewName" ).exists() ); LocalProject updated = manager.projectFromProjectId( QStringLiteral( "NewName" ) ); QVERIFY( updated.isValid() ); QCOMPARE( updated.projectName, QStringLiteral( "NewName" ) ); } + +void TestLocalProjectsManager::testCanRenameProjectAccepts() +{ + createFakeProject( mDataDir, "OriginalName" ); + + LocalProjectsManager manager( mDataDir ); + QString projectId = manager.projects().first().id(); + + QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); + QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); + + QCOMPARE( manager.canRenameProject( projectId, QStringLiteral( "NewName" ) ), QString() ); + QCOMPARE( manager.canRenameProject( projectId, QStringLiteral( "OriginalName" ) ), QString() ); // unchanged name is fine too + + // canRenameProject() must be side-effect-free + QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); + QCOMPARE( manager.projectFromProjectId( projectId ).projectName, QStringLiteral( "OriginalName" ) ); + QCOMPARE( renamedSpy.count(), 0 ); + QCOMPARE( finishedSpy.count(), 0 ); +} + +void TestLocalProjectsManager::testCanRenameProjectRejects() +{ + createFakeProject( mDataDir, "ProjectA" ); + createFakeProject( mDataDir, "ProjectB" ); + + LocalProjectsManager manager( mDataDir ); + QString projectId = manager.projectFromDirectory( mDataDir + "/ProjectA" ).id(); + + QVERIFY( !manager.canRenameProject( projectId, QStringLiteral( " " ) ).isEmpty() ); + QVERIFY( !manager.canRenameProject( projectId, QStringLiteral( "Bad/Name" ) ).isEmpty() ); + QVERIFY( !manager.canRenameProject( projectId, QStringLiteral( "ProjectB" ) ).isEmpty() ); + QVERIFY( !manager.canRenameProject( QStringLiteral( "does-not-exist" ), QStringLiteral( "NewName" ) ).isEmpty() ); +} diff --git a/app/test/testlocalprojectsmanager.h b/app/test/testlocalprojectsmanager.h index 994d85352..446a21ca5 100644 --- a/app/test/testlocalprojectsmanager.h +++ b/app/test/testlocalprojectsmanager.h @@ -29,6 +29,9 @@ class TestLocalProjectsManager : public QObject void testRenameDirectoryCollision(); void testRenameTrimsWhitespace(); + void testCanRenameProjectAccepts(); + void testCanRenameProjectRejects(); + private: QString mDataDir; }; diff --git a/app/test/testmodels.cpp b/app/test/testmodels.cpp index ecd56cf90..858cfcef0 100644 --- a/app/test/testmodels.cpp +++ b/app/test/testmodels.cpp @@ -608,7 +608,7 @@ void TestModels::testProjectsModelOnProjectRenamedActiveProject() QCOMPARE( model.activeProjectId(), p1.id() ); QCOMPARE( inactiveRenameSpy.count(), 0 ); - // Renaming the ACTIVE project must update activeProjectId to follow it + // Renaming the ACTIVE project must NOT make ProjectsModel follow the id itself anymore model.setActiveProjectId( renamed.id() ); // simulate that the just-renamed project is now active QSignalSpy activeRenameSpy( &model, &ProjectsModel::activeProjectIdChanged ); @@ -618,6 +618,6 @@ void TestModels::testProjectsModelOnProjectRenamedActiveProject() model.onProjectRenamed( renamed.id(), renamedAgain ); - QCOMPARE( model.activeProjectId(), renamedAgain.id() ); - QCOMPARE( activeRenameSpy.count(), 1 ); + QCOMPARE( model.activeProjectId(), renamed.id() ); + QCOMPARE( activeRenameSpy.count(), 0 ); } diff --git a/core/localprojectsmanager.cpp b/core/localprojectsmanager.cpp index 7b501292b..c12bc135e 100644 --- a/core/localprojectsmanager.cpp +++ b/core/localprojectsmanager.cpp @@ -253,10 +253,8 @@ void LocalProjectsManager::addProject( const QString &projectDir, const QString emit localProjectAdded( project ); } -QString LocalProjectsManager::renameLocalProject( const QString &projectId, const QString &newName ) +QString LocalProjectsManager::validateRename( const QString &projectId, const QString &trimmedName, int *projectIndexOut ) const { - const QString trimmedName = newName.trimmed(); - if ( trimmedName.isEmpty() ) { return tr( "The project name cannot be empty" ); @@ -286,45 +284,73 @@ QString LocalProjectsManager::renameLocalProject( const QString &projectId, cons return tr( "Project not found" ); } - LocalProject &project = mProjects[projectIndex]; - - if ( project.projectName == trimmedName ) + if ( projectIndexOut ) { - return {}; // name did not change, nothing to rename + *projectIndexOut = projectIndex; } - const QString oldProjectId = project.id(); + return {}; +} + +QString LocalProjectsManager::canRenameProject( const QString &projectId, const QString &newName ) const +{ + return validateRename( projectId, newName.trimmed() ); +} - const QString parentDir = QFileInfo( project.projectDir ).dir().absolutePath(); - const QString newProjectDir = CoreUtils::findUniquePath( parentDir + "/" + trimmedName ); +void LocalProjectsManager::renameLocalProject( const QString &projectId, const QString &newName ) +{ + const QString trimmedName = newName.trimmed(); - if ( !QDir().rename( project.projectDir, newProjectDir ) ) + int projectIndex = -1; + const QString validationError = validateRename( projectId, trimmedName, &projectIndex ); + + if ( !validationError.isEmpty() ) { - CoreUtils::log( "Rename project", QStringLiteral( "Failed to rename directory %1 to %2" ).arg( project.projectDir, newProjectDir ) ); - return tr( "Failed to rename the project directory" ); + CoreUtils::log( "Rename project", validationError ); + emit renameLocalProjectFinished( false ); + return; } - if ( !project.qgisProjectFilePath.isEmpty() ) + LocalProject &project = mProjects[projectIndex]; + const QString oldProjectId = project.id(); + + if ( project.projectName != trimmedName ) { - const QString relativeFilePath = QDir( project.projectDir ).relativeFilePath( project.qgisProjectFilePath ); - const QString oldFilePath = newProjectDir + "/" + relativeFilePath; + // let listeners (e.g. ActiveProject) unload this project before it's renamed on disk + emit aboutToRenameLocalProject( oldProjectId ); - QFileInfo oldFileInfo( oldFilePath ); - const QString newFilePath = oldFileInfo.dir().absoluteFilePath( trimmedName + "." + oldFileInfo.suffix() ); + const QString parentDir = QFileInfo( project.projectDir ).dir().absolutePath(); + const QString newProjectDir = CoreUtils::findUniquePath( parentDir + "/" + trimmedName ); - if ( oldFilePath != newFilePath && QFile::rename( oldFilePath, newFilePath ) ) + if ( !QDir().rename( project.projectDir, newProjectDir ) ) { - project.qgisProjectFilePath = newFilePath; + CoreUtils::log( "Rename project", QStringLiteral( "Failed to rename directory %1 to %2" ).arg( project.projectDir, newProjectDir ) ); + emit renameLocalProjectFinished( false ); + return; } - else + + if ( !project.qgisProjectFilePath.isEmpty() ) { - project.qgisProjectFilePath = oldFilePath; + const QString relativeFilePath = QDir( project.projectDir ).relativeFilePath( project.qgisProjectFilePath ); + const QString oldFilePath = newProjectDir + "/" + relativeFilePath; + + QFileInfo oldFileInfo( oldFilePath ); + const QString newFilePath = oldFileInfo.dir().absoluteFilePath( trimmedName + "." + oldFileInfo.suffix() ); + + if ( oldFilePath != newFilePath && QFile::rename( oldFilePath, newFilePath ) ) + { + project.qgisProjectFilePath = newFilePath; + } + else + { + project.qgisProjectFilePath = oldFilePath; + } } - } - project.projectDir = newProjectDir; - project.projectName = trimmedName; + project.projectDir = newProjectDir; + project.projectName = trimmedName; + } emit localProjectRenamed( oldProjectId, project ); - return {}; + emit renameLocalProjectFinished( true ); } diff --git a/core/localprojectsmanager.h b/core/localprojectsmanager.h index e41f2b396..83f1446f0 100644 --- a/core/localprojectsmanager.h +++ b/core/localprojectsmanager.h @@ -53,9 +53,13 @@ class LocalProjectsManager : public QObject Q_INVOKABLE QString projectName( const QString &projectId ) const; + //! Returns an empty string if newName is a valid, available name for projectId, or a + //! user-facing error message otherwise. Does not touch the filesystem. + Q_INVOKABLE QString canRenameProject( const QString &projectId, const QString &newName ) const; + //! Renames the local project's directory and main QGIS project file to newName. - //! Returns an empty string on success, or a user-facing error message on failure. - QString renameLocalProject( const QString &projectId, const QString &newName ); + //! Reports the outcome via renameLocalProjectFinished(); failures are logged, not returned. + void renameLocalProject( const QString &projectId, const QString &newName ); /** * Returns changes of a project specified by projectId in the form : @@ -76,6 +80,8 @@ class LocalProjectsManager : public QObject void localProjectAdded( const LocalProject &project ); void localProjectDataChanged( const LocalProject &project ); void localProjectRenamed( const QString &oldProjectId, const LocalProject &project ); + void renameLocalProjectFinished( bool success ); + void aboutToRenameLocalProject( const QString &projectId ); void aboutToRemoveLocalProject( const LocalProject &project ); void dataDirReloaded(); @@ -83,6 +89,10 @@ class LocalProjectsManager : public QObject private: void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); + //! Shared validation for canRenameProject()/renameLocalProject(); on success, optionally + //! outputs projectId's index in mProjects via projectIndexOut. + QString validateRename( const QString &projectId, const QString &trimmedName, int *projectIndexOut = nullptr ) const; + QString mDataDir; //!< directory with all local projects LocalProjectsList mProjects; }; From f5f00ddffb76aea19a40746860fe08c687fbec52 Mon Sep 17 00:00:00 2001 From: Richard Kello Date: Thu, 1 Oct 2026 19:09:34 +0200 Subject: [PATCH 5/5] Add requested changes --- app/projectsmodel.cpp | 2 +- app/qml/project/MMProjectList.qml | 8 +- .../components/MMRenameProjectDialog.qml | 1 - app/test/testlocalprojectsmanager.cpp | 89 ++++++++----------- app/test/testlocalprojectsmanager.h | 4 +- app/test/testmodels.cpp | 10 +-- app/test/testutils.cpp | 14 +++ app/test/testutils.h | 4 + core/localprojectsmanager.cpp | 28 +++--- core/localprojectsmanager.h | 9 +- 10 files changed, 87 insertions(+), 82 deletions(-) diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 68b21ec11..55aff6d2e 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -384,7 +384,7 @@ void ProjectsModel::removeLocalProject( const QString &projectId ) QString ProjectsModel::canRenameProject( const QString &projectId, const QString &newName ) const { - return mLocalProjectsManager->canRenameProject( projectId, newName ); + return mLocalProjectsManager->validateRename( projectId, newName ); } void ProjectsModel::renameLocalProject( const QString &projectId, const QString &newName ) diff --git a/app/qml/project/MMProjectList.qml b/app/qml/project/MMProjectList.qml index 93500d4e7..93759f280 100644 --- a/app/qml/project/MMProjectList.qml +++ b/app/qml/project/MMProjectList.qml @@ -321,14 +321,12 @@ Item { sourceComponent: MMProjectComponents.MMRenameProjectDialog { id: renameDialog - projectId: renameDialogLoader.projectIdToRename - onRenameClicked: function( newName ) { - if ( !renameDialog.projectId ) { + if ( !renameDialogLoader.projectIdToRename ) { return } - controllerModel.renameLocalProject( renameDialog.projectId, newName ) + controllerModel.renameLocalProject( renameDialogLoader.projectIdToRename, newName ) } onTextEdited: function( text ) { @@ -358,7 +356,7 @@ Item { interval: 300 onTriggered: { - renameDialog.errorText = controllerModel.canRenameProject( renameDialog.projectId, pendingText ) + renameDialog.errorText = controllerModel.canRenameProject( renameDialogLoader.projectIdToRename, pendingText ) } } } diff --git a/app/qml/project/components/MMRenameProjectDialog.qml b/app/qml/project/components/MMRenameProjectDialog.qml index 5a371d227..66136170a 100644 --- a/app/qml/project/components/MMRenameProjectDialog.qml +++ b/app/qml/project/components/MMRenameProjectDialog.qml @@ -15,7 +15,6 @@ import "../../inputs" MMDrawer { id: root - property string projectId: "" property alias errorText: newNameField.errorMsg signal renameClicked( string newName ) diff --git a/app/test/testlocalprojectsmanager.cpp b/app/test/testlocalprojectsmanager.cpp index 25e6883e8..e8f3fd88d 100644 --- a/app/test/testlocalprojectsmanager.cpp +++ b/app/test/testlocalprojectsmanager.cpp @@ -9,28 +9,12 @@ #include "testlocalprojectsmanager.h" #include "localprojectsmanager.h" +#include "testutils.h" #include #include #include -namespace -{ - //! Creates a fake local project directory containing a dummy .qgz file, so - //! LocalProjectsManager::findQgisProjectFile() has something to discover. - QString createFakeProject( const QString &dataDir, const QString &name ) - { - QString projectDir = dataDir + "/" + name; - QDir().mkpath( projectDir ); - - QFile qgzFile( projectDir + "/" + name + ".qgz" ); - qgzFile.open( QIODevice::WriteOnly ); - qgzFile.close(); - - return projectDir; - } -} - void TestLocalProjectsManager::init() { mDataDir = QDir::tempPath() + QStringLiteral( "/testLocalProjectsManager" ); @@ -40,6 +24,8 @@ void TestLocalProjectsManager::init() dir.removeRecursively(); QDir().mkpath( mDataDir ); + + TestUtils::createFakeLocalProject( mDataDir, QStringLiteral( "OriginalName" ) ); } void TestLocalProjectsManager::cleanup() @@ -49,8 +35,6 @@ void TestLocalProjectsManager::cleanup() void TestLocalProjectsManager::testRenameSuccess() { - createFakeProject( mDataDir, "OriginalName" ); - LocalProjectsManager manager( mDataDir ); QCOMPARE( manager.projects().size(), 1 ); @@ -80,26 +64,29 @@ void TestLocalProjectsManager::testRenameSuccess() void TestLocalProjectsManager::testRenameEmptyName() { - createFakeProject( mDataDir, "OriginalName" ); - LocalProjectsManager manager( mDataDir ); QString projectId = manager.projects().first().id(); QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); - manager.renameLocalProject( projectId, QStringLiteral( " " ) ); + manager.renameLocalProject( projectId, QString() ); QCOMPARE( finishedSpy.count(), 1 ); QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), false ); QCOMPARE( renamedSpy.count(), 0 ); QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); + + manager.renameLocalProject( projectId, QStringLiteral( " " ) ); + + QCOMPARE( finishedSpy.count(), 2 ); + QCOMPARE( finishedSpy.at( 1 ).at( 0 ).toBool(), false ); + QCOMPARE( renamedSpy.count(), 0 ); + QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); } void TestLocalProjectsManager::testRenameInvalidCharacters() { - createFakeProject( mDataDir, "OriginalName" ); - LocalProjectsManager manager( mDataDir ); QString projectId = manager.projects().first().id(); @@ -116,8 +103,7 @@ void TestLocalProjectsManager::testRenameInvalidCharacters() void TestLocalProjectsManager::testRenameNameAlreadyTaken() { - createFakeProject( mDataDir, "ProjectA" ); - createFakeProject( mDataDir, "ProjectB" ); + TestUtils::createFakeLocalProject( mDataDir, QStringLiteral( "OtherProject" ) ); LocalProjectsManager manager( mDataDir ); QCOMPARE( manager.projects().size(), 2 ); @@ -125,19 +111,17 @@ void TestLocalProjectsManager::testRenameNameAlreadyTaken() QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); - manager.renameLocalProject( QStringLiteral( "ProjectA" ), QStringLiteral( "ProjectB" ) ); + manager.renameLocalProject( QStringLiteral( "OriginalName" ), QStringLiteral( "OtherProject" ) ); QCOMPARE( finishedSpy.count(), 1 ); QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), false ); QCOMPARE( renamedSpy.count(), 0 ); - QVERIFY( QDir( mDataDir + "/ProjectA" ).exists() ); - QVERIFY( QDir( mDataDir + "/ProjectB" ).exists() ); + QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); + QVERIFY( QDir( mDataDir + "/OtherProject" ).exists() ); } void TestLocalProjectsManager::testRenameSameName() { - createFakeProject( mDataDir, "OriginalName" ); - LocalProjectsManager manager( mDataDir ); QString projectId = manager.projects().first().id(); @@ -156,7 +140,6 @@ void TestLocalProjectsManager::testRenameSameName() void TestLocalProjectsManager::testRenameUnknownProject() { LocalProjectsManager manager( mDataDir ); - QCOMPARE( manager.projects().size(), 0 ); QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); @@ -166,12 +149,12 @@ void TestLocalProjectsManager::testRenameUnknownProject() QCOMPARE( finishedSpy.count(), 1 ); QCOMPARE( finishedSpy.at( 0 ).at( 0 ).toBool(), false ); QCOMPARE( renamedSpy.count(), 0 ); + QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); + QVERIFY( !QDir( mDataDir + "/NewName" ).exists() ); } void TestLocalProjectsManager::testRenameDirectoryCollision() { - createFakeProject( mDataDir, "OriginalName" ); - LocalProjectsManager manager( mDataDir ); QString projectId = manager.projects().first().id(); @@ -196,8 +179,6 @@ void TestLocalProjectsManager::testRenameDirectoryCollision() void TestLocalProjectsManager::testRenameTrimsWhitespace() { - createFakeProject( mDataDir, "OriginalName" ); - LocalProjectsManager manager( mDataDir ); QString projectId = manager.projects().first().id(); @@ -216,36 +197,42 @@ void TestLocalProjectsManager::testRenameTrimsWhitespace() QCOMPARE( updated.projectName, QStringLiteral( "NewName" ) ); } -void TestLocalProjectsManager::testCanRenameProjectAccepts() +void TestLocalProjectsManager::testValidateRenameAccepts() { - createFakeProject( mDataDir, "OriginalName" ); - LocalProjectsManager manager( mDataDir ); QString projectId = manager.projects().first().id(); QSignalSpy renamedSpy( &manager, &LocalProjectsManager::localProjectRenamed ); QSignalSpy finishedSpy( &manager, &LocalProjectsManager::renameLocalProjectFinished ); - QCOMPARE( manager.canRenameProject( projectId, QStringLiteral( "NewName" ) ), QString() ); - QCOMPARE( manager.canRenameProject( projectId, QStringLiteral( "OriginalName" ) ), QString() ); // unchanged name is fine too + QCOMPARE( manager.validateRename( projectId, QStringLiteral( "NewName" ) ), QString() ); + QCOMPARE( manager.validateRename( projectId, QStringLiteral( "OriginalName" ) ), QString() ); // unchanged name is fine too - // canRenameProject() must be side-effect-free + // validateRename() must be side-effect-free QVERIFY( QDir( mDataDir + "/OriginalName" ).exists() ); - QCOMPARE( manager.projectFromProjectId( projectId ).projectName, QStringLiteral( "OriginalName" ) ); + QVERIFY( QFile::exists( mDataDir + "/OriginalName/OriginalName.qgz" ) ); + QVERIFY( !QDir( mDataDir + "/NewName" ).exists() ); + + const LocalProject project = manager.projectFromProjectId( projectId ); + QVERIFY( project.isValid() ); + QCOMPARE( project.projectName, QStringLiteral( "OriginalName" ) ); + QCOMPARE( project.projectDir, mDataDir + "/OriginalName" ); + QCOMPARE( project.qgisProjectFilePath, mDataDir + "/OriginalName/OriginalName.qgz" ); + QCOMPARE( renamedSpy.count(), 0 ); QCOMPARE( finishedSpy.count(), 0 ); } -void TestLocalProjectsManager::testCanRenameProjectRejects() +void TestLocalProjectsManager::testValidateRenameRejects() { - createFakeProject( mDataDir, "ProjectA" ); - createFakeProject( mDataDir, "ProjectB" ); + TestUtils::createFakeLocalProject( mDataDir, QStringLiteral( "OtherProject" ) ); LocalProjectsManager manager( mDataDir ); - QString projectId = manager.projectFromDirectory( mDataDir + "/ProjectA" ).id(); + QString projectId = manager.projectFromDirectory( mDataDir + "/OriginalName" ).id(); - QVERIFY( !manager.canRenameProject( projectId, QStringLiteral( " " ) ).isEmpty() ); - QVERIFY( !manager.canRenameProject( projectId, QStringLiteral( "Bad/Name" ) ).isEmpty() ); - QVERIFY( !manager.canRenameProject( projectId, QStringLiteral( "ProjectB" ) ).isEmpty() ); - QVERIFY( !manager.canRenameProject( QStringLiteral( "does-not-exist" ), QStringLiteral( "NewName" ) ).isEmpty() ); + QVERIFY( !manager.validateRename( projectId, QString() ).isEmpty() ); + QVERIFY( !manager.validateRename( projectId, QStringLiteral( " " ) ).isEmpty() ); + QVERIFY( !manager.validateRename( projectId, QStringLiteral( "Bad/Name" ) ).isEmpty() ); + QVERIFY( !manager.validateRename( projectId, QStringLiteral( "OtherProject" ) ).isEmpty() ); + QVERIFY( !manager.validateRename( QStringLiteral( "does-not-exist" ), QStringLiteral( "NewName" ) ).isEmpty() ); } diff --git a/app/test/testlocalprojectsmanager.h b/app/test/testlocalprojectsmanager.h index 446a21ca5..d00198cde 100644 --- a/app/test/testlocalprojectsmanager.h +++ b/app/test/testlocalprojectsmanager.h @@ -29,8 +29,8 @@ class TestLocalProjectsManager : public QObject void testRenameDirectoryCollision(); void testRenameTrimsWhitespace(); - void testCanRenameProjectAccepts(); - void testCanRenameProjectRejects(); + void testValidateRenameAccepts(); + void testValidateRenameRejects(); private: QString mDataDir; diff --git a/app/test/testmodels.cpp b/app/test/testmodels.cpp index 858cfcef0..b669cb332 100644 --- a/app/test/testmodels.cpp +++ b/app/test/testmodels.cpp @@ -534,7 +534,7 @@ void TestModels::testProjectsModelOnProjectRenamed() // exactly like LocalProjectsManager::renameLocalProject() produces after renaming on disk. Project p0; p0.local.projectName = QStringLiteral( "OriginalName" ); - p0.local.projectDir = QStringLiteral( "/tmp/OriginalName" ); + p0.local.projectDir = QDir::tempPath() + QStringLiteral( "/OriginalName" ); Project p1; p1.local.projectNamespace = QStringLiteral( "namespace" ); @@ -551,7 +551,7 @@ void TestModels::testProjectsModelOnProjectRenamed() LocalProject renamed = p0.local; renamed.projectName = QStringLiteral( "NewName" ); - renamed.projectDir = QStringLiteral( "/tmp/NewName" ); + renamed.projectDir = QDir::tempPath() + QStringLiteral( "/NewName" ); QVERIFY( oldId != renamed.id() ); // renaming a local-only project does change its id @@ -582,7 +582,7 @@ void TestModels::testProjectsModelOnProjectRenamedActiveProject() { Project p0; p0.local.projectName = QStringLiteral( "OriginalName" ); - p0.local.projectDir = QStringLiteral( "/tmp/OriginalName" ); + p0.local.projectDir = QDir::tempPath() + QStringLiteral( "/OriginalName" ); Project p1; p1.local.projectNamespace = QStringLiteral( "namespace" ); @@ -597,7 +597,7 @@ void TestModels::testProjectsModelOnProjectRenamedActiveProject() LocalProject renamed = p0.local; renamed.projectName = QStringLiteral( "NewName" ); - renamed.projectDir = QStringLiteral( "/tmp/NewName" ); + renamed.projectDir = QDir::tempPath() + QStringLiteral( "/NewName" ); // Renaming a project that is NOT the active one must leave activeProjectId untouched model.setActiveProjectId( p1.id() ); @@ -614,7 +614,7 @@ void TestModels::testProjectsModelOnProjectRenamedActiveProject() LocalProject renamedAgain = renamed; renamedAgain.projectName = QStringLiteral( "NewerName" ); - renamedAgain.projectDir = QStringLiteral( "/tmp/NewerName" ); + renamedAgain.projectDir = QDir::tempPath() + QStringLiteral( "/NewerName" ); model.onProjectRenamed( renamed.id(), renamedAgain ); diff --git a/app/test/testutils.cpp b/app/test/testutils.cpp index be15db547..be15ae494 100644 --- a/app/test/testutils.cpp +++ b/app/test/testutils.cpp @@ -8,6 +8,8 @@ ***************************************************************************/ #include "QtDebug" +#include +#include #include #include #include @@ -251,6 +253,18 @@ bool TestUtils::generateProjectFolder( const QString &rootPath, const QJsonDocum return allGood; } +QString TestUtils::createFakeLocalProject( const QString &dataDir, const QString &name ) +{ + const QString projectDir = dataDir + "/" + name; + QDir().mkpath( projectDir ); + + QFile qgzFile( projectDir + "/" + name + ".qgz" ); + qgzFile.open( QIODevice::WriteOnly ); + qgzFile.close(); + + return projectDir; +} + QgsProject *TestUtils::loadPlanesTestProject() { QString projectDir = TestUtils::testDataDir() + "/planes"; diff --git a/app/test/testutils.h b/app/test/testutils.h index 3de1e286b..15bfebc88 100644 --- a/app/test/testutils.h +++ b/app/test/testutils.h @@ -73,6 +73,10 @@ namespace TestUtils */ bool generateProjectFolder( const QString &rootPath, const QJsonDocument &structure ); + //! Creates a fake local project directory dataDir/name containing an empty name.qgz file, + //! so LocalProjectsManager can discover it. Returns the project directory path. + QString createFakeLocalProject( const QString &dataDir, const QString &name ); + bool testExifPositionMetadataExists( const QString &imageSource ); //! Creates an in-memory layer with a single field of the given type and registers it in QgsProject::instance() diff --git a/core/localprojectsmanager.cpp b/core/localprojectsmanager.cpp index c12bc135e..d9a9b53d3 100644 --- a/core/localprojectsmanager.cpp +++ b/core/localprojectsmanager.cpp @@ -253,8 +253,16 @@ void LocalProjectsManager::addProject( const QString &projectDir, const QString emit localProjectAdded( project ); } -QString LocalProjectsManager::validateRename( const QString &projectId, const QString &trimmedName, int *projectIndexOut ) const +QString LocalProjectsManager::validateRename( const QString &projectId, const QString &newName ) const { + int projectIndex = -1; + return validateRename( projectId, newName, projectIndex ); +} + +QString LocalProjectsManager::validateRename( const QString &projectId, const QString &newName, int &projectIndexOut ) const +{ + const QString trimmedName = newName.trimmed(); + if ( trimmedName.isEmpty() ) { return tr( "The project name cannot be empty" ); @@ -275,7 +283,7 @@ QString LocalProjectsManager::validateRename( const QString &projectId, const QS if ( i != projectIndex && mProjects[i].projectName == trimmedName ) { - return tr( "A project name is already taken" ); + return tr( "The project name is already taken" ); } } @@ -284,25 +292,17 @@ QString LocalProjectsManager::validateRename( const QString &projectId, const QS return tr( "Project not found" ); } - if ( projectIndexOut ) - { - *projectIndexOut = projectIndex; - } + projectIndexOut = projectIndex; return {}; } -QString LocalProjectsManager::canRenameProject( const QString &projectId, const QString &newName ) const -{ - return validateRename( projectId, newName.trimmed() ); -} - void LocalProjectsManager::renameLocalProject( const QString &projectId, const QString &newName ) { const QString trimmedName = newName.trimmed(); int projectIndex = -1; - const QString validationError = validateRename( projectId, trimmedName, &projectIndex ); + const QString validationError = validateRename( projectId, trimmedName, projectIndex ); if ( !validationError.isEmpty() ) { @@ -322,6 +322,8 @@ void LocalProjectsManager::renameLocalProject( const QString &projectId, const Q const QString parentDir = QFileInfo( project.projectDir ).dir().absolutePath(); const QString newProjectDir = CoreUtils::findUniquePath( parentDir + "/" + trimmedName ); + // rename the directory first - if this fails, nothing has been changed yet, so we keep the + // project as it is and just report the failure if ( !QDir().rename( project.projectDir, newProjectDir ) ) { CoreUtils::log( "Rename project", QStringLiteral( "Failed to rename directory %1 to %2" ).arg( project.projectDir, newProjectDir ) ); @@ -329,6 +331,8 @@ void LocalProjectsManager::renameLocalProject( const QString &projectId, const Q return; } + // the QGIS project file now lives in the renamed directory, so rename it to match the new name too; + // this is best-effort - if it fails, the project still works, we just keep pointing to the old file name if ( !project.qgisProjectFilePath.isEmpty() ) { const QString relativeFilePath = QDir( project.projectDir ).relativeFilePath( project.qgisProjectFilePath ); diff --git a/core/localprojectsmanager.h b/core/localprojectsmanager.h index 83f1446f0..b272c9ce5 100644 --- a/core/localprojectsmanager.h +++ b/core/localprojectsmanager.h @@ -53,9 +53,9 @@ class LocalProjectsManager : public QObject Q_INVOKABLE QString projectName( const QString &projectId ) const; - //! Returns an empty string if newName is a valid, available name for projectId, or a + //! Returns an empty string if newName (trimmed) is a valid, available name for projectId, or a //! user-facing error message otherwise. Does not touch the filesystem. - Q_INVOKABLE QString canRenameProject( const QString &projectId, const QString &newName ) const; + QString validateRename( const QString &projectId, const QString &newName ) const; //! Renames the local project's directory and main QGIS project file to newName. //! Reports the outcome via renameLocalProjectFinished(); failures are logged, not returned. @@ -89,9 +89,8 @@ class LocalProjectsManager : public QObject private: void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); - //! Shared validation for canRenameProject()/renameLocalProject(); on success, optionally - //! outputs projectId's index in mProjects via projectIndexOut. - QString validateRename( const QString &projectId, const QString &trimmedName, int *projectIndexOut = nullptr ) const; + //! Same as the public validateRename(), but on success also outputs projectId's index in mProjects + QString validateRename( const QString &projectId, const QString &newName, int &projectIndexOut ) const; QString mDataDir; //!< directory with all local projects LocalProjectsList mProjects;