Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/application/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
69 changes: 59 additions & 10 deletions src/application/ProjectListWidget.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#include "application/ProjectListWidget.h"

#include <QDateTime>
#include <QIcon>
#include <QHBoxLayout>
#include <QLabel>
#include <QLocale>
#include <QPainter>
#include <QPixmap>
#include <QPushButton>
#include <QSizePolicy>
#include <QVBoxLayout>
Expand All @@ -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<ProjectMetadata>& projects, QWidget* parent)
Expand Down Expand Up @@ -58,38 +78,67 @@ void ProjectListWidget::setOpenProjectHandler(std::function<void(const ProjectMe
openProjectHandler_ = std::move(handler);
}

void ProjectListWidget::setDeleteProjectHandler(std::function<void(const ProjectMetadata&)> 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<QVBoxLayout*>(this->layout())) {
parentLayout->addWidget(row);
}
Expand Down
2 changes: 2 additions & 0 deletions src/application/ProjectListWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ class ProjectListWidget : public QFrame {
explicit ProjectListWidget(const QList<ProjectMetadata>& projects, QWidget* parent = nullptr);

void setOpenProjectHandler(std::function<void(const ProjectMetadata&)> handler);
void setDeleteProjectHandler(std::function<void(const ProjectMetadata&)> handler);

private:
void addProjectRow(const ProjectMetadata& project);

std::function<void(const ProjectMetadata&)> openProjectHandler_{};
std::function<void(const ProjectMetadata&)> deleteProjectHandler_{};
};

} // namespace safecrowd::application
4 changes: 4 additions & 0 deletions src/application/ProjectNavigatorWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@ void ProjectNavigatorWidget::setOpenProjectHandler(std::function<void(const Proj
projectList_->setOpenProjectHandler(std::move(handler));
}

void ProjectNavigatorWidget::setDeleteProjectHandler(std::function<void(const ProjectMetadata&)> handler) {
projectList_->setDeleteProjectHandler(std::move(handler));
}

} // namespace safecrowd::application
1 change: 1 addition & 0 deletions src/application/ProjectNavigatorWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ProjectNavigatorWidget : public QWidget {

void setNewProjectHandler(std::function<void()> handler);
void setOpenProjectHandler(std::function<void(const ProjectMetadata&)> handler);
void setDeleteProjectHandler(std::function<void(const ProjectMetadata&)> handler);

private:
ProjectNavigatorActions* actions_{nullptr};
Expand Down
64 changes: 64 additions & 0 deletions src/application/ProjectPersistence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/application/ProjectPersistence.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class ProjectPersistence {
public:
static QList<ProjectMetadata> 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(
Expand Down
Loading