support backup file

Add configs:

- backup_directory
- backup_extension
- enable_backup_file
This commit is contained in:
Le Tan 2017-11-16 19:59:49 +08:00
parent 141b404240
commit e6ce66ec7d
15 changed files with 411 additions and 17 deletions

View File

@ -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 <br>
; Convert '\n' in paragraphs into <br/>
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

View File

@ -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,

View File

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

View File

@ -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 &section, 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

View File

@ -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:

View File

@ -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()
{
}

View File

@ -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<VFile> 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);

View File

@ -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();
}
}

View File

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

View File

@ -3,7 +3,15 @@
#include <QDir>
#include <QTextEdit>
#include <QFileInfo>
#include <QDebug>
#include <QFile>
#include <QTextStream>
#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);
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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 <span style=\"%1\">%2</span> "
"when opening note <span style=\"%1\">%3</span>.")
.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.<br/>"
"Please choose to recover from the backup file or delete it.<br/><br/>"
"Note file last modified: <span style=\"%1\">%2</span><br/>"
"Backup file last modified: <span style=\"%1\">%3</span><br/>"
"Content comparison: <span style=\"%1\">%4</span>")
.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;
}

View File

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