diff --git a/Ember.pro b/Ember.pro index 625d71a..64985e2 100644 --- a/Ember.pro +++ b/Ember.pro @@ -31,7 +31,8 @@ SOURCES += \ model/project.cpp \ model/projectitem.cpp \ storage/appdatastorage.cpp \ - business/projectmanager.cpp + business/projectmanager.cpp \ + ui/welcome/recentprojectwidget.cpp HEADERS += \ ui/welcome/welcomedialog.h \ @@ -41,11 +42,13 @@ HEADERS += \ model/projectitem.h \ properties/config.h \ storage/appdatastorage.h \ - business/projectmanager.h + business/projectmanager.h \ + ui/welcome/recentprojectwidget.h FORMS += \ ui/welcome/welcomedialog.ui \ - ui/dialogs/newprojectdialog.ui + ui/dialogs/newprojectdialog.ui \ + ui/welcome/recentprojectwidget.ui RESOURCES += \ resources/appresources.qrc @@ -53,3 +56,5 @@ RESOURCES += \ LIBS += -lboost_filesystem LIBS += -lboost_system LIBS += -lpugixml + +CONFIG += c++14 diff --git a/business/projectmanager.cpp b/business/projectmanager.cpp index e02a034..f7e403c 100644 --- a/business/projectmanager.cpp +++ b/business/projectmanager.cpp @@ -1,11 +1,63 @@ #include "projectmanager.h" +#include namespace Ember { ProjectManager::ProjectManager() + : m_currentProject(nullptr), + m_recentProjects(), + m_recentProjectsLoaded(false) { - } +const ProjectManager::RecentProjectMap &ProjectManager::recentProjects() +{ + if (!m_recentProjectsLoaded) + { + std::vector projects; + AppDataStorage::readRecentProjects(projects); + + for (RecentProject& project : projects) + m_recentProjects.emplace(project.path, project); + + m_recentProjectsLoaded = true; + } + + return m_recentProjects; +} + +void ProjectManager::pinRecentProject(boost::filesystem::path path, bool pinned) +{ + auto it = m_recentProjects.find(path); + if (it != m_recentProjects.end()) + { + if (it->second.pinned != pinned) + { + it->second.pinned = pinned; + saveRecentProjects(); + } + } +} + +void ProjectManager::deleteRecentProject(boost::filesystem::path path) +{ + auto it = m_recentProjects.find(path); + if (it != m_recentProjects.end()) + { + m_recentProjects.erase(it); + saveRecentProjects(); + } +} + +void ProjectManager::saveRecentProjects() +{ + std::vector projects; + for (auto pair : m_recentProjects) + projects.push_back(pair.second); + + AppDataStorage::storeRecentProjects(projects); +} + + } diff --git a/business/projectmanager.h b/business/projectmanager.h index 8e3ba80..92aedf5 100644 --- a/business/projectmanager.h +++ b/business/projectmanager.h @@ -1,13 +1,49 @@ #ifndef PROJECTMANAGER_H #define PROJECTMANAGER_H +#include +#include +#include +#include + namespace Ember { class ProjectManager { public: + typedef std::map RecentProjectMap; + + /** + * @brief Constructor + */ ProjectManager(); + + /** + * @brief Gets a list of recent projects. First time reads from disk. + * @return Map of recent projects. + */ + const RecentProjectMap& recentProjects(); + + /** + * @brief Pins or unpins a recent project. Updates storage as well. + * @param path Path + * @param pinned True to pin, false to unpin + */ + void pinRecentProject(boost::filesystem::path path, bool pinned = true); + + /** + * @brief Deletes a recent project. + * @param path + */ + void deleteRecentProject(boost::filesystem::path path); + +private: + void saveRecentProjects(); + + Project* m_currentProject; + RecentProjectMap m_recentProjects; + bool m_recentProjectsLoaded; }; } diff --git a/main.cpp b/main.cpp index 1562d05..a21f464 100644 --- a/main.cpp +++ b/main.cpp @@ -1,5 +1,7 @@ #include -#include "ui/mainwindow.h" +#include +#include +#include #include int main(int argc, char *argv[]) @@ -9,7 +11,11 @@ int main(int argc, char *argv[]) a.setApplicationDisplayName(APP_NAME); a.setApplicationVersion(APP_VERSION); - MainWindow w; + Ember::AppDataStorage::initialize(); + + Ember::ProjectManager projectManager; + + Ember::MainWindow w(&projectManager); w.show(); return a.exec(); diff --git a/model/projectitem.cpp b/model/projectitem.cpp index bf492ca..05d7a6a 100644 --- a/model/projectitem.cpp +++ b/model/projectitem.cpp @@ -19,6 +19,11 @@ ProjectItem::ProjectItem(boost::filesystem::path path, ProjectItem* parent, Proj { } +ProjectItem::~ProjectItem() +{ + +} + std::string ProjectItem::name() const { return m_path.filename().string(); diff --git a/model/projectitem.h b/model/projectitem.h index e74392f..daa351e 100644 --- a/model/projectitem.h +++ b/model/projectitem.h @@ -33,6 +33,11 @@ public: */ ProjectItem(boost::filesystem::path path, ProjectItem* parent, Project* project); + /** + * @brief Destructor + */ + virtual ~ProjectItem(); + // Getters /** * @brief Gets the name of the item diff --git a/storage/appdatastorage.cpp b/storage/appdatastorage.cpp index ad3a7c7..81b5c0c 100644 --- a/storage/appdatastorage.cpp +++ b/storage/appdatastorage.cpp @@ -8,20 +8,23 @@ namespace Ember { -AppDataStorage::AppDataStorage() +boost::filesystem::path AppDataStorage::s_appData; +boost::filesystem::path AppDataStorage::s_recentProjects; + +void AppDataStorage::initialize() { QString appData = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); - m_appData = appData.toStdString(); - m_recentProjects = m_appData; - m_recentProjects += RECENT_PROJECTS_FILENAME; + s_appData = appData.toStdString(); + s_recentProjects = s_appData; + s_recentProjects += RECENT_PROJECTS_FILENAME; } void AppDataStorage::readRecentProjects(std::vector &projects) { - if (boost::filesystem::exists(m_recentProjects)) + if (boost::filesystem::exists(s_recentProjects)) { pugi::xml_document doc; - doc.load_file(m_recentProjects.c_str()); + doc.load_file(s_recentProjects.c_str()); for (auto& node : doc.document_element()) { @@ -54,8 +57,8 @@ void AppDataStorage::storeRecentProjects(const std::vector &proje } // Save file, ensure directory exists - boost::filesystem::create_directories(m_appData); - doc.save_file(m_recentProjects.string().c_str()); + boost::filesystem::create_directories(s_appData); + doc.save_file(s_recentProjects.string().c_str()); } } diff --git a/storage/appdatastorage.h b/storage/appdatastorage.h index 2fb4221..316086f 100644 --- a/storage/appdatastorage.h +++ b/storage/appdatastorage.h @@ -12,23 +12,23 @@ namespace Ember class AppDataStorage { public: - AppDataStorage(); + static void initialize(); /** * @brief Reads recent projects * @param projects List will be saved to given vector. */ - void readRecentProjects(std::vector& projects); + static void readRecentProjects(std::vector& projects); /** * @brief Stores recent projects * @param projects List of projects. */ - void storeRecentProjects(const std::vector& projects); + static void storeRecentProjects(const std::vector& projects); private: - boost::filesystem::path m_appData; - boost::filesystem::path m_recentProjects; + static boost::filesystem::path s_appData; + static boost::filesystem::path s_recentProjects; }; } diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp index 31d230d..d3331ce 100644 --- a/ui/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -1,11 +1,60 @@ +#include +#include + #include "mainwindow.h" #include "ui_mainwindow.h" -MainWindow::MainWindow(QWidget *parent) : - QMainWindow(parent) +namespace Ember { + +MainWindow::MainWindow(ProjectManager* projectManager, QWidget *parent) : + QMainWindow(parent), + m_projectManager(projectManager) +{ + setupUi(); + setupActions(); + QTimer::singleShot(0, this, &MainWindow::showWelcomeDialog); } MainWindow::~MainWindow() { } + +void MainWindow::setupUi() +{ + QWidget* central = new QWidget(); + setCentralWidget(central); +} + +void MainWindow::setupActions() +{ +} + +void MainWindow::showWelcomeDialog() +{ + WelcomeDialog dialog(m_projectManager, this); + if (dialog.exec() == QDialog::Accepted) + { + switch(dialog.resultAction()) + { + case WelcomeDialog::NEW_PROJECT: + newProject(); + break; + + case WelcomeDialog::OPEN_PROJECT: + openProject(dialog.openPath()); + break; + } + } +} + +void MainWindow::newProject() +{ +} + +void MainWindow::openProject(boost::filesystem::path path) +{ +} + + +} diff --git a/ui/mainwindow.h b/ui/mainwindow.h index 6a6dcf1..6a7341c 100644 --- a/ui/mainwindow.h +++ b/ui/mainwindow.h @@ -1,18 +1,33 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H +#include #include +#include + +namespace Ember +{ + class MainWindow : public QMainWindow { Q_OBJECT public: - explicit MainWindow(QWidget *parent = 0); - ~MainWindow(); + explicit MainWindow(ProjectManager* projectManager, QWidget *parent = 0); + virtual ~MainWindow(); + +private slots: + void showWelcomeDialog(); + void newProject(); + void openProject(boost::filesystem::path path); private: + void setupUi(); + void setupActions(); + ProjectManager* m_projectManager; }; +} #endif // MAINWINDOW_H diff --git a/ui/welcome/recentprojectwidget.cpp b/ui/welcome/recentprojectwidget.cpp new file mode 100644 index 0000000..eaa1a9c --- /dev/null +++ b/ui/welcome/recentprojectwidget.cpp @@ -0,0 +1,63 @@ +#include "recentprojectwidget.h" +#include "ui_recentprojectwidget.h" + +namespace Ember { + +RecentProjectWidget::RecentProjectWidget(RecentProject project, QWidget *parent) : + QWidget(parent), + ui(new Ui::RecentProjectWidget), + m_project(project), + m_selected(false) +{ + ui->setupUi(this); + setupActions(); + setSelected(false); +} + +RecentProjectWidget::~RecentProjectWidget() +{ + delete ui; +} + +void RecentProjectWidget::setupUi() +{ + ui->text->setText(QString::fromStdString(m_project.name)); + ui->buttonPin->setChecked(m_project.pinned); +} + +void RecentProjectWidget::setupActions() +{ + connect(ui->buttonDelete, &QToolButton::clicked, this, &RecentProjectWidget::onDeleteClicked); + connect(ui->buttonPin, &QToolButton::toggled, this, &RecentProjectWidget::onPinToggled); +} + +const RecentProject& RecentProjectWidget::project() const +{ + return m_project; +} + +void RecentProjectWidget::setSelected(bool selected) +{ + m_selected = selected; + ui->buttonPin->setVisible(selected); + ui->buttonDelete->setVisible(selected); + ui->textPin->setVisible(!selected && ui->buttonPin->isChecked()); +} + +void RecentProjectWidget::onDeleteClicked() +{ + emit deleted(m_project); +} + +void RecentProjectWidget::onPinToggled(bool pinned) +{ + ui->textPin->setVisible(!m_selected && pinned); + if (m_project.pinned != pinned) + { + m_project.pinned = pinned; + emit pinToggled(m_project, pinned); + } +} + + +} diff --git a/ui/welcome/recentprojectwidget.h b/ui/welcome/recentprojectwidget.h new file mode 100644 index 0000000..94c5a35 --- /dev/null +++ b/ui/welcome/recentprojectwidget.h @@ -0,0 +1,43 @@ +#ifndef RECENTPROJECTWIDGET_H +#define RECENTPROJECTWIDGET_H + +#include +#include + +namespace Ui { +class RecentProjectWidget; +} + +namespace Ember { + +class RecentProjectWidget : public QWidget +{ + Q_OBJECT + +public: + explicit RecentProjectWidget(RecentProject project, QWidget *parent = 0); + virtual ~RecentProjectWidget(); + + const RecentProject& project() const; + + void setSelected(bool selected); + +signals: + void deleted(const RecentProject& project); + void pinToggled(const RecentProject& project, bool pinned); + +private slots: + void onDeleteClicked(); + void onPinToggled(bool pinned); + +private: + void setupUi(); + void setupActions(); + + Ui::RecentProjectWidget *ui; + RecentProject m_project; + bool m_selected; +}; + +} +#endif // RECENTPROJECTWIDGET_H diff --git a/ui/welcome/recentprojectwidget.ui b/ui/welcome/recentprojectwidget.ui new file mode 100644 index 0000000..35221e5 --- /dev/null +++ b/ui/welcome/recentprojectwidget.ui @@ -0,0 +1,76 @@ + + + RecentProjectWidget + + + + 0 + 0 + 663 + 46 + + + + Form + + + + QLayout::SetMinimumSize + + + + + + 1 + 0 + + + + TextLabel + + + + + + + + Font Awesome 5 Free + + + + + + + true + + + + + + + + Font Awesome 5 Free + + + + + + + + + + + + Font Awesome 5 Free + + + + + + + + + + + + diff --git a/ui/welcome/welcomedialog.cpp b/ui/welcome/welcomedialog.cpp index 10075e0..c5d214d 100644 --- a/ui/welcome/welcomedialog.cpp +++ b/ui/welcome/welcomedialog.cpp @@ -1,14 +1,157 @@ +#include + +#include + #include "welcomedialog.h" #include "ui_welcomedialog.h" +#include "recentprojectwidget.h" -WelcomeDialog::WelcomeDialog(QWidget *parent) : +namespace Ember { + +WelcomeDialog::WelcomeDialog(ProjectManager* projectManager, QWidget *parent) : QDialog(parent), - ui(new Ui::WelcomeDialog) + ui(new Ui::WelcomeDialog), + m_projectManager(projectManager), + m_originalPicture() { ui->setupUi(this); + m_originalPicture = QPixmap(*ui->picture->pixmap()); + setResult(Rejected); + + setupActions(); + populateProjects(); } WelcomeDialog::~WelcomeDialog() { delete ui; } + +void WelcomeDialog::setupActions() +{ + connect(ui->buttonNewProject, &QCommandLinkButton::clicked, this, &WelcomeDialog::onNewProjectClicked); + connect(ui->buttonOpenProject, &QCommandLinkButton::clicked, this, &WelcomeDialog::onOpenProjectClicked); + connect(ui->listRecentProjects, &QListWidget::currentItemChanged, this, &WelcomeDialog::onProjectCurrentChanged); + connect(ui->listRecentProjects, &QListWidget::itemActivated, this, &WelcomeDialog::onProjectActivated); +} + +void WelcomeDialog::populateProjects() +{ + std::vector projects; + for (auto pair : m_projectManager->recentProjects()) + projects.push_back(pair.second); + + // Sort by pinned and access date + std::sort(projects.begin(), projects.end(), [](const RecentProject& a, const RecentProject& b) -> bool + { + if (a.pinned == b.pinned) + return a.access > b.access; + + return a.pinned > b.pinned; + }); + + // Add items + for (auto& project : projects) + { + RecentProjectWidget* itemWidget = new RecentProjectWidget(project); + connect(itemWidget, &RecentProjectWidget::pinToggled, this, &WelcomeDialog::onProjectPinned); + connect(itemWidget, &RecentProjectWidget::deleted, this, &WelcomeDialog::onProjectDeleted); + + QListWidgetItem* item = new QListWidgetItem(); + item->setSizeHint(itemWidget->sizeHint()); + + ui->listRecentProjects->addItem(item); + ui->listRecentProjects->setItemWidget(item, itemWidget); + } +} + +void WelcomeDialog::resizeEvent(QResizeEvent*) +{ + QSize pictSize = ui->picture->size(); + QPixmap pictScaled = m_originalPicture.scaled(pictSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + ui->picture->setPixmap(pictScaled); +} + +void WelcomeDialog::onNewProjectClicked() +{ + m_resultAction = NEW_PROJECT; + accept(); +} + +void WelcomeDialog::onOpenProjectClicked() +{ + QString path = QFileDialog::getOpenFileName(this, tr("Open project"), "", buildFilterString()); + + if (!path.isEmpty()) + { + QByteArray str = path.toUtf8(); + m_openPath.assign(str.begin(), str.end()); + m_resultAction = OPEN_PROJECT; + accept(); + } +} + +void WelcomeDialog::onProjectPinned(const RecentProject& project, bool pinned) +{ + m_projectManager->pinRecentProject(project.path, pinned); +} + +void WelcomeDialog::onProjectDeleted(const RecentProject& project) +{ + m_projectManager->deleteRecentProject(project.path); + delete ui->listRecentProjects->takeItem(ui->listRecentProjects->currentRow()); +} + +void WelcomeDialog::onProjectCurrentChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + if (previous) + { + RecentProjectWidget* itemWidget = (RecentProjectWidget*) ui->listRecentProjects->itemWidget(previous); + if (itemWidget) + itemWidget->setSelected(false); + } + + if (current) + { + RecentProjectWidget* itemWidget = (RecentProjectWidget*) ui->listRecentProjects->itemWidget(current); + if (itemWidget) + itemWidget->setSelected(true); + } +} + +void WelcomeDialog::onProjectActivated(QListWidgetItem *item) +{ + RecentProjectWidget* itemWidget = (RecentProjectWidget*) ui->listRecentProjects->itemWidget(item); + if (itemWidget) + { + m_resultAction = OPEN_PROJECT; + m_openPath = itemWidget->project().path; + accept(); + } +} + +QString WelcomeDialog::buildFilterString() +{ + static QString str; + if (str.isEmpty()) + { + str = QString("%1 (*%2);;%3 (*.*)").arg( + tr("Project files"), + EMBER_PROJECT_EXTENSION, + tr("All files")); + } + + return str; +} + +boost::filesystem::path WelcomeDialog::openPath() const +{ + return m_openPath; +} + +WelcomeDialog::ResultAction WelcomeDialog::resultAction() const +{ + return m_resultAction; +} + +} diff --git a/ui/welcome/welcomedialog.h b/ui/welcome/welcomedialog.h index c035d79..7b8cc07 100644 --- a/ui/welcome/welcomedialog.h +++ b/ui/welcome/welcomedialog.h @@ -2,21 +2,54 @@ #define WELCOMEDIALOG_H #include +#include +#include namespace Ui { class WelcomeDialog; } +namespace Ember { + class WelcomeDialog : public QDialog { Q_OBJECT public: - explicit WelcomeDialog(QWidget *parent = 0); - ~WelcomeDialog(); + enum ResultAction + { + NEW_PROJECT, + OPEN_PROJECT + }; + + explicit WelcomeDialog(ProjectManager* projectManager, QWidget *parent = 0); + virtual ~WelcomeDialog(); + + ResultAction resultAction() const; + boost::filesystem::path openPath() const; + +protected: + virtual void resizeEvent(QResizeEvent*) override; + +private slots: + void onNewProjectClicked(); + void onOpenProjectClicked(); + void onProjectPinned(const RecentProject& project, bool pinned); + void onProjectDeleted(const RecentProject& project); + void onProjectCurrentChanged(QListWidgetItem* current, QListWidgetItem* previous); + void onProjectActivated(QListWidgetItem* item); private: + void setupActions(); + void populateProjects(); + static QString buildFilterString(); + Ui::WelcomeDialog *ui; + ProjectManager* m_projectManager; + QPixmap m_originalPicture; + ResultAction m_resultAction; + boost::filesystem::path m_openPath; }; +} #endif // WELCOMEDIALOG_H