diff --git a/src/application/MainWindow.cpp b/src/application/MainWindow.cpp index c7de3f6..0d91c19 100644 --- a/src/application/MainWindow.cpp +++ b/src/application/MainWindow.cpp @@ -58,6 +58,31 @@ void MainWindow::showProjectNavigator() { navigator->setOpenProjectHandler([this](const ProjectMetadata& metadata) { openProject(metadata); }); + navigator->setDeleteProjectHandler([this](const ProjectMetadata& metadata) { + if (metadata.isBuiltInDemo()) { + QMessageBox::information(this, "Delete Project", "The built-in demo project cannot be deleted."); + return; + } + + const auto choice = QMessageBox::question( + this, + "Delete Project", + QString("Delete \"%1\" and its project folder?\n\n%2") + .arg(metadata.name, metadata.folderPath), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if (choice != QMessageBox::Yes) { + return; + } + + QString errorMessage; + if (!ProjectPersistence::deleteProject(metadata, &errorMessage)) { + QMessageBox::warning(this, "Delete Project", errorMessage); + return; + } + + showProjectNavigator(); + }); setCentralWidget(navigator); } diff --git a/src/application/ProjectListWidget.cpp b/src/application/ProjectListWidget.cpp index 6e45305..d093e4e 100644 --- a/src/application/ProjectListWidget.cpp +++ b/src/application/ProjectListWidget.cpp @@ -1,9 +1,12 @@ #include "application/ProjectListWidget.h" #include +#include #include #include #include +#include +#include #include #include #include @@ -21,6 +24,23 @@ QString displaySavedAt(const QString& savedAt) { return QLocale().toString(dateTime, "yyyy-MM-dd AP h:mm"); } +QIcon makeTrashIcon(const QColor& color) { + QPixmap pixmap(32, 32); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(QPen(color, 2.2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); + painter.setBrush(Qt::NoBrush); + painter.drawLine(QPointF(10, 11), QPointF(22, 11)); + painter.drawLine(QPointF(14, 8), QPointF(18, 8)); + painter.drawLine(QPointF(13, 8), QPointF(19, 8)); + painter.drawRoundedRect(QRectF(11, 13, 10, 13), 2, 2); + painter.drawLine(QPointF(14, 16), QPointF(14, 23)); + painter.drawLine(QPointF(18, 16), QPointF(18, 23)); + return QIcon(pixmap); +} + } // namespace ProjectListWidget::ProjectListWidget(const QList& projects, QWidget* parent) @@ -58,38 +78,67 @@ void ProjectListWidget::setOpenProjectHandler(std::function handler) { + deleteProjectHandler_ = std::move(handler); +} + void ProjectListWidget::addProjectRow(const ProjectMetadata& project) { - auto* row = new QPushButton(this); + auto* row = new QWidget(this); row->setMinimumHeight(72); row->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - row->setCursor(Qt::PointingHandCursor); - row->setStyleSheet(ui::ghostRowStyleSheet()); auto* layout = new QHBoxLayout(row); layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(16); + layout->setSpacing(8); + + auto* openButton = new QPushButton(row); + openButton->setMinimumHeight(64); + openButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + openButton->setCursor(Qt::PointingHandCursor); + openButton->setStyleSheet(ui::ghostRowStyleSheet()); - auto* nameLabel = new QLabel(project.name, row); + auto* openLayout = new QHBoxLayout(openButton); + openLayout->setContentsMargins(0, 0, 0, 0); + openLayout->setSpacing(16); + + auto* nameLabel = new QLabel(project.name, openButton); nameLabel->setFont(ui::font(ui::FontRole::Body)); nameLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents); - auto* dateLabel = new QLabel(displaySavedAt(project.savedAt), row); + auto* dateLabel = new QLabel(displaySavedAt(project.savedAt), openButton); dateLabel->setFont(ui::font(ui::FontRole::Caption)); dateLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); dateLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); dateLabel->setAttribute(Qt::WA_TransparentForMouseEvents); dateLabel->setStyleSheet(ui::subtleTextStyleSheet()); - layout->addWidget(nameLabel, 1); - layout->addWidget(dateLabel, 0); - - connect(row, &QPushButton::clicked, this, [this, project]() { + openLayout->addWidget(nameLabel, 1); + openLayout->addWidget(dateLabel, 0); + layout->addWidget(openButton, 1); + + auto* deleteButton = new QPushButton(row); + deleteButton->setIcon(makeTrashIcon(project.isBuiltInDemo() ? QColor("#9aa8b6") : QColor("#b42318"))); + deleteButton->setIconSize(QSize(24, 24)); + deleteButton->setToolTip(project.isBuiltInDemo() ? "Demo project cannot be deleted" : "Delete project"); + deleteButton->setAccessibleName(deleteButton->toolTip()); + deleteButton->setFixedSize(42, 42); + deleteButton->setEnabled(!project.isBuiltInDemo()); + deleteButton->setStyleSheet(ui::secondaryButtonStyleSheet()); + layout->addWidget(deleteButton, 0, Qt::AlignVCenter); + + connect(openButton, &QPushButton::clicked, this, [this, project]() { if (openProjectHandler_) { openProjectHandler_(project); } }); + connect(deleteButton, &QPushButton::clicked, this, [this, project]() { + if (deleteProjectHandler_) { + deleteProjectHandler_(project); + } + }); + if (auto* parentLayout = qobject_cast(this->layout())) { parentLayout->addWidget(row); } diff --git a/src/application/ProjectListWidget.h b/src/application/ProjectListWidget.h index 2a3a2ec..3923485 100644 --- a/src/application/ProjectListWidget.h +++ b/src/application/ProjectListWidget.h @@ -15,11 +15,13 @@ class ProjectListWidget : public QFrame { explicit ProjectListWidget(const QList& projects, QWidget* parent = nullptr); void setOpenProjectHandler(std::function handler); + void setDeleteProjectHandler(std::function handler); private: void addProjectRow(const ProjectMetadata& project); std::function openProjectHandler_{}; + std::function deleteProjectHandler_{}; }; } // namespace safecrowd::application diff --git a/src/application/ProjectNavigatorWidget.cpp b/src/application/ProjectNavigatorWidget.cpp index 72c5498..4238b1c 100644 --- a/src/application/ProjectNavigatorWidget.cpp +++ b/src/application/ProjectNavigatorWidget.cpp @@ -49,4 +49,8 @@ void ProjectNavigatorWidget::setOpenProjectHandler(std::functionsetOpenProjectHandler(std::move(handler)); } +void ProjectNavigatorWidget::setDeleteProjectHandler(std::function handler) { + projectList_->setDeleteProjectHandler(std::move(handler)); +} + } // namespace safecrowd::application diff --git a/src/application/ProjectNavigatorWidget.h b/src/application/ProjectNavigatorWidget.h index d164510..91b6e08 100644 --- a/src/application/ProjectNavigatorWidget.h +++ b/src/application/ProjectNavigatorWidget.h @@ -18,6 +18,7 @@ class ProjectNavigatorWidget : public QWidget { void setNewProjectHandler(std::function handler); void setOpenProjectHandler(std::function handler); + void setDeleteProjectHandler(std::function handler); private: ProjectNavigatorActions* actions_{nullptr}; diff --git a/src/application/ProjectPersistence.cpp b/src/application/ProjectPersistence.cpp index 3ebdb2d..afd2aa8 100644 --- a/src/application/ProjectPersistence.cpp +++ b/src/application/ProjectPersistence.cpp @@ -99,6 +99,29 @@ void upsertRecentProject(const ProjectMetadata& metadata) { writeJsonDocument(recentPath, QJsonDocument(root), &ignoredError); } +void removeRecentProject(const QString& folderPath) { + const auto recentPath = recentProjectsPath(); + const auto document = readJsonDocument(recentPath); + if (!document.isObject()) { + return; + } + + QJsonArray updated; + const auto normalizedFolder = QDir(folderPath).absolutePath(); + for (const auto& value : document.object().value("projects").toArray()) { + const auto existing = fromJson(value.toObject()); + if (QDir(existing.folderPath).absolutePath() == normalizedFolder) { + continue; + } + updated.append(value); + } + + QJsonObject root; + root["projects"] = updated; + QString ignoredError; + writeJsonDocument(recentPath, QJsonDocument(root), &ignoredError); +} + bool copyLayoutIntoProject(ProjectMetadata& metadata, QString* errorMessage) { const auto sourcePath = QFileInfo(metadata.layoutPath).absoluteFilePath(); const auto targetPath = QDir(metadata.folderPath).filePath(kLayoutFileName); @@ -447,6 +470,47 @@ ProjectMetadata ProjectPersistence::loadProject(const QString& folderPath) { return fromJson(document.object()); } +bool ProjectPersistence::deleteProject(const ProjectMetadata& metadata, QString* errorMessage) { + if (metadata.isBuiltInDemo()) { + if (errorMessage != nullptr) { + *errorMessage = "Built-in demo projects cannot be deleted."; + } + return false; + } + + if (metadata.folderPath.isEmpty()) { + if (errorMessage != nullptr) { + *errorMessage = "Project folder is missing."; + } + return false; + } + + const auto projectFile = projectFilePath(metadata.folderPath); + if (!QFileInfo::exists(projectFile)) { + removeRecentProject(metadata.folderPath); + return true; + } + + const auto loaded = loadProject(metadata.folderPath); + if (!loaded.isValid()) { + if (errorMessage != nullptr) { + *errorMessage = "The selected folder does not contain a valid SafeCrowd project."; + } + return false; + } + + QDir folder(metadata.folderPath); + if (!folder.removeRecursively()) { + if (errorMessage != nullptr) { + *errorMessage = QString("Failed to delete project folder: %1").arg(metadata.folderPath); + } + return false; + } + + removeRecentProject(metadata.folderPath); + return true; +} + bool ProjectPersistence::loadProjectReview(const ProjectMetadata& metadata, safecrowd::domain::ImportResult* importResult) { if (metadata.isBuiltInDemo() || importResult == nullptr) { return false; diff --git a/src/application/ProjectPersistence.h b/src/application/ProjectPersistence.h index a794cee..de2c7d3 100644 --- a/src/application/ProjectPersistence.h +++ b/src/application/ProjectPersistence.h @@ -11,6 +11,7 @@ class ProjectPersistence { public: static QList loadRecentProjects(); static ProjectMetadata loadProject(const QString& folderPath); + static bool deleteProject(const ProjectMetadata& metadata, QString* errorMessage = nullptr); static bool loadProjectReview(const ProjectMetadata& metadata, safecrowd::domain::ImportResult* importResult); static bool saveProject(ProjectMetadata metadata, QString* errorMessage = nullptr); static bool saveProjectReview(