Skip to content
Snippets Groups Projects
ProjectManager.cpp 15.14 KiB
//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Project/ProjectManager.cpp
//! @brief     Implements class ProjectManager
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "GUI/View/Project/ProjectManager.h"
#include "Base/Util/Assert.h"
#include "GUI/Application/ApplicationSettings.h"
#include "GUI/Model/IO/ProjectUtils.h"
#include "GUI/Model/State/SessionData.h"
#include "GUI/Util/Error.h"
#include "GUI/Util/MessageService.h"
#include "GUI/View/Info/MessageBox.h"
#include "GUI/View/Info/ProjectLoadProblemDialog.h"
#include "GUI/View/Global/Globals.h"
#include "GUI/View/Project/NewProjectDialog.h"
#include "GUI/View/Project/SaveService.h"
#include "GUI/View/Tool/mainwindow_constants.h"
#include <QApplication>
#include <QDateTime>
#include <QFileDialog>
#include <QMessageBox>
#include <QSettings>

namespace {

const QString S_PROJECTMANAGER = "ProjectManager";
const QString S_AUTOSAVE = "EnableAutosave";
const QString S_DEFAULTPROJECTPATH = "DefaultProjectPath";
const QString S_RECENTPROJECTS = "RecentProjects";
const QString S_LASTUSEDIMPORTDIR = "LastUsedImportDir";
const QString S_LASTUSEDIMPORFILTER1D = "LastUsedImportFilter1D";
const QString S_LASTUSEDIMPORFILTER2D = "LastUsedImportFilter2D";

} // namespace


ProjectManager* ProjectManager::s_instance = nullptr;

ProjectManager::ProjectManager(MainWindow*)
    : m_saveService(new SaveService(this))

{
    if (s_instance != nullptr)
        throw Error("ProjectManager::ProjectManager -> Error. Attempt to create "
                    "ProjectManager twice.");

    s_instance = this;
}

ProjectManager::~ProjectManager()
{
    s_instance = nullptr;
    delete gSessionData->projectDocument;
}

ProjectManager* ProjectManager::instance()
{
    if (!s_instance)
        throw Error("ProjectManager::instance() -> Error. Attempt to access "
                    "non existing ProjectManager.");

    return s_instance;
}

//! Reads settings of ProjectManager from global settings.

void ProjectManager::readSettings()
{
    QSettings settings;
    m_workingDirectory = QDir::homePath();
    if (settings.childGroups().contains(S_PROJECTMANAGER)) {
        settings.beginGroup(S_PROJECTMANAGER);

        if (!settings.contains(S_AUTOSAVE))
            settings.setValue(S_AUTOSAVE, true);

        m_workingDirectory = settings.value(S_DEFAULTPROJECTPATH).toString();
        m_recentProjects = settings.value(S_RECENTPROJECTS).toStringList();

        if (settings.contains(S_LASTUSEDIMPORTDIR))
            m_importDirectory = settings.value(S_LASTUSEDIMPORTDIR, QString()).toString();

        m_importFilter1D = settings.value(S_LASTUSEDIMPORFILTER1D, m_importFilter1D).toString();
        m_importFilter2D = settings.value(S_LASTUSEDIMPORFILTER2D, m_importFilter2D).toString();

        setAutosaveEnabled(settings.value(S_AUTOSAVE).toBool());

        settings.endGroup();
    }
}

//! Saves settings of ProjectManager in global settings.

void ProjectManager::writeSettings()
{
    QSettings settings;
    settings.beginGroup(S_PROJECTMANAGER);
    settings.setValue(S_DEFAULTPROJECTPATH, m_workingDirectory);
    settings.setValue(S_RECENTPROJECTS, m_recentProjects);

    if (!m_importDirectory.isEmpty())
        settings.setValue(S_LASTUSEDIMPORTDIR, m_importDirectory);
    settings.setValue(S_LASTUSEDIMPORFILTER1D, m_importFilter1D);
    settings.setValue(S_LASTUSEDIMPORFILTER2D, m_importFilter2D);

    settings.endGroup();
}

//! Returns list of recent projects, validates if projects still exists on disk.

QStringList ProjectManager::recentProjects()
{
    QStringList updatedList;
    for (const QString& fileName : m_recentProjects)
        if (QFile fin(fileName); fin.exists())
            updatedList.append(fileName);
    m_recentProjects = updatedList;
    return m_recentProjects;
}

//! Returns name of the current project directory.

QString ProjectManager::projectDir() const
{
    if (auto* const pd = gSessionData->projectDocument)
        return pd->validProjectDir();
    return "";
}

//! Returns directory name which was used by the user to import files.

QString ProjectManager::userImportDir() const
{
    if (m_importDirectory.isEmpty()) {
        if (auto* const pd = gSessionData->projectDocument)
            return pd->userExportDir();
        return "";
    }
    return m_importDirectory;
}

QString ProjectManager::recentlyUsedImportFilter1D() const
{
    return m_importFilter1D;
}

QString ProjectManager::recentlyUsedImportFilter2D() const
{
    return m_importFilter2D;
}

//! Sets user import directory in system settings.

void ProjectManager::setImportDir(const QString& dirname)
{
    m_importDirectory = dirname;
}

//! Sets user import directory in system settings.

void ProjectManager::setImportDirFromFilePath(const QString& filePath)
{
    m_importDirectory = QFileInfo(filePath).absolutePath();
}

void ProjectManager::setRecentlyUsedImportFilter1D(const QString& filter)
{
    m_importFilter1D = filter;
}

void ProjectManager::setRecentlyUsedImportFilter2D(const QString& filter)
{
    m_importFilter2D = filter;
}

bool ProjectManager::isAutosaveEnabled() const
{
    return m_saveService->isAutosaveEnabled();
}

void ProjectManager::setAutosaveEnabled(bool value)
{
    m_saveService->setAutosaveEnabled(value);
    QSettings settings;
    settings.setValue(S_PROJECTMANAGER + "/" + S_AUTOSAVE, value);
}

//! Clears list of recent projects.

void ProjectManager::clearRecentProjects()
{
    m_recentProjects.clear();
    emit recentListModified();
}

//! Processes new project request (close old project, rise dialog for project name, create project).

void ProjectManager::newProject()
{
    if (!closeCurrentProject())
        return;
    createNewProject();
    emit documentOpenedOrClosed(true);
}

//! Processes close current project request. Call save/discard/cancel dialog, if necessary.
//! Returns false if saving was canceled.

bool ProjectManager::closeCurrentProject()
{
    if (!gSessionData->projectDocument)
        return true;

    if (gSessionData->projectDocument->isModified()) {
        QMessageBox msgBox;
        msgBox.setText("The project has been modified.");
        msgBox.setInformativeText("Do you want to save your changes?");
        msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
        msgBox.setDefaultButton(QMessageBox::Save);

        switch (msgBox.exec()) {
        case QMessageBox::Save:
            if (!saveProject())
                return false;
            break;
        case QMessageBox::Discard:
            break;
        case QMessageBox::Cancel:
            return false;
        default:
            break;
        }
    }

    deleteCurrentProject();
    emit documentOpenedOrClosed(false);
    return true;
}

//! Processes save project request.

bool ProjectManager::saveProject(QString projectFileName)
{
    if (projectFileName.isEmpty()) {
        if (gSessionData->projectDocument->hasValidNameAndPath())
            projectFileName = gSessionData->projectDocument->projectFileName();
        else
            projectFileName = acquireProjectFileName();
    }

    if (projectFileName.isEmpty())
        return false;

    try {
        m_saveService->save(projectFileName);
    } catch (const std::exception& ex) {
        QString message = QString("Failed to save project under '%1'. \n\n").arg(projectFileName);
        message.append("Exception was thrown.\n\n");
        message.append(ex.what());

        QMessageBox::warning(GUI::Global::mainWindow, "Error while saving project", message);
        return false;
    }

    addToRecentProjects();

    return true;
}

//! Processes 'save project as' request.

bool ProjectManager::saveProjectAs()
{
    QString projectFileName = acquireProjectFileName();

    if (projectFileName.isEmpty())
        return false;

    return saveProject(projectFileName);
}

//! Opens existing project. If fileName is empty, will popup file selection dialog.

void ProjectManager::openProject(QString fileName)
{
    if (!closeCurrentProject())
        return;

    if (fileName.isEmpty()) {
        fileName = QFileDialog::getOpenFileName(
            GUI::Global::mainWindow, "Open project file", workingDirectory(),
            "BornAgain project Files (*.pro)", nullptr,
            appSettings->useNativeFileDialog() ? QFileDialog::Options()
                                               : QFileDialog::DontUseNativeDialog);
        if (fileName.isEmpty())
            return;
    }

    createNewProject();
    MessageService messageService;
    const auto readResult = loadProject(fileName, messageService);

    if (readResult == ProjectDocument::ReadResult::ok)
        addToRecentProjects();
    else if (readResult == ProjectDocument::ReadResult::error) {
        riseProjectLoadFailedDialog(messageService);
        deleteCurrentProject();
        createNewProject();
    } else if (readResult == ProjectDocument::ReadResult::warning) {
        riseProjectLoadProblemDialog(messageService);
        addToRecentProjects();
    }
    if (gSessionData->projectDocument != nullptr)
        emit documentOpenedOrClosed(true);
}

//! Calls dialog window to define project path and name.

void ProjectManager::createNewProject()
{
    if (gSessionData->projectDocument)
        throw Error("ProjectManager::createNewProject() -> Project already exists");

    gSessionData->projectDocument = new ProjectDocument();

    const QVariant defFunctionalities =
        appSettings->defaultFunctionalities(toVariant(ProjectDocument::All));
    gSessionData->projectDocument->setFunctionalities(toFunctionalities(defFunctionalities));
    gSessionData->projectDocument->setSingleInstrumentMode(
        appSettings->defaultIsSingleInstrumentMode());
    gSessionData->projectDocument->setSingleSampleMode(appSettings->defaultIsSingleSampleMode());

    gSessionData->projectDocument->setProjectName("Untitled");
    gSessionData->projectDocument->clearModified();

    m_saveService->setDocument(gSessionData->projectDocument);

    connect(gSessionData->projectDocument, &ProjectDocument::modified,
            [this]() { emit documentModified(); });
}

void ProjectManager::deleteCurrentProject()
{
    emit aboutToCloseDocument();
    m_saveService->stopService();

    delete gSessionData->projectDocument;
    gSessionData->projectDocument = nullptr;
}

//! Load project data from file name. If autosave info exists, opens dialog for project restore.

ProjectDocument::ReadResult ProjectManager::loadProject(const QString& projectFileName,
                                                        MessageService& messageService)
{
    auto readResult = ProjectDocument::ReadResult::ok;

    const bool useAutosave =
        m_saveService && GUI::Project::Utils::hasAutosavedData(projectFileName);
    const QString autosaveName = GUI::Project::Utils::autosaveName(projectFileName);
    if (useAutosave && restoreProjectDialog(projectFileName, autosaveName)) {
        QApplication::setOverrideCursor(Qt::WaitCursor);
        readResult = gSessionData->projectDocument->loadProjectFile(autosaveName, messageService);
        gSessionData->projectDocument->setProjectFileName(projectFileName);
        gSessionData->projectDocument->setModified();
    } else {
        QApplication::setOverrideCursor(Qt::WaitCursor);
        readResult =
            gSessionData->projectDocument->loadProjectFile(projectFileName, messageService);
    }
    QApplication::restoreOverrideCursor();
    return readResult;
}

//! Returns project file name from dialog. Returns empty string if dialog was canceled.

QString ProjectManager::acquireProjectFileName()
{
    NewProjectDialog dialog(GUI::Global::mainWindow, workingDirectory(), untitledProjectName());

    if (dialog.exec() != QDialog::Accepted)
        return "";

    m_workingDirectory = dialog.getWorkingDirectory();

    return dialog.getProjectFileName();
}

//! Add name of the current project to the name of recent projects

void ProjectManager::addToRecentProjects()
{
    QString fileName = gSessionData->projectDocument->projectFileName();
    m_recentProjects.removeAll(fileName);
    m_recentProjects.prepend(fileName);
    while (m_recentProjects.size() > GUI::Constants::MAX_RECENT_PROJECTS)
        m_recentProjects.removeLast();

    emit recentListModified();
}

//! Returns default project path.

QString ProjectManager::workingDirectory()
{
    return m_workingDirectory;
}

//! Will return 'Untitled' if the directory with such name doesn't exist in project
//! path. Otherwise will return Untitled1, Untitled2 etc.

QString ProjectManager::untitledProjectName()
{
    QString result = "Untitled";
    QDir projectDir = workingDirectory() + "/" + result;
    if (projectDir.exists()) {
        for (size_t i = 1; i < 99; ++i) {
            result = QString("Untitled") + QString::number(i);
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
            projectDir.setPath(workingDirectory() + "/" + result);
#else
            projectDir = workingDirectory() + "/" + result;
#endif
            if (!projectDir.exists())
                break;
        }
    }
    return result;
}

void ProjectManager::riseProjectLoadFailedDialog(const MessageService& messageService)
{
    QString message = QString("Failed to load the project '%1' \n\n")
                          .arg(gSessionData->projectDocument->projectFileName());

    for (const auto& details : messageService.errors())
        message.append(details + "\n");

    QMessageBox::warning(GUI::Global::mainWindow, "Error while opening project file", message);
}

void ProjectManager::riseProjectLoadProblemDialog(const MessageService& messageService)
{
    ASSERT(gSessionData->projectDocument);
    auto* problemDialog =
        new ProjectLoadProblemDialog(GUI::Global::mainWindow, messageService.warnings(true),
                                     gSessionData->projectDocument->documentVersion());

    problemDialog->show();
    problemDialog->raise();
}

//! Rises dialog if the project should be restored from autosave. Returns true, if yes.

bool ProjectManager::restoreProjectDialog(const QString& projectFileName,
                                          const QString autosaveName)
{
    const QString title("Recover project");
    const QString lmProject =
        QFileInfo(projectFileName).lastModified().toString("hh:mm:ss, MMMM d, yyyy");
    const QString lmAutoSave =
        QFileInfo(autosaveName).lastModified().toString("hh:mm:ss, MMMM d, yyyy");

    QString message = QString("Project '%1' contains autosaved data.\n\n"
                              "Project saved at %2\nAutosave from %3")
                          .arg(GUI::Project::Utils::projectName(projectFileName))
                          .arg(lmProject)
                          .arg(lmAutoSave);

    return GUI::View::Helpers::question(GUI::Global::mainWindow, title, message,
                                        "\nDo you want to restore from autosave?\n",
                                        "Yes, please restore.", "No, keep loading original");
}