From 7a47faa21202a5655e1e07881f3cb548a8e8b00c Mon Sep 17 00:00:00 2001 From: Matt Chang Date: Sat, 8 Aug 2020 15:23:54 +1000 Subject: [PATCH 01/10] First working version for crash recovery --- app/src/mainwindow2.cpp | 50 +++++++++++++- app/src/mainwindow2.h | 7 +- core_lib/src/structure/filemanager.cpp | 92 ++++++++++++++++++++++++++ core_lib/src/structure/filemanager.h | 7 ++ core_lib/src/structure/object.cpp | 18 +++-- core_lib/src/structure/object.h | 1 + core_lib/src/util/fileformat.cpp | 26 +++++--- core_lib/src/util/fileformat.h | 3 +- tests/src/test_filemanager.cpp | 4 +- 9 files changed, 189 insertions(+), 19 deletions(-) diff --git a/app/src/mainwindow2.cpp b/app/src/mainwindow2.cpp index 6d963868ac..276307fe72 100644 --- a/app/src/mainwindow2.cpp +++ b/app/src/mainwindow2.cpp @@ -1,4 +1,4 @@ -/* +/* Pencil - Traditional Animation Software Copyright (C) 2005-2007 Patrick Corrieri & Pascal Naidon @@ -518,6 +518,16 @@ void MainWindow2::closeEvent(QCloseEvent* event) } } +void MainWindow2::showEvent(QShowEvent*) +{ + static bool firstShowEvent = true; + if (firstShowEvent) + { + firstShowEvent = false; + tryRecoverUnsavedProject(); + } +} + void MainWindow2::tabletEvent(QTabletEvent* event) { event->ignore(); @@ -1525,3 +1535,41 @@ void MainWindow2::displayMessageBoxNoTitle(const QString& body) { QMessageBox::information(this, nullptr, tr(qPrintable(body)), QMessageBox::Ok); } + +void MainWindow2::tryRecoverUnsavedProject() +{ + FileManager fm; + QStringList recoverables = fm.searchForUnsavedProjects(); + + if (recoverables.size() == 0) + { + return; + } + + QString caption = tr("Restore Project?"); + QString text = tr("Pencil2D didn't close correctly. Would you like to restore the project?"); + + QString recoverPath = recoverables[0]; + + QMessageBox* msgBox = new QMessageBox(this); + msgBox->setWindowTitle(tr("Restore project")); + msgBox->setWindowModality(Qt::ApplicationModal); + msgBox->setAttribute(Qt::WA_DeleteOnClose); + msgBox->setIconPixmap(QPixmap(":/icons/logo.png")); + msgBox->setText(QString("

%1

%2").arg(caption).arg(text)); + msgBox->setInformativeText(QString("%1").arg(retrieveProjectNameFromTempPath(recoverPath))); + msgBox->setStandardButtons(QMessageBox::Open | QMessageBox::Discard); + hideQuestionMark(*msgBox); + + connect(msgBox, &QMessageBox::finished, [recoverPath, this](int result) + { + if (QMessageBox::Open == result) + { + FileManager fm; + Object* o = fm.recoverUnsavedProject(recoverPath); + mEditor->setObject(o); + updateSaveState(); + } + }); + msgBox->open(); +} diff --git a/app/src/mainwindow2.h b/app/src/mainwindow2.h index fd3acd916e..b8686a720d 100644 --- a/app/src/mainwindow2.h +++ b/app/src/mainwindow2.h @@ -96,7 +96,7 @@ public slots: void setSoundScrubMsec(int msec); void setOpacity(int opacity); void preferences(); - + void openFile(QString filename); PreferencesDialog* getPrefDialog() { return mPrefDialog; } @@ -111,6 +111,7 @@ public slots: protected: void tabletEvent(QTabletEvent*) override; void closeEvent(QCloseEvent*) override; + void showEvent(QShowEvent*) override; private slots: void resetAndDockAllSubWidgets(); @@ -150,6 +151,8 @@ private slots: void bindActionWithSetting(QAction*, const SETTING&); + void tryRecoverUnsavedProject(); + // UI: Dock widgets ColorBox* mColorBox = nullptr; ColorPaletteWidget* mColorPalette = nullptr; @@ -181,7 +184,7 @@ private slots: // whether we are currently importing an image sequence. bool mIsImportingImageSequence = false; - + Ui::MainWindow2* ui = nullptr; }; diff --git a/core_lib/src/structure/filemanager.cpp b/core_lib/src/structure/filemanager.cpp index 1ab0e78b0e..930f1df6cf 100644 --- a/core_lib/src/structure/filemanager.cpp +++ b/core_lib/src/structure/filemanager.cpp @@ -630,3 +630,95 @@ Status FileManager::verifyObject(Object* obj) } return Status::OK; } + +QStringList FileManager::searchForUnsavedProjects() +{ + QDir pencil2DTempDir = QDir::temp(); + bool folderExists = pencil2DTempDir.cd("Pencil2D"); + if (!folderExists) + { + return QStringList(); + } + + const QStringList nameFilter("*_" PFF_TMP_DECOMPRESS_EXT "_*"); // match name pattern like "Default_Y2xD_0a4e44e9" + QStringList entries = pencil2DTempDir.entryList(nameFilter, QDir::Dirs | QDir::Readable); + + QStringList recoverables; + for (const QString path : entries) + { + QString fullPath = pencil2DTempDir.filePath(path); + if (isProjectRecoverable(fullPath)) + { + qDebug() << "Found debris at" << fullPath; + recoverables.append(fullPath); + } + } + return recoverables; +} + +bool FileManager::isProjectRecoverable(const QString& projectFolder) +{ + QDir dir(projectFolder); + if (!dir.exists()) { return false; } + + // There must be a subfolder called "data" + if (!dir.exists("data")) { return false; } + + bool ok = dir.cd("data"); + Q_ASSERT(ok); + + QStringList nameFiler; + nameFiler << "*.png" << "*.vec" << "*.xml"; + QStringList entries = dir.entryList(nameFiler, QDir::Files); + + return (entries.size() > 0); +} + +Object* FileManager::recoverUnsavedProject(QString intermeidatePath) +{ + qDebug() << "TODO: recover project" << intermeidatePath; + + QDir projectDir(intermeidatePath); + const QString mainXMLPath = projectDir.filePath(PFF_XML_FILE_NAME); + const QString dataFolder = projectDir.filePath(PFF_DATA_DIR); + + std::unique_ptr object = std::make_unique(); + object->setWorkingDir(intermeidatePath); + object->setMainXMLFile(mainXMLPath); + object->setDataDir(dataFolder); + + recoverObject(object.get()); + + // Transfer ownership to the caller + return object.release(); +} + +Status FileManager::recoverObject(Object* object) +{ + QFile file(object->mainXMLFile()); + if (!file.exists()) { return Status::FAIL; } + + bool openOK = file.open(QFile::ReadOnly); + if (!openOK) { return Status::FAIL; } + + QDomDocument xmlDoc; + if (!xmlDoc.setContent(&file)) { return Status::FAIL; } + + QDomDocumentType type = xmlDoc.doctype(); + if (!(type.name() == "PencilDocument" || type.name() == "MyObject")) + { + return Status::FAIL;; + } + + QDomElement root = xmlDoc.documentElement(); + if (root.isNull()) { return Status::FAIL; } + + QDomElement e = root.firstChildElement("object"); + if (e.isNull()) { return Status::FAIL; } + + bool ok = loadObject(object, root); + verifyObject(object); + + return ok ? Status::OK : Status::FAIL; +} + diff --git a/core_lib/src/structure/filemanager.h b/core_lib/src/structure/filemanager.h index ee4d257a2c..48b1dd5b9f 100644 --- a/core_lib/src/structure/filemanager.h +++ b/core_lib/src/structure/filemanager.h @@ -45,6 +45,9 @@ class FileManager : public QObject Status error() const { return mError; } Status verifyObject(Object* obj); + QStringList searchForUnsavedProjects(); + Object* recoverUnsavedProject(QString projectIntermediatePath); + Q_SIGNALS: void progressChanged(int progress); void progressRangeChanged(int maxValue); @@ -68,6 +71,10 @@ class FileManager : public QObject void progressForward(); +private: // Project recovery + Status recoverObject(Object* object); + bool isProjectRecoverable(const QString& projectFolder); + private: Status mError = Status::OK; QString mstrLastTempFolder; diff --git a/core_lib/src/structure/object.cpp b/core_lib/src/structure/object.cpp index c8e738f529..83d751c2b0 100644 --- a/core_lib/src/structure/object.cpp +++ b/core_lib/src/structure/object.cpp @@ -163,15 +163,15 @@ LayerCamera* Object::addNewCameraLayer() void Object::createWorkingDir() { - QString strFolderName; + QString projectName; if (mFilePath.isEmpty()) { - strFolderName = "Default"; + projectName = "Default"; } else { QFileInfo fileInfo(mFilePath); - strFolderName = fileInfo.completeBaseName(); + projectName = fileInfo.completeBaseName(); } QDir dir(QDir::tempPath()); @@ -180,7 +180,7 @@ void Object::createWorkingDir() { strWorkingDir = QString("%1/Pencil2D/%2_%3_%4/") .arg(QDir::tempPath()) - .arg(strFolderName) + .arg(projectName) .arg(PFF_TMP_DECOMPRESS_EXT) .arg(uniqueString(8)); } @@ -200,10 +200,18 @@ void Object::deleteWorkingDir() const if (!mWorkingDirPath.isEmpty()) { QDir dir(mWorkingDirPath); - dir.removeRecursively(); + bool ok = dir.removeRecursively(); + Q_ASSERT(ok); } } +void Object::setWorkingDir(const QString& path) +{ + QDir dir(path); + Q_ASSERT(dir.exists()); + mWorkingDirPath = path; +} + void Object::createDefaultLayers() { // default layers diff --git a/core_lib/src/structure/object.h b/core_lib/src/structure/object.h index b21917af0b..77d55e0db1 100644 --- a/core_lib/src/structure/object.h +++ b/core_lib/src/structure/object.h @@ -62,6 +62,7 @@ class Object : public QObject void init(); void createWorkingDir(); void deleteWorkingDir() const; + void setWorkingDir(const QString& path); // used by crash recovery void createDefaultLayers(); QString filePath() const { return mFilePath; } diff --git a/core_lib/src/util/fileformat.cpp b/core_lib/src/util/fileformat.cpp index 217d93c01c..7f7fbed570 100644 --- a/core_lib/src/util/fileformat.cpp +++ b/core_lib/src/util/fileformat.cpp @@ -17,25 +17,25 @@ GNU General Public License for more details. #include "fileformat.h" #include +#include -bool removePFFTmpDirectory (const QString& dirName) +bool removePFFTmpDirectory(const QString& dirName) { - if ( dirName.isEmpty() ) + if (dirName.isEmpty()) { return false; } - QDir dir( dirName ); - - if ( !dir.exists() ) + QDir dir(dirName); + + if (!dir.exists()) { - Q_ASSERT( false ); + Q_ASSERT(false); return false; } bool result = dir.removeRecursively(); - - return result; + return result; } QString uniqueString(int len) @@ -53,3 +53,13 @@ QString uniqueString(int len) s[len] = 0; return QString::fromUtf8(s); } + +QString retrieveProjectNameFromTempPath(const QString& path) +{ + QFileInfo info(path); + QString fileName = info.completeBaseName(); + + QStringList tokens = fileName.split("_"); + //qDebug() << tokens; + return tokens[0]; +} diff --git a/core_lib/src/util/fileformat.h b/core_lib/src/util/fileformat.h index 5fe61ffbbd..0e28e565c4 100644 --- a/core_lib/src/util/fileformat.h +++ b/core_lib/src/util/fileformat.h @@ -71,8 +71,9 @@ GNU General Public License for more details. #define PFF_TMP_DECOMPRESS_EXT "Y2xD" #define PFF_PALETTE_FILE "palette.xml" -bool removePFFTmpDirectory (const QString& dirName); +bool removePFFTmpDirectory(const QString& dirName); QString uniqueString(int len); +QString retrieveProjectNameFromTempPath(const QString& path); #endif diff --git a/tests/src/test_filemanager.cpp b/tests/src/test_filemanager.cpp index 0fdfb70621..e5236eb352 100644 --- a/tests/src/test_filemanager.cpp +++ b/tests/src/test_filemanager.cpp @@ -1,4 +1,4 @@ -/* +/* Pencil - Traditional Animation Software Copyright (C) 2012-2020 Matthew Chiawen Chang @@ -430,7 +430,7 @@ TEST_CASE("Empty Sound Frames") REQUIRE(newObj->getLayer(0)->type() == 4); REQUIRE(newObj->getLayer(0)->id() == 5); REQUIRE(newObj->getLayer(0)->name() == "GoodLayer"); - REQUIRE(newObj->getLayer(0)->getVisibility() == 1); + REQUIRE(newObj->getLayer(0)->getVisibility() == true); REQUIRE(newObj->getLayer(0)->getKeyFrameAt(1) == nullptr); delete newObj; From b89b803987b00182abf3d78a98639c970b7ae466 Mon Sep 17 00:00:00 2001 From: Matt Chang Date: Wed, 19 Aug 2020 18:02:41 +1000 Subject: [PATCH 02/10] Error check --- app/src/mainwindow2.cpp | 8 ++++++-- core_lib/src/structure/filemanager.cpp | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/mainwindow2.cpp b/app/src/mainwindow2.cpp index 276307fe72..df854b2a76 100644 --- a/app/src/mainwindow2.cpp +++ b/app/src/mainwindow2.cpp @@ -1559,6 +1559,7 @@ void MainWindow2::tryRecoverUnsavedProject() msgBox->setText(QString("

%1

%2").arg(caption).arg(text)); msgBox->setInformativeText(QString("%1").arg(retrieveProjectNameFromTempPath(recoverPath))); msgBox->setStandardButtons(QMessageBox::Open | QMessageBox::Discard); + hideQuestionMark(*msgBox); connect(msgBox, &QMessageBox::finished, [recoverPath, this](int result) @@ -1567,8 +1568,11 @@ void MainWindow2::tryRecoverUnsavedProject() { FileManager fm; Object* o = fm.recoverUnsavedProject(recoverPath); - mEditor->setObject(o); - updateSaveState(); + if (fm.error().ok()) + { + mEditor->setObject(o); + updateSaveState(); + } } }); msgBox->open(); diff --git a/core_lib/src/structure/filemanager.cpp b/core_lib/src/structure/filemanager.cpp index 930f1df6cf..33cb19054d 100644 --- a/core_lib/src/structure/filemanager.cpp +++ b/core_lib/src/structure/filemanager.cpp @@ -687,8 +687,12 @@ Object* FileManager::recoverUnsavedProject(QString intermeidatePath) object->setMainXMLFile(mainXMLPath); object->setDataDir(dataFolder); - recoverObject(object.get()); - + Status st = recoverObject(object.get()); + if (!st.ok()) + { + mError = st; + return nullptr; + } // Transfer ownership to the caller return object.release(); } From f2b0b3340f6a9c0e109d0fa89040542d10b8d246 Mon Sep 17 00:00:00 2001 From: Matt Chang Date: Thu, 20 Aug 2020 15:11:38 +1000 Subject: [PATCH 03/10] Rebuild main.xml from debris if the main.xml is broken --- core_lib/src/structure/filemanager.cpp | 170 +++++++++++++++++++++++-- core_lib/src/structure/filemanager.h | 10 +- core_lib/src/structure/layerbitmap.cpp | 2 +- 3 files changed, 168 insertions(+), 14 deletions(-) diff --git a/core_lib/src/structure/filemanager.cpp b/core_lib/src/structure/filemanager.cpp index 33cb19054d..55181c5faa 100644 --- a/core_lib/src/structure/filemanager.cpp +++ b/core_lib/src/structure/filemanager.cpp @@ -699,26 +699,38 @@ Object* FileManager::recoverUnsavedProject(QString intermeidatePath) Status FileManager::recoverObject(Object* object) { - QFile file(object->mainXMLFile()); - if (!file.exists()) { return Status::FAIL; } + // Check whether the main.xml is fine, if not we should make a valid one. + bool mainXmlOK = true; - bool openOK = file.open(QFile::ReadOnly); - if (!openOK) { return Status::FAIL; } + QFile file(object->mainXMLFile()); + mainXmlOK &= file.exists(); + mainXmlOK &= file.open(QFile::ReadOnly); QDomDocument xmlDoc; - if (!xmlDoc.setContent(&file)) { return Status::FAIL; } + mainXmlOK &= xmlDoc.setContent(&file); QDomDocumentType type = xmlDoc.doctype(); - if (!(type.name() == "PencilDocument" || type.name() == "MyObject")) - { - return Status::FAIL;; - } + mainXmlOK &= (type.name() == "PencilDocument" || type.name() == "MyObject"); QDomElement root = xmlDoc.documentElement(); - if (root.isNull()) { return Status::FAIL; } + mainXmlOK &= (!root.isNull()); - QDomElement e = root.firstChildElement("object"); - if (e.isNull()) { return Status::FAIL; } + QDomElement objectTag = root.firstChildElement("object"); + mainXmlOK &= (objectTag.isNull()); + + if (mainXmlOK == false) + { + // the main.xml is broken, try to rebuild one + rebuildMainXML(object); + + // Load the newly built main.xml + QFile file(object->mainXMLFile()); + file.open(QFile::ReadOnly); + xmlDoc.setContent(&file); + root = xmlDoc.documentElement(); + objectTag = root.firstChildElement("object"); + } + loadPalette(object); bool ok = loadObject(object, root); verifyObject(object); @@ -726,3 +738,137 @@ Status FileManager::recoverObject(Object* object) return ok ? Status::OK : Status::FAIL; } +/** Create a new main.xml based on the png/vec filenames left in the data folder */ +Status FileManager::rebuildMainXML(Object* object) +{ + QDir dataDir(object->dataDir()); + + QStringList nameFiler; + nameFiler << "*.png" << "*.vec"; + const QStringList entries = dataDir.entryList(nameFiler, QDir::Files | QDir::Readable, QDir::Name); + + QMap keyFrameGroups; + + // grouping keyframe files by layers + for (const QString& s : entries) + { + int layerIndex = layerIndexFromFilename(s); + if (layerIndex > 0) + { + keyFrameGroups[layerIndex].append(s); + } + } + + // build the new main XML file + const QString mainXMLPath = object->mainXMLFile(); + QFile file(mainXMLPath); + if (!file.open(QFile::WriteOnly | QFile::Text)) + { + return Status::ERROR_FILE_CANNOT_OPEN; + } + + QDomDocument xmlDoc("PencilDocument"); + QDomElement root = xmlDoc.createElement("document"); + QDomProcessingInstruction encoding = xmlDoc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""); + xmlDoc.appendChild(encoding); + xmlDoc.appendChild(root); + + // save editor information + QDomElement projDataXml = saveProjectData(object->data(), xmlDoc); + root.appendChild(projDataXml); + + // save object + QDomElement elemObject = xmlDoc.createElement("object"); + root.appendChild(elemObject); + + for (const int layerIndex : keyFrameGroups.keys()) + { + const QStringList& frames = keyFrameGroups.value(layerIndex); + Status st = rebuildLayerXmlTag(xmlDoc, elemObject, layerIndex, frames); + } + + QTextStream fout(&file); + xmlDoc.save(fout, 2); + fout.flush(); + file.close(); + + return Status::OK; +} +/** + * Rebuild a layer xml tag. example: + * + * + * + */ +Status FileManager::rebuildLayerXmlTag(QDomDocument& doc, + QDomElement& elemObject, + const int layerIndex, + const QStringList& frames) +{ + Q_ASSERT(frames.length() > 0); + + Layer::LAYER_TYPE type = frames[0].endsWith(".png") ? Layer::BITMAP : Layer::VECTOR; + + QDomElement elemLayer = doc.createElement("layer"); + elemLayer.setAttribute("id", layerIndex + 1); // starts from 1, not 0. + elemLayer.setAttribute("name", recoverLayerName(type, layerIndex)); + elemLayer.setAttribute("visibility", true); + elemLayer.setAttribute("type", type); + elemObject.appendChild(elemLayer); + + for (const QString& s : frames) + { + const int framePos = framePosFromFilename(s); + if (framePos < 0) { continue; } + + QDomElement elemFrame = doc.createElement("image"); + elemFrame.setAttribute("frame", framePos); + elemFrame.setAttribute("src", s); + + if (type == Layer::BITMAP) + { + // Since we have no way to know the original img position + // Put it at the top left corner of the default camera + elemFrame.setAttribute("topLeftX", -800); + elemFrame.setAttribute("topLeftY", -600); + } + elemLayer.appendChild(elemFrame); + } + return Status::OK; +} + +QString FileManager::recoverLayerName(Layer::LAYER_TYPE type, int index) +{ + switch (type) + { + case Layer::BITMAP: + return QString("%1 %2").arg(tr("Bitmap Layer")).arg(index); + case Layer::VECTOR: + return QString("%1 %2").arg(tr("Vector Layer")).arg(index); + case Layer::SOUND: + return QString("%1 %2").arg(tr("Sound Layer")).arg(index); + default: + Q_ASSERT(false); + } + return ""; +} + +int FileManager::layerIndexFromFilename(const QString& filename) +{ + const QStringList tokens = filename.split("."); // e.g., 001.019.png or 012.132.vec + if (tokens.length() >= 3) // a correct file name must have 3 tokens + { + return tokens[0].toInt(); + } + return -1; +} + +int FileManager::framePosFromFilename(const QString& filename) +{ + const QStringList tokens = filename.split("."); // e.g., 001.019.png or 012.132.vec + if (tokens.length() >= 3) // a correct file name must have 3 tokens + { + return tokens[1].toInt(); + } + return -1; +} \ No newline at end of file diff --git a/core_lib/src/structure/filemanager.h b/core_lib/src/structure/filemanager.h index 48b1dd5b9f..d4816ff2ef 100644 --- a/core_lib/src/structure/filemanager.h +++ b/core_lib/src/structure/filemanager.h @@ -26,6 +26,7 @@ GNU General Public License for more details. #include "pencildef.h" #include "pencilerror.h" #include "colorref.h" +#include "layer.h" class Object; class ObjectData; @@ -71,9 +72,16 @@ class FileManager : public QObject void progressForward(); + private: // Project recovery - Status recoverObject(Object* object); bool isProjectRecoverable(const QString& projectFolder); + Status recoverObject(Object* object); + Status rebuildMainXML(Object* object); + Status rebuildLayerXmlTag(QDomDocument& doc, QDomElement& elemObject, + const int layerIndex, const QStringList& frames); + QString recoverLayerName(Layer::LAYER_TYPE, int index); + int layerIndexFromFilename(const QString& filename); + int framePosFromFilename(const QString& filename); private: Status mError = Status::OK; diff --git a/core_lib/src/structure/layerbitmap.cpp b/core_lib/src/structure/layerbitmap.cpp index 7c98e5349c..bdfea0b0d3 100644 --- a/core_lib/src/structure/layerbitmap.cpp +++ b/core_lib/src/structure/layerbitmap.cpp @@ -149,7 +149,7 @@ QString LayerBitmap::fileName(KeyFrame* key) const } bool LayerBitmap::needSaveFrame(KeyFrame* key, const QString& savePath) -{ +{ if (key->isModified()) // keyframe was modified return true; if (QFile::exists(savePath) == false) // hasn't been saved before From 73c2e9ba430c6221e0da89bd2b15e6d23b99bd3f Mon Sep 17 00:00:00 2001 From: Matt Chang Date: Thu, 20 Aug 2020 15:28:02 +1000 Subject: [PATCH 04/10] Pop-up to show success or failure of the project recovery --- app/src/mainwindow2.cpp | 38 ++++++++++++++++++++++++-------------- app/src/mainwindow2.h | 1 + 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/src/mainwindow2.cpp b/app/src/mainwindow2.cpp index df854b2a76..9cd26cdcea 100644 --- a/app/src/mainwindow2.cpp +++ b/app/src/mainwindow2.cpp @@ -1559,21 +1559,31 @@ void MainWindow2::tryRecoverUnsavedProject() msgBox->setText(QString("

%1

%2").arg(caption).arg(text)); msgBox->setInformativeText(QString("%1").arg(retrieveProjectNameFromTempPath(recoverPath))); msgBox->setStandardButtons(QMessageBox::Open | QMessageBox::Discard); - + msgBox->setProperty("RecoverPath", recoverPath); hideQuestionMark(*msgBox); - connect(msgBox, &QMessageBox::finished, [recoverPath, this](int result) - { - if (QMessageBox::Open == result) - { - FileManager fm; - Object* o = fm.recoverUnsavedProject(recoverPath); - if (fm.error().ok()) - { - mEditor->setObject(o); - updateSaveState(); - } - } - }); + connect(msgBox, &QMessageBox::finished, this, &MainWindow2::startProjectRecovery); msgBox->open(); } + +void MainWindow2::startProjectRecovery(int result) +{ + if (result != QMessageBox::Open) + { + return; + } + QMessageBox* msgBox = dynamic_cast(QObject::sender()); + const QString recoverPath = msgBox->property("RecoverPath").toString(); + + FileManager fm; + Object* o = fm.recoverUnsavedProject(recoverPath); + if (!fm.error().ok()) + { + QMessageBox::information(this, tr("Recovery Failed."), tr("Sorry! Pencil2D is unable to restore your project")); + } + + mEditor->setObject(o); + updateSaveState(); + + QMessageBox::information(this, tr("Recovery Succeeded!"), tr("Please save your work immediately to prevent loss of data")); +} diff --git a/app/src/mainwindow2.h b/app/src/mainwindow2.h index b8686a720d..085d106c30 100644 --- a/app/src/mainwindow2.h +++ b/app/src/mainwindow2.h @@ -152,6 +152,7 @@ private slots: void bindActionWithSetting(QAction*, const SETTING&); void tryRecoverUnsavedProject(); + void startProjectRecovery(int result); // UI: Dock widgets ColorBox* mColorBox = nullptr; From d751c8b871465786757644df4158d6e32f991048 Mon Sep 17 00:00:00 2001 From: Matt Chang Date: Thu, 20 Aug 2020 16:07:08 +1000 Subject: [PATCH 05/10] Remove c++14 feature std::make_unique() --- app/src/mainwindow2.cpp | 1 + core_lib/src/structure/filemanager.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/mainwindow2.cpp b/app/src/mainwindow2.cpp index 9cd26cdcea..b8afed4059 100644 --- a/app/src/mainwindow2.cpp +++ b/app/src/mainwindow2.cpp @@ -1579,6 +1579,7 @@ void MainWindow2::startProjectRecovery(int result) Object* o = fm.recoverUnsavedProject(recoverPath); if (!fm.error().ok()) { + Q_ASSERT(o == nullptr); QMessageBox::information(this, tr("Recovery Failed."), tr("Sorry! Pencil2D is unable to restore your project")); } diff --git a/core_lib/src/structure/filemanager.cpp b/core_lib/src/structure/filemanager.cpp index 55181c5faa..28d85a6eaf 100644 --- a/core_lib/src/structure/filemanager.cpp +++ b/core_lib/src/structure/filemanager.cpp @@ -682,7 +682,7 @@ Object* FileManager::recoverUnsavedProject(QString intermeidatePath) const QString mainXMLPath = projectDir.filePath(PFF_XML_FILE_NAME); const QString dataFolder = projectDir.filePath(PFF_DATA_DIR); - std::unique_ptr object = std::make_unique(); + std::unique_ptr object(new Object); object->setWorkingDir(intermeidatePath); object->setMainXMLFile(mainXMLPath); object->setDataDir(dataFolder); From ea15c2c53e4e4aad71c7bb5f8fdf6e25710bdf09 Mon Sep 17 00:00:00 2001 From: Matt Chang Date: Sat, 22 Aug 2020 14:33:30 +1000 Subject: [PATCH 06/10] Apply message styles --- app/src/mainwindow2.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/mainwindow2.cpp b/app/src/mainwindow2.cpp index b8afed4059..38c89e760e 100644 --- a/app/src/mainwindow2.cpp +++ b/app/src/mainwindow2.cpp @@ -1580,11 +1580,15 @@ void MainWindow2::startProjectRecovery(int result) if (!fm.error().ok()) { Q_ASSERT(o == nullptr); - QMessageBox::information(this, tr("Recovery Failed."), tr("Sorry! Pencil2D is unable to restore your project")); + const QString title = tr("Recovery Failed."); + const QString text = tr("Sorry! Pencil2D is unable to restore your project"); + QMessageBox::information(this, title, QString("

%1

%2").arg(title).arg(text)); } mEditor->setObject(o); updateSaveState(); - QMessageBox::information(this, tr("Recovery Succeeded!"), tr("Please save your work immediately to prevent loss of data")); + const QString title = tr("Recovery Succeeded!"); + const QString text = tr("Please save your work immediately to prevent loss of data"); + QMessageBox::information(this, title, QString("

%1

%2").arg(title).arg(text)); } From b03226757badb1377fb962e341d5c1cdb8bad6ef Mon Sep 17 00:00:00 2001 From: Matt Chang Date: Sat, 22 Aug 2020 14:36:02 +1000 Subject: [PATCH 07/10] Delete the temp folder if the user presses Discard button --- app/src/mainwindow2.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/mainwindow2.cpp b/app/src/mainwindow2.cpp index 38c89e760e..261bf1d7e7 100644 --- a/app/src/mainwindow2.cpp +++ b/app/src/mainwindow2.cpp @@ -1568,12 +1568,15 @@ void MainWindow2::tryRecoverUnsavedProject() void MainWindow2::startProjectRecovery(int result) { + QMessageBox* msgBox = dynamic_cast(QObject::sender()); + const QString recoverPath = msgBox->property("RecoverPath").toString(); + if (result != QMessageBox::Open) { + // The user presses discard + QDir(recoverPath).removeRecursively(); return; } - QMessageBox* msgBox = dynamic_cast(QObject::sender()); - const QString recoverPath = msgBox->property("RecoverPath").toString(); FileManager fm; Object* o = fm.recoverUnsavedProject(recoverPath); From b3febae7e2bc75819360f9710491a97a779e44b1 Mon Sep 17 00:00:00 2001 From: Matt Chang Date: Sun, 23 Aug 2020 01:17:12 +1000 Subject: [PATCH 08/10] No preset dialog at app startup if there is a project recovery dialog --- app/src/mainwindow2.cpp | 19 ++++++++++++------- app/src/mainwindow2.h | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/mainwindow2.cpp b/app/src/mainwindow2.cpp index 261bf1d7e7..c5e114c525 100644 --- a/app/src/mainwindow2.cpp +++ b/app/src/mainwindow2.cpp @@ -132,8 +132,6 @@ MainWindow2::MainWindow2(QWidget* parent) : ui->background->init(mEditor->preference()); setWindowTitle(PENCIL_WINDOW_TITLE); - - tryLoadPreset(); } MainWindow2::~MainWindow2() @@ -524,7 +522,11 @@ void MainWindow2::showEvent(QShowEvent*) if (firstShowEvent) { firstShowEvent = false; - tryRecoverUnsavedProject(); + bool needRecovery = tryRecoverUnsavedProject(); + if (!needRecovery) + { + tryLoadPreset(); + } } } @@ -1536,14 +1538,14 @@ void MainWindow2::displayMessageBoxNoTitle(const QString& body) QMessageBox::information(this, nullptr, tr(qPrintable(body)), QMessageBox::Ok); } -void MainWindow2::tryRecoverUnsavedProject() +bool MainWindow2::tryRecoverUnsavedProject() { FileManager fm; QStringList recoverables = fm.searchForUnsavedProjects(); if (recoverables.size() == 0) { - return; + return false; } QString caption = tr("Restore Project?"); @@ -1564,19 +1566,22 @@ void MainWindow2::tryRecoverUnsavedProject() connect(msgBox, &QMessageBox::finished, this, &MainWindow2::startProjectRecovery); msgBox->open(); + return true; } void MainWindow2::startProjectRecovery(int result) { - QMessageBox* msgBox = dynamic_cast(QObject::sender()); + const QMessageBox* msgBox = dynamic_cast(QObject::sender()); const QString recoverPath = msgBox->property("RecoverPath").toString(); - if (result != QMessageBox::Open) + if (result == QMessageBox::Discard) { // The user presses discard QDir(recoverPath).removeRecursively(); + tryLoadPreset(); return; } + Q_ASSERT(result == QMessageBox::Open); FileManager fm; Object* o = fm.recoverUnsavedProject(recoverPath); diff --git a/app/src/mainwindow2.h b/app/src/mainwindow2.h index 085d106c30..b3b46b4ccb 100644 --- a/app/src/mainwindow2.h +++ b/app/src/mainwindow2.h @@ -151,7 +151,7 @@ private slots: void bindActionWithSetting(QAction*, const SETTING&); - void tryRecoverUnsavedProject(); + bool tryRecoverUnsavedProject(); void startProjectRecovery(int result); // UI: Dock widgets From 84a794d74d21e1b284785e806487b48e33809c41 Mon Sep 17 00:00:00 2001 From: Matt Chang Date: Mon, 24 Aug 2020 11:58:32 +1000 Subject: [PATCH 09/10] Fix a wrong condition --- core_lib/src/structure/filemanager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core_lib/src/structure/filemanager.cpp b/core_lib/src/structure/filemanager.cpp index 28d85a6eaf..755d090b7b 100644 --- a/core_lib/src/structure/filemanager.cpp +++ b/core_lib/src/structure/filemanager.cpp @@ -716,7 +716,7 @@ Status FileManager::recoverObject(Object* object) mainXmlOK &= (!root.isNull()); QDomElement objectTag = root.firstChildElement("object"); - mainXmlOK &= (objectTag.isNull()); + mainXmlOK &= (objectTag.isNull() == false); if (mainXmlOK == false) { @@ -871,4 +871,4 @@ int FileManager::framePosFromFilename(const QString& filename) return tokens[1].toInt(); } return -1; -} \ No newline at end of file +} From 3d0b295fd362f23d235bc5f04d3c7661899603c5 Mon Sep 17 00:00:00 2001 From: Matt Chang Date: Mon, 24 Aug 2020 12:08:55 +1000 Subject: [PATCH 10/10] Close file early because there could be a 2nd file open soon --- core_lib/src/structure/filemanager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core_lib/src/structure/filemanager.cpp b/core_lib/src/structure/filemanager.cpp index 755d090b7b..f60c74e81c 100644 --- a/core_lib/src/structure/filemanager.cpp +++ b/core_lib/src/structure/filemanager.cpp @@ -705,6 +705,7 @@ Status FileManager::recoverObject(Object* object) QFile file(object->mainXMLFile()); mainXmlOK &= file.exists(); mainXmlOK &= file.open(QFile::ReadOnly); + file.close(); QDomDocument xmlDoc; mainXmlOK &= xmlDoc.setContent(&file);