diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index f5234460..133f6939 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -116,7 +116,7 @@ tool_bar_icon_size=18 ; Markdown-it options ; Enable HTML tags in source markdownit_opt_html=true -; Convert '\n' in paragraphs into
+; Convert '\n' in paragraphs into
markdownit_opt_breaks=false ; Auto-convert URL-like text to links markdownit_opt_linkify=true @@ -155,6 +155,16 @@ startup_pages= ; Timer interval to check file modification or save file to tmp file in milliseconds file_timer_interval=2000 +; Directory for the backup file +; A directory "." means to put the backup file in the same directory as the edited file +backup_directory=. + +; String which is appended to a file name to make the name of the backup file +backup_extension=.vswp + +; Enable back file +enable_backup_file=true + [web] ; Location and configuration for Mathjax mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 3a5ae0f3..6520fd69 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -891,7 +891,14 @@ bool VUtils::deleteFile(const VNotebook *p_notebook, bool VUtils::deleteFile(const QString &p_path) { QFile file(p_path); - return file.remove(); + bool ret = file.remove(); + if (ret) { + qDebug() << "deleted file" << p_path; + } else { + qWarning() << "fail to delete file" << p_path; + } + + return ret; } bool VUtils::deleteFile(const VOrphanFile *p_file, diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index 944a7bee..a1f970a1 100644 --- a/src/vconfigmanager.cpp +++ b/src/vconfigmanager.cpp @@ -268,6 +268,18 @@ void VConfigManager::initialize() if (m_fileTimerInterval < 100) { m_fileTimerInterval = 100; } + + m_backupDirectory = getConfigFromSettings("global", + "backup_directory").toString(); + + m_backupExtension = getConfigFromSettings("global", + "backup_extension").toString(); + if (m_backupExtension.isEmpty()) { + m_backupExtension = "."; + } + + m_enableBackupFile = getConfigFromSettings("global", + "enable_backup_file").toBool(); } void VConfigManager::initSettings() diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 97b5a8e2..b48294da 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -375,6 +375,15 @@ public: // Return the timer interval for checking file. int getFileTimerInterval() const; + // Get the backup directory. + const QString &getBackupDirectory() const; + + // Get the backup file extension. + const QString &getBackupExtension() const; + + // Whether backup file is enabled. + bool getEnableBackupFile() const; + private: // Look up a config from user and default settings. QVariant getConfigFromSettings(const QString §ion, const QString &key) const; @@ -709,6 +718,15 @@ private: // Timer interval to check file in milliseconds. int m_fileTimerInterval; + // Directory for the backup file (relative or absolute path). + QString m_backupDirectory; + + // Extension of the backup file. + QString m_backupExtension; + + // Whether enable backup file. + bool m_enableBackupFile; + // The name of the config file in each directory, obsolete. // Use c_dirConfigFile instead. static const QString c_obsoleteDirConfigFile; @@ -1790,4 +1808,19 @@ inline int VConfigManager::getFileTimerInterval() const return m_fileTimerInterval; } +inline const QString &VConfigManager::getBackupDirectory() const +{ + return m_backupDirectory; +} + +inline const QString &VConfigManager::getBackupExtension() const +{ + return m_backupExtension; +} + +inline bool VConfigManager::getEnableBackupFile() const +{ + return m_enableBackupFile; +} + #endif // VCONFIGMANAGER_H diff --git a/src/veditor.h b/src/veditor.h index 63a2e892..733b7bcc 100644 --- a/src/veditor.h +++ b/src/veditor.h @@ -139,6 +139,11 @@ public: void setVimMode(VimMode p_mode); + virtual QString getContent() const = 0; + + // @p_modified: if true, delete the whole content and insert the new content. + virtual void setContent(const QString &p_content, bool p_modified = false) = 0; + // Wrapper functions for QPlainTextEdit/QTextEdit. // Ends with W to distinguish it from the original interfaces. public: diff --git a/src/vedittab.cpp b/src/vedittab.cpp index 7f0d2f2d..6822112a 100644 --- a/src/vedittab.cpp +++ b/src/vedittab.cpp @@ -15,7 +15,9 @@ VEditTab::VEditTab(VFile *p_file, VEditArea *p_editArea, QWidget *p_parent) m_currentHeader(p_file, -1), m_editArea(p_editArea), m_checkFileChange(true), - m_fileDiverged(false) + m_fileDiverged(false), + m_ready(0), + m_enableBackupFile(g_config->getEnableBackupFile()) { connect(qApp, &QApplication::focusChanged, this, &VEditTab::handleFocusChanged); @@ -181,3 +183,7 @@ void VEditTab::reloadFromDisk() m_checkFileChange = true; reload(); } + +void VEditTab::writeBackupFile() +{ +} diff --git a/src/vedittab.h b/src/vedittab.h index 8be6fcd0..445b5e42 100644 --- a/src/vedittab.h +++ b/src/vedittab.h @@ -124,6 +124,9 @@ protected: // Return true if succeed. virtual bool restoreFromTabInfo(const VEditTabInfo &p_info) = 0; + // Write modified buffer content to backup file. + virtual void writeBackupFile(); + // File related to this tab. QPointer m_file; @@ -146,6 +149,12 @@ protected: // File has diverged from disk. bool m_fileDiverged; + // Tab has been ready or not. + int m_ready; + + // Whether backup file is enabled. + bool m_enableBackupFile; + signals: void getFocused(); @@ -161,6 +170,9 @@ signals: void vimStatusUpdated(const VVim *p_vim); + // Request to close itself. + void closeRequested(VEditTab *p_tab); + private slots: // Called when app focus changed. void handleFocusChanged(QWidget *p_old, QWidget *p_now); diff --git a/src/veditwindow.cpp b/src/veditwindow.cpp index 6367fea4..656a0848 100644 --- a/src/veditwindow.cpp +++ b/src/veditwindow.cpp @@ -960,6 +960,8 @@ void VEditWindow::connectEditTab(const VEditTab *p_tab) this, &VEditWindow::handleTabStatusMessage); connect(p_tab, &VEditTab::vimStatusUpdated, this, &VEditWindow::handleTabVimStatusUpdated); + connect(p_tab, &VEditTab::closeRequested, + this, &VEditWindow::tabRequestToClose); } void VEditWindow::setCurrentWindow(bool p_current) @@ -1094,3 +1096,16 @@ void VEditWindow::checkFileChangeOutside() getTab(i)->checkFileChangeOutside(); } } + +void VEditWindow::tabRequestToClose(VEditTab *p_tab) +{ + bool ok = p_tab->closeFile(false); + if (ok) { + removeTab(indexOf(p_tab)); + + // Disconnect all the signals. + disconnect(p_tab, 0, this, 0); + + p_tab->deleteLater(); + } +} diff --git a/src/veditwindow.h b/src/veditwindow.h index 5e3bfb15..8b64f55d 100644 --- a/src/veditwindow.h +++ b/src/veditwindow.h @@ -138,6 +138,9 @@ private slots: // Handle the statusUpdated signal of VEditTab. void handleTabStatusUpdated(const VEditTabInfo &p_info); + // @p_tab request to close itself. + void tabRequestToClose(VEditTab *p_tab); + private: void initTabActions(); void setupCornerWidget(); diff --git a/src/vfile.cpp b/src/vfile.cpp index 10cfa379..37547dab 100644 --- a/src/vfile.cpp +++ b/src/vfile.cpp @@ -3,7 +3,15 @@ #include #include #include +#include +#include +#include #include "utils/vutils.h" +#include "vconfigmanager.h" + +extern VConfigManager *g_config; + +const QString VFile::c_backupFileHeadMagic = "vnote_backup_file_826537664"; VFile::VFile(QObject *p_parent, const QString &p_name, @@ -50,6 +58,11 @@ void VFile::close() } m_content.clear(); + if (!m_backupName.isEmpty()) { + VUtils::deleteFile(fetchBackupFilePath()); + m_backupName.clear(); + } + m_opened = false; } @@ -57,6 +70,7 @@ bool VFile::save() { Q_ASSERT(m_opened); Q_ASSERT(m_modifiable); + bool ret = VUtils::writeFileToDisk(fetchPath(), m_content); if (ret) { m_lastModified = QFileInfo(fetchPath()).lastModified(); @@ -110,3 +124,87 @@ void VFile::reload() m_content = VUtils::readFileFromDisk(filePath); m_lastModified = QFileInfo(filePath).lastModified(); } + +QString VFile::backupFileOfPreviousSession() const +{ + Q_ASSERT(m_modifiable && m_backupName.isEmpty()); + + QString basePath = QDir(fetchBasePath()).filePath(g_config->getBackupDirectory()); + QDir dir(basePath); + + QStringList files = getPotentialBackupFiles(basePath); + foreach (const QString &file, files) { + QString filePath = dir.filePath(file); + if (isBackupFile(filePath)) { + return filePath; + } + } + + return QString(); +} + +QString VFile::fetchBackupFilePath() +{ + QString basePath = QDir(fetchBasePath()).filePath(g_config->getBackupDirectory()); + QDir dir(basePath); + + if (m_backupName.isEmpty()) { + m_backupName = VUtils::getFileNameWithSequence(basePath, + m_name + g_config->getBackupExtension(), + true); + + m_lastBackupFilePath = dir.filePath(m_backupName); + } else { + QString filePath = dir.filePath(m_backupName); + if (filePath != m_lastBackupFilePath) { + // File has been moved. + // Delete the original backup file if it still exists. + VUtils::deleteFile(m_lastBackupFilePath); + + m_lastBackupFilePath = filePath; + } + } + + return m_lastBackupFilePath; +} + +QStringList VFile::getPotentialBackupFiles(const QString &p_dir) const +{ + QString nameFilter = QString("%1*%2").arg(m_name).arg(g_config->getBackupExtension()); + QStringList files = QDir(p_dir).entryList(QStringList(nameFilter), + QDir::Files + | QDir::Hidden + | QDir::NoSymLinks + | QDir::NoDotAndDotDot); + return files; +} + +bool VFile::isBackupFile(const QString &p_file) const +{ + QFile file(p_file); + if (!file.open(QFile::ReadOnly | QIODevice::Text)) { + return false; + } + + QTextStream st(&file); + QString head = st.readLine(); + return head == fetchBackupFileHead(); +} + +QString VFile::fetchBackupFileHead() const +{ + return c_backupFileHeadMagic + " " + fetchPath(); +} + +bool VFile::writeBackupFile(const QString &p_content) +{ + return VUtils::writeFileToDisk(fetchBackupFilePath(), + fetchBackupFileHead() + "\n" + p_content); +} + +QString VFile::readBackupFile(const QString &p_file) +{ + const QString content = VUtils::readFileFromDisk(p_file); + int idx = content.indexOf("\n"); + return content.mid(idx + 1); +} diff --git a/src/vfile.h b/src/vfile.h index 5f4e5694..b1cbb36e 100644 --- a/src/vfile.h +++ b/src/vfile.h @@ -79,6 +79,14 @@ public: // Whether this file was changed outside VNote. bool isChangedOutside() const; + // Return backup file of previous session if there exists one. + QString backupFileOfPreviousSession() const; + + // Write @p_content to backup file. + bool writeBackupFile(const QString &p_content); + + QString readBackupFile(const QString &p_file); + protected: // Name of this file. QString m_name; @@ -107,6 +115,25 @@ protected: // Last modified date and local time when the file is last modified // corresponding to m_content. QDateTime m_lastModified; + + // Name of the backup file. + QString m_backupName; + + // Used to identify file path change. + QString m_lastBackupFilePath; + +private: + // Fetch backup file path. + QString fetchBackupFilePath(); + + QStringList getPotentialBackupFiles(const QString &p_dir) const; + + // Read the file content to check if it is a backup file. + bool isBackupFile(const QString &p_file) const; + + QString fetchBackupFileHead() const; + + static const QString c_backupFileHeadMagic; }; inline const QString &VFile::getName() const diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index 3e295e06..65ec0536 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -59,6 +59,11 @@ VMdEditor::VMdEditor(VFile *p_file, connect(m_mdHighlighter, &HGMarkdownHighlighter::highlightCompleted, this, [this]() { makeBlockVisible(textCursor().block()); + + if (m_freshEdit) { + m_freshEdit = false; + emit m_object->ready(); + } }); m_cbHighlighter = new VCodeBlockHighlightHelper(m_mdHighlighter, @@ -106,11 +111,6 @@ void VMdEditor::beginEdit() emit statusChanged(); updateHeaders(m_mdHighlighter->getHeaderRegions()); - - if (m_freshEdit) { - m_freshEdit = false; - emit m_object->ready(); - } } void VMdEditor::endEdit() @@ -161,14 +161,14 @@ void VMdEditor::makeBlockVisible(const QTextBlock &p_block) } QScrollBar *vbar = verticalScrollBar(); - if (!vbar || !vbar->isVisible()) { + if (!vbar || (vbar->minimum() == vbar->maximum())) { // No vertical scrollbar. No need to scroll. return; } int height = rect().height(); QScrollBar *hbar = horizontalScrollBar(); - if (hbar && hbar->isVisible()) { + if (hbar && (hbar->minimum() != hbar->maximum())) { height -= hbar->height(); } @@ -870,3 +870,20 @@ void VMdEditor::updateConfig() updateEditConfig(); updateTextEditConfig(); } + +QString VMdEditor::getContent() const +{ + return toPlainText(); +} + +void VMdEditor::setContent(const QString &p_content, bool p_modified) +{ + if (p_modified) { + QTextCursor cursor = textCursor(); + cursor.select(QTextCursor::Document); + cursor.insertText(p_content); + setTextCursor(cursor); + } else { + setPlainText(p_content); + } +} diff --git a/src/vmdeditor.h b/src/vmdeditor.h index c914a098..b8f0254d 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -55,6 +55,10 @@ public: void updateConfig() Q_DECL_OVERRIDE; + QString getContent() const Q_DECL_OVERRIDE; + + void setContent(const QString &p_content, bool p_modified = false) Q_DECL_OVERRIDE; + public slots: bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE; diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index 8f83fcc9..01a50e30 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -33,7 +33,8 @@ VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea, m_webViewer(NULL), m_document(NULL), m_mdConType(g_config->getMdConverterType()), - m_enableHeadingSequence(false) + m_enableHeadingSequence(false), + m_backupFileChecked(false) { V_ASSERT(m_file->getDocType() == DocType::Markdown); @@ -49,6 +50,14 @@ VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea, setupUI(); + m_backupTimer = new QTimer(this); + m_backupTimer->setSingleShot(true); + m_backupTimer->setInterval(g_config->getFileTimerInterval()); + connect(m_backupTimer, &QTimer::timeout, + this, [this]() { + writeBackupFile(); + }); + if (p_mode == OpenFileMode::Edit) { showFileEditMode(); } else { @@ -325,9 +334,13 @@ bool VMdTab::saveFile() m_editor->saveFile(); ret = m_file->save(); if (!ret) { - VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."), + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to save note."), tr("Fail to write to disk when saving a note. Please try it again."), - QMessageBox::Ok, QMessageBox::Ok, this); + QMessageBox::Ok, + QMessageBox::Ok, + this); m_editor->setModified(true); } else { m_fileDiverged = false; @@ -376,8 +389,17 @@ void VMdTab::setupMarkdownViewer() this, SLOT(updateCurrentHeader(const QString &))); connect(m_document, &VDocument::keyPressed, this, &VMdTab::handleWebKeyPressed); - connect(m_document, SIGNAL(logicsFinished(void)), - this, SLOT(restoreFromTabInfo(void))); + connect(m_document, &VDocument::logicsFinished, + this, [this]() { + if (m_ready & TabReady::ReadMode) { + return; + } + + m_ready |= TabReady::ReadMode; + + tabIsReady(TabReady::ReadMode); + }); + page->setWebChannel(channel); m_webViewer->setHtml(VUtils::generateHtmlTemplate(m_mdConType, false), @@ -417,8 +439,16 @@ void VMdTab::setupMarkdownEditor() this, [this]() { this->m_editArea->getFindReplaceDialog()->closeDialog(); }); - connect(m_editor->object(), SIGNAL(ready(void)), - this, SLOT(restoreFromTabInfo(void))); + connect(m_editor->object(), &VEditorObject::ready, + this, [this]() { + if (m_ready & TabReady::EditMode) { + return; + } + + m_ready |= TabReady::EditMode; + + tabIsReady(TabReady::EditMode); + }); enableHeadingSequence(m_enableHeadingSequence); m_editor->reloadFile(); @@ -842,3 +872,100 @@ void VMdTab::reload() showFileReadMode(); } } + +void VMdTab::tabIsReady(TabReady p_mode) +{ + bool isCurrentMode = m_isEditMode && p_mode == TabReady::EditMode + || !m_isEditMode && p_mode == TabReady::ReadMode; + + if (isCurrentMode) { + restoreFromTabInfo(); + + if (m_enableBackupFile + && !m_backupFileChecked + && m_file->isModifiable()) { + if (!checkPreviousBackupFile()) { + return; + } + } + } + + if (m_enableBackupFile + && m_file->isModifiable() + && p_mode == TabReady::EditMode) { + // contentsChanged will be emitted even the content is not changed. + connect(m_editor->document(), &QTextDocument::contentsChange, + this, [this]() { + if (m_isEditMode) { + m_backupTimer->stop(); + m_backupTimer->start(); + } + }); + } +} + +void VMdTab::writeBackupFile() +{ + Q_ASSERT(m_enableBackupFile && m_file->isModifiable()); + m_file->writeBackupFile(m_editor->getContent()); +} + +bool VMdTab::checkPreviousBackupFile() +{ + m_backupFileChecked = true; + + QString preFile = m_file->backupFileOfPreviousSession(); + if (preFile.isEmpty()) { + return true; + } + + QMessageBox box(QMessageBox::Warning, + tr("Backup File Found"), + tr("Found backup file %2 " + "when opening note %3.") + .arg(g_config->c_dataTextStyle) + .arg(preFile) + .arg(m_file->fetchPath()), + QMessageBox::NoButton, + this); + QString backupContent = m_file->readBackupFile(preFile); + QString info = tr("VNote may crash while editing this note before.
" + "Please choose to recover from the backup file or delete it.

" + "Note file last modified: %2
" + "Backup file last modified: %3
" + "Content comparison: %4") + .arg(g_config->c_dataTextStyle) + .arg(VUtils::displayDateTime(QFileInfo(m_file->fetchPath()).lastModified())) + .arg(VUtils::displayDateTime(QFileInfo(preFile).lastModified())) + .arg(m_file->getContent() == backupContent ? tr("Identical") + : tr("Different")); + box.setInformativeText(info); + QPushButton *recoverBtn = box.addButton(tr("Recover From Backup File"), QMessageBox::YesRole); + box.addButton(tr("Discard Backup File"), QMessageBox::NoRole); + QPushButton *cancelBtn = box.addButton(tr("Cancel"), QMessageBox::RejectRole); + + box.setDefaultButton(cancelBtn); + box.setTextInteractionFlags(Qt::TextSelectableByMouse); + + box.exec(); + QAbstractButton *btn = box.clickedButton(); + if (btn == cancelBtn || !btn) { + // Close current tab. + emit closeRequested(this); + return false; + } else if (btn == recoverBtn) { + // Load content from the backup file. + if (!m_isEditMode) { + showFileEditMode(); + } + + Q_ASSERT(m_editor); + m_editor->setContent(backupContent, true); + + updateStatus(); + } + + VUtils::deleteFile(preFile); + + return true; +} diff --git a/src/vmdtab.h b/src/vmdtab.h index 6f10bee3..481fc49b 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -13,6 +13,7 @@ class QStackedLayout; class VDocument; class VMdEditor; class VInsertSelector; +class QTimer; class VMdTab : public VEditTab { @@ -88,6 +89,9 @@ public slots: // Enter edit mode. void editFile() Q_DECL_OVERRIDE; +protected: + void writeBackupFile() Q_DECL_OVERRIDE; + private slots: // Update m_outline according to @p_tocHtml for read mode. void updateOutlineFromHtml(const QString &p_tocHtml); @@ -115,6 +119,8 @@ private slots: void restoreFromTabInfo(); private: + enum TabReady { None = 0, ReadMode = 0x1, EditMode = 0x2 }; + // Setup UI. void setupUI(); @@ -166,6 +172,13 @@ private: // Prepare insert selector with snippets. VInsertSelector *prepareSnippetSelector(QWidget *p_parent = nullptr); + // Called once read or edit mode is ready. + void tabIsReady(TabReady p_mode); + + // Check if there exists backup file from previous session. + // Return true if we could continue. + bool checkPreviousBackupFile(); + VMdEditor *m_editor; VWebView *m_webViewer; VDocument *m_document; @@ -175,6 +188,11 @@ private: bool m_enableHeadingSequence; QStackedLayout *m_stacks; + + // Timer to write backup file when content has been changed. + QTimer *m_backupTimer; + + bool m_backupFileChecked; }; inline VMdEditor *VMdTab::getEditor()