diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 26a58519a..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(); @@ -380,6 +381,37 @@ 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::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 900bec57e..691387940 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 ); @@ -170,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 cffff9b66..188bfcaa4 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -183,6 +183,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 7636434b9..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 @@ -139,7 +140,7 @@ Item { return ["changes", "remove"] } else if ( !model.ProjectIsMergin && model.ProjectIsLocal ) { - return ["upload", "remove"] + return ["upload", "remove", "rename"] } return ["download"] } @@ -176,6 +177,10 @@ Item { } onStopSyncRequested: controllerModel.stopProjectSync( projectId ) onShowChangesRequested: root.showLocalChangesRequested( projectId ) + onRenameRequested: { + root.projectIdToRename = projectId + renameDialog.open() + } } } @@ -306,6 +311,25 @@ Item { } } + MMProjectComponents.MMRenameProjectDialog { + id: renameDialog + + onRenameClicked: function( newName ) { + if ( !root.projectIdToRename ) { + return + } + + const renameResult = controllerModel.renameLocalProject( root.projectIdToRename, newName ) + + if ( !renameResult ) { + renameDialog.close() + } + else { + renameDialog.errorText = 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/app/qml/project/components/MMRenameProjectDialog.qml b/app/qml/project/components/MMRenameProjectDialog.qml new file mode 100644 index 000000000..ff37b8974 --- /dev/null +++ b/app/qml/project/components/MMRenameProjectDialog.qml @@ -0,0 +1,88 @@ +/*************************************************************************** + * * + * 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 errorText: "" + + signal renameClicked( string newName ) + + drawerHeader.title: qsTr( "Rename project" ) + drawerHeader.titleFont: __style.t2 + + onOpened: { + root.errorText = "" + newNameField.text = "" + } + + drawerContent: Column { + width: parent.width + spacing: 0 + + 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 + + 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 + } + } + } + + MMButton { + width: parent.width + + text: qsTr( "Confirm" ) + + onClicked: { + root.renameClicked( newNameField.text ) + } + } + } +} \ No newline at end of file diff --git a/core/localprojectsmanager.cpp b/core/localprojectsmanager.cpp index f3d134fc8..7b501292b 100644 --- a/core/localprojectsmanager.cpp +++ b/core/localprojectsmanager.cpp @@ -15,6 +15,8 @@ #include #include +#include +#include LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) : mDataDir( dataDir ) @@ -25,6 +27,7 @@ LocalProjectsManager::LocalProjectsManager( const QString &dataDir ) void LocalProjectsManager::reloadDataDir() { mProjects.clear(); + QStringList entryList = QDir( mDataDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); for ( const QString &folderName : entryList ) { @@ -249,3 +252,79 @@ void LocalProjectsManager::addProject( const QString &projectDir, const QString mProjects << project; emit localProjectAdded( project ); } + +QString LocalProjectsManager::renameLocalProject( const QString &projectId, const QString &newName ) +{ + const QString trimmedName = newName.trimmed(); + + if ( trimmedName.isEmpty() ) + { + return tr( "The project name cannot be empty" ); + } + + if ( !CoreUtils::isValidName( trimmedName ) ) + { + 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 == trimmedName ) + { + return tr( "A project name is already taken" ); + } + } + + if ( projectIndex == -1 ) + { + return tr( "Project not found" ); + } + + 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 f90290861..ca02b0054 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; + 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) @@ -71,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/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