diff --git a/app/src/mainwindow2.cpp b/app/src/mainwindow2.cpp index a2401e3627..93329fea62 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 @@ -132,9 +132,6 @@ MainWindow2::MainWindow2(QWidget* parent) : ui->background->init(mEditor->preference()); setWindowTitle(PENCIL_WINDOW_TITLE); - - if (!loadMostRecent()) - tryLoadPreset(); } MainWindow2::~MainWindow2() @@ -519,6 +516,19 @@ void MainWindow2::closeEvent(QCloseEvent* event) } } +void MainWindow2::showEvent(QShowEvent*) +{ + static bool firstShowEvent = true; + if (firstShowEvent) + { + firstShowEvent = false; + if (tryRecoverUnsavedProject()) { return; } + if (loadMostRecent()) { return; } + if (tryLoadPreset()) { return; } + newObject(); + } +} + void MainWindow2::tabletEvent(QTabletEvent* event) { event->ignore(); @@ -528,7 +538,10 @@ void MainWindow2::newDocument() { if (maybeSave()) { - tryLoadPreset(); + if (!tryLoadPreset()) + { + newObject(); + } } } @@ -641,7 +654,7 @@ bool MainWindow2::openObject(QString strFilePath) dd.collect(error.details()); ErrorDialog errorDialog(error.title(), error.description(), dd.str()); errorDialog.exec(); - newEmptyDocumentAfterErrorOccurred(); + emptyDocumentWhenErrorOccurred(); return false; } @@ -651,7 +664,7 @@ bool MainWindow2::openObject(QString strFilePath) tr("An unknown error occurred while trying to load the file and we are not able to load your file."), QString("Raw file path: %1\nResolved file path: %2").arg(strFilePath, fullPath)); errorDialog.exec(); - newEmptyDocumentAfterErrorOccurred(); + emptyDocumentWhenErrorOccurred(); return false; } @@ -816,7 +829,7 @@ bool MainWindow2::autoSave() return false; } -void MainWindow2::newEmptyDocumentAfterErrorOccurred() +void MainWindow2::emptyDocumentWhenErrorOccurred() { newObject(); @@ -1062,19 +1075,21 @@ bool MainWindow2::newObject() bool MainWindow2::newObjectFromPresets(int presetIndex) { Object* object = nullptr; - QString presetFilePath = (presetIndex > 0) ? PresetDialog::getPresetPath(presetIndex) : ""; - if (!presetFilePath.isEmpty()) + QString presetFilePath = PresetDialog::getPresetPath(presetIndex); + + if (presetFilePath.isEmpty()) { - FileManager fm(this); - object = fm.load(presetFilePath); - if (fm.error().ok() == false) object = nullptr; + return false; } - if (object == nullptr) + + FileManager fm(this); + object = fm.load(presetFilePath); + + if (fm.error().ok() == false || object == nullptr) { - object = new Object(); - object->init(); - object->createDefaultLayers(); + return false; } + mEditor->setObject(object); object->setFilePath(QString()); @@ -1099,7 +1114,7 @@ bool MainWindow2::loadMostRecent() return false; } -void MainWindow2::tryLoadPreset() +bool MainWindow2::tryLoadPreset() { if (mEditor->preference()->isOn(SETTING::ASK_FOR_PRESET)) { @@ -1110,12 +1125,15 @@ void MainWindow2::tryLoadPreset() if (result == QDialog::Accepted) { int presetIndex = presetDialog->getPresetIndex(); - if (presetDialog->shouldAlwaysUse()) { + if (presetDialog->shouldAlwaysUse()) + { mEditor->preference()->set(SETTING::ASK_FOR_PRESET, false); mEditor->preference()->set(SETTING::DEFAULT_PRESET, presetIndex); } - newObjectFromPresets(presetIndex); - qDebug() << "Accepted!"; + if (!newObjectFromPresets(presetIndex)) + { + newObject(); + } } }); presetDialog->open(); @@ -1123,8 +1141,9 @@ void MainWindow2::tryLoadPreset() else { int defaultPreset = mEditor->preference()->getInt(SETTING::DEFAULT_PRESET); - newObjectFromPresets(defaultPreset); + return newObjectFromPresets(defaultPreset); } + return true; } void MainWindow2::readSettings() @@ -1541,3 +1560,66 @@ void MainWindow2::displayMessageBoxNoTitle(const QString& body) { QMessageBox::information(this, nullptr, tr(qPrintable(body)), QMessageBox::Ok); } + +bool MainWindow2::tryRecoverUnsavedProject() +{ + FileManager fm; + QStringList recoverables = fm.searchForUnsavedProjects(); + + if (recoverables.size() == 0) + { + return false; + } + + 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); + msgBox->setProperty("RecoverPath", recoverPath); + hideQuestionMark(*msgBox); + + connect(msgBox, &QMessageBox::finished, this, &MainWindow2::startProjectRecovery); + msgBox->open(); + return true; +} + +void MainWindow2::startProjectRecovery(int result) +{ + const QMessageBox* msgBox = dynamic_cast(QObject::sender()); + const QString recoverPath = msgBox->property("RecoverPath").toString(); + + 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); + if (!fm.error().ok()) + { + Q_ASSERT(o == nullptr); + 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(); + + 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)); +} diff --git a/app/src/mainwindow2.h b/app/src/mainwindow2.h index c3d4f3bb32..07adc52232 100644 --- a/app/src/mainwindow2.h +++ b/app/src/mainwindow2.h @@ -77,7 +77,7 @@ public slots: bool saveAsNewDocument(); bool maybeSave(); bool autoSave(); - void newEmptyDocumentAfterErrorOccurred(); + void emptyDocumentWhenErrorOccurred(); // import void importImage(); @@ -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(); @@ -128,7 +129,7 @@ private slots: void clearKeyboardShortcuts(); void updateZoomLabel(); bool loadMostRecent(); - void tryLoadPreset(); + bool tryLoadPreset(); void openPalette(); void importPalette(); @@ -151,6 +152,9 @@ private slots: void bindActionWithSetting(QAction*, const SETTING&); + bool tryRecoverUnsavedProject(); + void startProjectRecovery(int result); + // UI: Dock widgets ColorBox* mColorBox = nullptr; ColorPaletteWidget* mColorPalette = nullptr; @@ -182,7 +186,6 @@ private slots: // whether we are currently importing an image sequence. bool mIsImportingImageSequence = false; -// bool mLoadMostRecent = true; Ui::MainWindow2* ui = nullptr; }; diff --git a/app/src/presetdialog.cpp b/app/src/presetdialog.cpp index db738bb9c7..d7623a9d4f 100644 --- a/app/src/presetdialog.cpp +++ b/app/src/presetdialog.cpp @@ -45,11 +45,19 @@ bool PresetDialog::shouldAlwaysUse() QString PresetDialog::getPresetPath(int index) { - QString filename = QString("%1.pclx").arg(index); + if (index == 0) + { + return QString(); + } + + const QString filename = QString("%1.pclx").arg(index); QDir dataDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); if (dataDir.cd("presets")) { - return dataDir.filePath(filename); + if (dataDir.exists(filename)) + { + return dataDir.filePath(filename); + } } return QString(); } @@ -77,7 +85,7 @@ void PresetDialog::initPresets() return; } QSettings presets(dataDir.filePath("presets.ini"), QSettings::IniFormat, this); - + bool ok = true; for (const QString& key : presets.allKeys()) { diff --git a/core_lib/src/structure/filemanager.cpp b/core_lib/src/structure/filemanager.cpp index 1ab0e78b0e..f60c74e81c 100644 --- a/core_lib/src/structure/filemanager.cpp +++ b/core_lib/src/structure/filemanager.cpp @@ -630,3 +630,246 @@ 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(new Object); + object->setWorkingDir(intermeidatePath); + object->setMainXMLFile(mainXMLPath); + object->setDataDir(dataFolder); + + Status st = recoverObject(object.get()); + if (!st.ok()) + { + mError = st; + return nullptr; + } + // Transfer ownership to the caller + return object.release(); +} + +Status FileManager::recoverObject(Object* object) +{ + // Check whether the main.xml is fine, if not we should make a valid one. + bool mainXmlOK = true; + + QFile file(object->mainXMLFile()); + mainXmlOK &= file.exists(); + mainXmlOK &= file.open(QFile::ReadOnly); + file.close(); + + QDomDocument xmlDoc; + mainXmlOK &= xmlDoc.setContent(&file); + + QDomDocumentType type = xmlDoc.doctype(); + mainXmlOK &= (type.name() == "PencilDocument" || type.name() == "MyObject"); + + QDomElement root = xmlDoc.documentElement(); + mainXmlOK &= (!root.isNull()); + + QDomElement objectTag = root.firstChildElement("object"); + mainXmlOK &= (objectTag.isNull() == false); + + 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); + + 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; +} diff --git a/core_lib/src/structure/filemanager.h b/core_lib/src/structure/filemanager.h index ee4d257a2c..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; @@ -45,6 +46,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 +72,17 @@ class FileManager : public QObject void progressForward(); + +private: // Project recovery + 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; QString mstrLastTempFolder; 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 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;