mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-06 06:19:52 +08:00
578 lines
15 KiB
C++
578 lines
15 KiB
C++
#include "buffer.h"
|
|
|
|
#include <QTimer>
|
|
|
|
#include <notebook/node.h>
|
|
#include <utils/fileutils.h>
|
|
#include <widgets/viewwindow.h>
|
|
#include <utils/pathutils.h>
|
|
|
|
#include <core/configmgr.h>
|
|
#include <core/editorconfig.h>
|
|
|
|
#include "bufferprovider.h"
|
|
#include "exception.h"
|
|
|
|
using namespace vnotex;
|
|
|
|
static vnotex::ID generateBufferID()
|
|
{
|
|
static vnotex::ID id = 0;
|
|
return ++id;
|
|
}
|
|
|
|
Buffer::Buffer(const BufferParameters &p_parameters,
|
|
QObject *p_parent)
|
|
: QObject(p_parent),
|
|
m_provider(p_parameters.m_provider),
|
|
m_id(generateBufferID()),
|
|
m_readOnly(m_provider->isReadOnly())
|
|
{
|
|
m_autoSaveTimer = new QTimer(this);
|
|
m_autoSaveTimer->setSingleShot(true);
|
|
m_autoSaveTimer->setInterval(1000);
|
|
connect(m_autoSaveTimer, &QTimer::timeout,
|
|
this, &Buffer::autoSave);
|
|
|
|
readContent();
|
|
|
|
checkBackupFileOfPreviousSession();
|
|
}
|
|
|
|
Buffer::~Buffer()
|
|
{
|
|
Q_ASSERT(m_attachedViewWindowCount == 0);
|
|
Q_ASSERT(!m_viewWindowToSync);
|
|
Q_ASSERT(!isModified());
|
|
Q_ASSERT(m_backupFilePath.isEmpty());
|
|
}
|
|
|
|
int Buffer::getAttachViewWindowCount() const
|
|
{
|
|
return m_attachedViewWindowCount;
|
|
}
|
|
|
|
void Buffer::attachViewWindow(ViewWindow *p_win)
|
|
{
|
|
Q_UNUSED(p_win);
|
|
Q_ASSERT(!(m_state & StateFlag::Discarded));
|
|
++m_attachedViewWindowCount;
|
|
}
|
|
|
|
void Buffer::detachViewWindow(ViewWindow *p_win)
|
|
{
|
|
Q_UNUSED(p_win);
|
|
Q_ASSERT(p_win != m_viewWindowToSync);
|
|
|
|
--m_attachedViewWindowCount;
|
|
Q_ASSERT(m_attachedViewWindowCount >= 0);
|
|
|
|
if (m_attachedViewWindowCount == 0) {
|
|
emit attachedViewWindowEmpty();
|
|
}
|
|
}
|
|
|
|
ViewWindow *Buffer::createViewWindow(const QSharedPointer<FileOpenParameters> &p_paras, QWidget *p_parent)
|
|
{
|
|
auto window = createViewWindowInternal(p_paras, p_parent);
|
|
Q_ASSERT(window);
|
|
window->attachToBuffer(this, p_paras);
|
|
return window;
|
|
}
|
|
|
|
bool Buffer::match(const Node *p_node) const
|
|
{
|
|
Q_ASSERT(p_node);
|
|
return m_provider->match(p_node);
|
|
}
|
|
|
|
bool Buffer::match(const QString &p_filePath) const
|
|
{
|
|
return m_provider->match(p_filePath);
|
|
}
|
|
|
|
QString Buffer::getName() const
|
|
{
|
|
return m_provider->getName();
|
|
}
|
|
|
|
QString Buffer::getPath() const
|
|
{
|
|
return m_provider->getPath();
|
|
}
|
|
|
|
QString Buffer::getContentPath() const
|
|
{
|
|
return m_provider->getContentPath();
|
|
}
|
|
|
|
QString Buffer::getResourcePath() const
|
|
{
|
|
return m_provider->getResourcePath();
|
|
}
|
|
|
|
ID Buffer::getId() const
|
|
{
|
|
return m_id;
|
|
}
|
|
|
|
const QString &Buffer::getContent() const
|
|
{
|
|
const_cast<Buffer *>(this)->syncContent();
|
|
return m_content;
|
|
}
|
|
|
|
void Buffer::setContent(const QString &p_content, int &p_revision)
|
|
{
|
|
m_viewWindowToSync = nullptr;
|
|
m_content = p_content;
|
|
p_revision = ++m_revision;
|
|
setModified(true);
|
|
m_autoSaveTimer->start();
|
|
emit contentsChanged();
|
|
}
|
|
|
|
void Buffer::invalidateContent(const ViewWindow *p_win,
|
|
const std::function<void(int)> &p_setRevision)
|
|
{
|
|
Q_ASSERT(!m_viewWindowToSync || m_viewWindowToSync == p_win);
|
|
++m_revision;
|
|
p_setRevision(m_revision);
|
|
m_viewWindowToSync = p_win;
|
|
m_autoSaveTimer->start();
|
|
emit contentsChanged();
|
|
}
|
|
|
|
int Buffer::getRevision() const
|
|
{
|
|
return m_revision;
|
|
}
|
|
|
|
void Buffer::syncContent(const ViewWindow *p_win)
|
|
{
|
|
if (m_viewWindowToSync == p_win) {
|
|
syncContent();
|
|
}
|
|
}
|
|
|
|
void Buffer::syncContent()
|
|
{
|
|
if (m_viewWindowToSync) {
|
|
// Need to sync content.
|
|
m_content = m_viewWindowToSync->getLatestContent();
|
|
m_viewWindowToSync = nullptr;
|
|
}
|
|
}
|
|
|
|
bool Buffer::isModified() const
|
|
{
|
|
return m_modified;
|
|
}
|
|
|
|
void Buffer::setModified(bool p_modified)
|
|
{
|
|
if (m_modified == p_modified) {
|
|
return;
|
|
}
|
|
|
|
m_modified = p_modified;
|
|
emit modified(m_modified);
|
|
}
|
|
|
|
bool Buffer::isReadOnly() const
|
|
{
|
|
return m_readOnly;
|
|
}
|
|
|
|
Buffer::OperationCode Buffer::save(bool p_force)
|
|
{
|
|
Q_ASSERT(!m_readOnly);
|
|
if (m_readOnly) {
|
|
return OperationCode::Failed;
|
|
}
|
|
|
|
if (m_modified
|
|
|| p_force
|
|
|| m_state & (StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside)) {
|
|
syncContent();
|
|
|
|
// We do not involve user here to handle file missing and changed outside cases.
|
|
// The active ViewWindow will check this periodically.
|
|
// Check if file still exists.
|
|
if (!p_force && !checkFileExistsOnDisk()) {
|
|
qWarning() << "failed to save buffer due to file missing on disk" << getPath();
|
|
return OperationCode::FileMissingOnDisk;
|
|
}
|
|
|
|
// Check if file is modified outside.
|
|
if (!p_force && checkFileChangedOutside()) {
|
|
qWarning() << "failed to save buffer due to file changed from outside" << getPath();
|
|
return OperationCode::FileChangedOutside;
|
|
}
|
|
|
|
try {
|
|
m_provider->write(m_content);
|
|
} catch (Exception &p_e) {
|
|
qWarning() << "failed to write the buffer content" << getPath() << p_e.what();
|
|
return OperationCode::Failed;
|
|
}
|
|
|
|
setModified(false);
|
|
m_state &= ~(StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside);
|
|
}
|
|
return OperationCode::Success;
|
|
}
|
|
|
|
Buffer::OperationCode Buffer::reload()
|
|
{
|
|
// Check if file is missing.
|
|
if (!checkFileExistsOnDisk()) {
|
|
qWarning() << "failed to save buffer due to file missing on disk" << getPath();
|
|
return OperationCode::FileMissingOnDisk;
|
|
}
|
|
|
|
if (m_modified
|
|
|| m_state & (StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside)) {
|
|
readContent();
|
|
|
|
emit modified(m_modified);
|
|
emit contentsChanged();
|
|
}
|
|
return OperationCode::Success;
|
|
}
|
|
|
|
void Buffer::readContent()
|
|
{
|
|
m_content = m_provider->read();
|
|
++m_revision;
|
|
|
|
// Reset state.
|
|
m_viewWindowToSync = nullptr;
|
|
m_modified = false;
|
|
}
|
|
|
|
void Buffer::discard()
|
|
{
|
|
Q_ASSERT(!(m_state & StateFlag::Discarded));
|
|
Q_ASSERT(m_attachedViewWindowCount == 1);
|
|
m_autoSaveTimer->stop();
|
|
m_content.clear();
|
|
m_state |= StateFlag::Discarded;
|
|
++m_revision;
|
|
|
|
m_viewWindowToSync = nullptr;
|
|
m_modified = false;
|
|
}
|
|
|
|
void Buffer::close()
|
|
{
|
|
// Delete the backup file if exists.
|
|
m_autoSaveTimer->stop();
|
|
if (!m_backupFilePath.isEmpty()) {
|
|
FileUtils::removeFile(m_backupFilePath);
|
|
m_backupFilePath.clear();
|
|
}
|
|
}
|
|
|
|
QString Buffer::getImageFolderPath() const
|
|
{
|
|
return const_cast<Buffer *>(this)->m_provider->fetchImageFolderPath();
|
|
}
|
|
|
|
QString Buffer::insertImage(const QString &p_srcImagePath, const QString &p_imageFileName)
|
|
{
|
|
Q_UNUSED(p_srcImagePath);
|
|
Q_UNUSED(p_imageFileName);
|
|
Q_ASSERT_X(false, "insertImage", "image insert is not supported");
|
|
return QString();
|
|
}
|
|
|
|
QString Buffer::insertImage(const QImage &p_image, const QString &p_imageFileName)
|
|
{
|
|
Q_UNUSED(p_image);
|
|
Q_UNUSED(p_imageFileName);
|
|
Q_ASSERT_X(false, "insertImage", "image insert is not supported");
|
|
return QString();
|
|
}
|
|
|
|
void Buffer::removeImage(const QString &p_imagePath)
|
|
{
|
|
Q_UNUSED(p_imagePath);
|
|
Q_ASSERT_X(false, "removeImage", "image remove is not supported");
|
|
}
|
|
|
|
void Buffer::autoSave()
|
|
{
|
|
if (m_readOnly) {
|
|
m_autoSaveTimer->stop();
|
|
return;
|
|
}
|
|
|
|
if (m_state & (StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside)) {
|
|
qDebug() << "disable AutoSave due to file missing on disk or changed outside";
|
|
return;
|
|
}
|
|
Q_ASSERT(!(m_state & StateFlag::Discarded));
|
|
auto policy = ConfigMgr::getInst().getEditorConfig().getAutoSavePolicy();
|
|
switch (policy) {
|
|
case EditorConfig::AutoSavePolicy::None:
|
|
return;
|
|
|
|
case EditorConfig::AutoSavePolicy::AutoSave:
|
|
if (save(false) != OperationCode::Success) {
|
|
qWarning() << "AutoSave failed to save buffer, retry later";
|
|
} else {
|
|
emit autoSaved();
|
|
}
|
|
break;
|
|
|
|
case EditorConfig::AutoSavePolicy::BackupFile:
|
|
try {
|
|
writeBackupFile();
|
|
} catch (Exception &p_e) {
|
|
qWarning() << "AutoSave failed to write backup file, retry later" << p_e.what();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Buffer::writeBackupFile()
|
|
{
|
|
if (m_backupFilePath.isEmpty()) {
|
|
const auto &config = ConfigMgr::getInst().getEditorConfig();
|
|
QString backupDirPath(QDir(getResourcePath()).filePath(config.getBackupFileDirectory()));
|
|
backupDirPath = QDir::cleanPath(backupDirPath);
|
|
auto backupFileName = FileUtils::generateFileNameWithSequence(backupDirPath,
|
|
getName(),
|
|
config.getBackupFileExtension());
|
|
QDir backupDir(backupDirPath);
|
|
backupDir.mkpath(backupDirPath);
|
|
m_backupFilePath = backupDir.filePath(backupFileName);
|
|
}
|
|
|
|
Q_ASSERT(m_backupFilePathOfPreviousSession.isEmpty());
|
|
|
|
// Just use FileUtils instead of notebook backend.
|
|
FileUtils::writeFile(m_backupFilePath, generateBackupFileHead() + getContent());
|
|
}
|
|
|
|
QString Buffer::generateBackupFileHead() const
|
|
{
|
|
return QString("vnotex_backup_file %1|").arg(getContentPath());
|
|
}
|
|
|
|
void Buffer::checkBackupFileOfPreviousSession()
|
|
{
|
|
const auto &config = ConfigMgr::getInst().getEditorConfig();
|
|
if (config.getAutoSavePolicy() != EditorConfig::AutoSavePolicy::BackupFile) {
|
|
return;
|
|
}
|
|
|
|
QString backupDirPath(QDir(getResourcePath()).filePath(config.getBackupFileDirectory()));
|
|
backupDirPath = QDir::cleanPath(backupDirPath);
|
|
QDir backupDir(backupDirPath);
|
|
QStringList backupFiles;
|
|
{
|
|
const QString nameFilter = QString("%1*%2").arg(getName(), config.getBackupFileExtension());
|
|
backupFiles = backupDir.entryList(QStringList(nameFilter),
|
|
QDir::Files | QDir::Hidden | QDir::NoSymLinks | QDir::NoDotAndDotDot);
|
|
}
|
|
|
|
if (backupFiles.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
for (const auto &file : backupFiles) {
|
|
const auto filePath = backupDir.filePath(file);
|
|
if (isBackupFileOfBuffer(filePath)) {
|
|
const auto backupContent = readBackupFile(filePath);
|
|
if (backupContent == getContent()) {
|
|
// Found backup file with identical content.
|
|
// Just discard the backup file.
|
|
FileUtils::removeFile(filePath);
|
|
qInfo() << "delete identical backup file of previous session" << filePath;
|
|
} else {
|
|
m_backupFilePathOfPreviousSession = filePath;
|
|
qInfo() << "found backup file of previous session" << filePath;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Buffer::isBackupFileOfBuffer(const QString &p_file) const
|
|
{
|
|
QFile file(p_file);
|
|
if (!file.open(QFile::ReadOnly | QIODevice::Text)) {
|
|
return false;
|
|
}
|
|
|
|
QTextStream st(&file);
|
|
const auto head = st.readLine();
|
|
return head.startsWith(generateBackupFileHead());
|
|
}
|
|
|
|
const QString &Buffer::getBackupFileOfPreviousSession() const
|
|
{
|
|
return m_backupFilePathOfPreviousSession;
|
|
}
|
|
|
|
QString Buffer::readBackupFile(const QString &p_filePath)
|
|
{
|
|
auto content = FileUtils::readTextFile(p_filePath);
|
|
return content.mid(content.indexOf(QLatin1Char('|')) + 1);
|
|
}
|
|
|
|
void Buffer::discardBackupFileOfPreviousSession()
|
|
{
|
|
Q_ASSERT(!m_backupFilePathOfPreviousSession.isEmpty());
|
|
|
|
FileUtils::removeFile(m_backupFilePathOfPreviousSession);
|
|
qInfo() << "discard backup file of previous session" << m_backupFilePathOfPreviousSession;
|
|
m_backupFilePathOfPreviousSession.clear();
|
|
}
|
|
|
|
void Buffer::recoverFromBackupFileOfPreviousSession()
|
|
{
|
|
Q_ASSERT(!m_backupFilePathOfPreviousSession.isEmpty());
|
|
|
|
m_content = readBackupFile(m_backupFilePathOfPreviousSession);
|
|
m_provider->write(m_content);
|
|
++m_revision;
|
|
|
|
FileUtils::removeFile(m_backupFilePathOfPreviousSession);
|
|
qInfo() << "recover from backup file of previous session" << m_backupFilePathOfPreviousSession;
|
|
m_backupFilePathOfPreviousSession.clear();
|
|
|
|
// Reset state.
|
|
m_viewWindowToSync = nullptr;
|
|
m_modified = false;
|
|
|
|
emit modified(m_modified);
|
|
emit contentsChanged();
|
|
}
|
|
|
|
bool Buffer::isChildOf(const Node *p_node) const
|
|
{
|
|
return m_provider->isChildOf(p_node);
|
|
}
|
|
|
|
bool Buffer::isAttachmentSupported() const
|
|
{
|
|
return !m_readOnly && m_provider->isAttachmentSupported();
|
|
}
|
|
|
|
bool Buffer::hasAttachment() const
|
|
{
|
|
if (!isAttachmentSupported()) {
|
|
return false;
|
|
}
|
|
|
|
if (m_provider->getAttachmentFolder().isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
QDir dir(getAttachmentFolderPath());
|
|
return !dir.isEmpty();
|
|
}
|
|
|
|
QString Buffer::getAttachmentFolderPath() const
|
|
{
|
|
Q_ASSERT(isAttachmentSupported());
|
|
return const_cast<Buffer *>(this)->m_provider->fetchAttachmentFolderPath();
|
|
}
|
|
|
|
QStringList Buffer::addAttachment(const QString &p_destFolderPath, const QStringList &p_files)
|
|
{
|
|
if (p_files.isEmpty()) {
|
|
return QStringList();
|
|
}
|
|
auto destFolderPath = p_destFolderPath.isEmpty() ? getAttachmentFolderPath() : p_destFolderPath;
|
|
Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), destFolderPath));
|
|
auto files = m_provider->addAttachment(destFolderPath, p_files);
|
|
if (!files.isEmpty()) {
|
|
emit attachmentChanged();
|
|
}
|
|
return files;
|
|
}
|
|
|
|
QString Buffer::newAttachmentFile(const QString &p_destFolderPath, const QString &p_name)
|
|
{
|
|
Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), p_destFolderPath));
|
|
auto filePath = m_provider->newAttachmentFile(p_destFolderPath, p_name);
|
|
emit attachmentChanged();
|
|
return filePath;
|
|
}
|
|
|
|
QString Buffer::newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name)
|
|
{
|
|
Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), p_destFolderPath));
|
|
auto folderPath = m_provider->newAttachmentFolder(p_destFolderPath, p_name);
|
|
emit attachmentChanged();
|
|
return folderPath;
|
|
}
|
|
|
|
QString Buffer::renameAttachment(const QString &p_path, const QString &p_name)
|
|
{
|
|
Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), p_path));
|
|
return m_provider->renameAttachment(p_path, p_name);
|
|
}
|
|
|
|
void Buffer::removeAttachment(const QStringList &p_paths)
|
|
{
|
|
m_provider->removeAttachment(p_paths);
|
|
emit attachmentChanged();
|
|
}
|
|
|
|
bool Buffer::isAttachment(const QString &p_path) const
|
|
{
|
|
return PathUtils::pathContains(getAttachmentFolderPath(), p_path);
|
|
}
|
|
|
|
bool Buffer::isTagSupported() const
|
|
{
|
|
return m_provider->isTagSupported();
|
|
}
|
|
|
|
Buffer::ProviderType Buffer::getProviderType() const
|
|
{
|
|
return m_provider->getType();
|
|
}
|
|
|
|
Node *Buffer::getNode() const
|
|
{
|
|
return m_provider->getNode();
|
|
}
|
|
|
|
bool Buffer::checkFileExistsOnDisk()
|
|
{
|
|
if (m_provider->checkFileExistsOnDisk()) {
|
|
m_state &= ~StateFlag::FileMissingOnDisk;
|
|
return true;
|
|
} else {
|
|
m_state |= StateFlag::FileMissingOnDisk;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool Buffer::checkFileChangedOutside()
|
|
{
|
|
if (m_provider->checkFileChangedOutside()) {
|
|
m_state |= StateFlag::FileChangedOutside;
|
|
return true;
|
|
} else {
|
|
m_state &= ~StateFlag::FileChangedOutside;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Buffer::StateFlags Buffer::state() const
|
|
{
|
|
return m_state;
|
|
}
|
|
|
|
QSharedPointer<File> Buffer::getFile() const
|
|
{
|
|
return m_provider->getFile();
|
|
}
|