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()