From e5b23ae39838eda8080e560871c65be2c4d785a2 Mon Sep 17 00:00:00 2001 From: peter Date: Sun, 8 Apr 2012 22:39:57 -0300 Subject: [PATCH] Asynchronous autosave. --- asyncwriter.cpp | 78 ++++++++++++++++++++ asyncwriter.h | 41 +++++++++++ fileio.cpp | 41 +++++++++++ fileio.h | 14 ++++ filelocker.h | 64 +++++++++++++++++ mainwidget.cpp | 29 ++++++-- mainwidget.h | 6 +- scribble.pro | 11 ++- scribble_document.cpp | 161 +++++++++++++++++++++--------------------- scribble_document.h | 17 ++++- 10 files changed, 371 insertions(+), 91 deletions(-) create mode 100644 asyncwriter.cpp create mode 100644 asyncwriter.h create mode 100644 fileio.cpp create mode 100644 fileio.h create mode 100644 filelocker.h diff --git a/asyncwriter.cpp b/asyncwriter.cpp new file mode 100644 index 0000000..d0bd4d7 --- /dev/null +++ b/asyncwriter.cpp @@ -0,0 +1,78 @@ +#include "asyncwriter.h" + +#include + +#include "fileio.h" + +AsyncWriter::AsyncWriter(QObject *parent) : + QThread(parent), abort(false), waiting(false) +{ +} + +AsyncWriter::~AsyncWriter() +{ + mutex.lock(); + abort = true; + workToDo.wakeOne(); + mutex.unlock(); + + wait(); +} + +void AsyncWriter::writeData(const QList &data, const QFile &file) +{ + QMutexLocker locker(&mutex); + + this->data = data; + this->file.setFileName(file.fileName()); + + if (!isRunning()) { + start(); + } else { + workToDo.wakeOne(); + } +} + +void AsyncWriter::stopWriting() +{ + QMutexLocker locker(&mutex); + + file.setFileName(QString()); + + if (!waiting && isRunning()) + workFinished.wait(&mutex, 3000); +} + +void AsyncWriter::run() +{ + forever { + mutex.lock(); + QFile f; + f.setFileName(file.fileName()); + const QList d = data; + mutex.unlock(); + + if (abort) { + workFinished.wakeAll(); + return; + } + + if (!f.fileName().isEmpty()) { + QByteArray output = ScribbleDocument::toXournalXMLFormat(d); + FileIO::writeGZFileLocked(f, output); + } + + mutex.lock(); + file.setFileName(QString()); + if (abort) { + workFinished.wakeAll(); + mutex.unlock(); + return; + } + waiting = true; + workFinished.wakeAll(); + workToDo.wait(&mutex); + waiting = false; + mutex.unlock(); + } +} diff --git a/asyncwriter.h b/asyncwriter.h new file mode 100644 index 0000000..f2b9134 --- /dev/null +++ b/asyncwriter.h @@ -0,0 +1,41 @@ +#ifndef ASYNCIO_H +#define ASYNCIO_H + +#include +#include +#include +#include +#include + +#include "scribble_document.h" + +/* Thread that is used to write data to a file asynchronously. + * At any time, there is at most one writing operation pending + * and any operation is allowed to fail. */ +class AsyncWriter : public QThread +{ + Q_OBJECT +public: + explicit AsyncWriter(QObject *parent = 0); + ~AsyncWriter(); + + void writeData(const QList &data, const QFile &file); + void stopWriting(); + +signals: + +protected: + void run(); + +private: + bool abort; + bool waiting; + QFile file; + QList data; + + QMutex mutex; + QWaitCondition workToDo; + QWaitCondition workFinished; +}; + +#endif // ASYNCIO_H diff --git a/fileio.cpp b/fileio.cpp new file mode 100644 index 0000000..c937222 --- /dev/null +++ b/fileio.cpp @@ -0,0 +1,41 @@ +#include "fileio.h" + +#include "filelocker.h" +#include "zlib.h" + +QByteArray FileIO::readGZFileLocked(const QFile &file) +{ + FileLocker locker(file); + + gzFile f = gzopen(file.fileName().toLocal8Bit().constData(), "r"); + if (f == 0) + return QByteArray(); + + QByteArray data; + + char buffer[1024]; + while (!gzeof(f)) { + int len = gzread(f, buffer, 1024); + if (len < 0) { + gzclose(f); + return QByteArray(); + } + data.append(buffer, len); + } + gzclose(f); + + return data; +} + +bool FileIO::writeGZFileLocked(const QFile &file, const QByteArray &data) +{ + FileLocker locker(file); + + gzFile f = gzopen(file.fileName().toLocal8Bit().constData(), "w"); + if (f == 0) { + return false; + } + gzwrite(f, data.data(), data.size()); + gzclose(f); + return true; +} diff --git a/fileio.h b/fileio.h new file mode 100644 index 0000000..e233456 --- /dev/null +++ b/fileio.h @@ -0,0 +1,14 @@ +#ifndef FILEIO_H +#define FILEIO_H + +#include +#include + +class FileIO +{ +public: + static QByteArray readGZFileLocked(const QFile &file); + static bool writeGZFileLocked(const QFile &file, const QByteArray &data); +}; + +#endif // FILEIO_H diff --git a/filelocker.h b/filelocker.h new file mode 100644 index 0000000..30249ce --- /dev/null +++ b/filelocker.h @@ -0,0 +1,64 @@ +#ifndef FILELOCKER_H +#define FILELOCKER_H + +#include +#include +#include +#include + +class FileLockerManager +{ +public: + FileLockerManager() { + } + + void lockFile(QString file) { + QMutexLocker locker(&mutex); + lockedFiles.insert(file); + } + void unlockFile(QString file) { + QMutexLocker locker(&mutex); + lockedFiles.remove(file); + } + + static FileLockerManager &instance() { + static FileLockerManager manager; + return manager; + } + +private: + QSet lockedFiles; + QMutex mutex; + + Q_DISABLE_COPY(FileLockerManager) +}; + +class FileLocker +{ +public: + inline explicit FileLocker(QString file) { + lockedFile = file; + lock(); + } + + inline explicit FileLocker(const QFile &file) { + lockedFile = QFileInfo(file).absoluteFilePath(); + lock(); + } + + inline ~FileLocker() { + unlock(); + } +private: + inline void lock() { + FileLockerManager::instance().lockFile(lockedFile); + } + inline void unlock() { + FileLockerManager::instance().unlockFile(lockedFile); + } + + QString lockedFile; + Q_DISABLE_COPY(FileLocker) +}; + +#endif // FILELOCKER_H diff --git a/mainwidget.cpp b/mainwidget.cpp index bc911a1..f718078 100644 --- a/mainwidget.cpp +++ b/mainwidget.cpp @@ -21,6 +21,7 @@ #include "mainwidget.h" #include "filebrowser.h" +#include "fileio.h" #include "onyx/screen/screen_proxy.h" #include "onyx/screen/screen_update_watcher.h" @@ -113,6 +114,8 @@ MainWidget::MainWidget(QWidget *parent) : setLayout(layout); onyx::screen::watcher().addWatcher(this); + asyncWriter = new AsyncWriter(this); + connect(document, SIGNAL(pageOrLayerNumberChanged(int,int,int,int)), SLOT(updateProgressBar(int,int,int,int))); connect(scribbleArea, SIGNAL(resized(QSize)), document, SLOT(setViewSize(QSize))); connect(statusBar, SIGNAL(progressClicked(int,int)), SLOT(setPage(int,int))); @@ -120,7 +123,7 @@ MainWidget::MainWidget(QWidget *parent) : connect(&touchListener, SIGNAL(touchData(TouchData &)), this, SLOT(touchEventDataReceived(TouchData &))); QTimer *save_timer = new QTimer(this); - connect(save_timer, SIGNAL(timeout()), SLOT(save())); + connect(save_timer, SIGNAL(timeout()), SLOT(saveAsynchronously())); /* save every 5 seconds */ save_timer->start(5000); } @@ -130,14 +133,15 @@ void MainWidget::loadFile(const QFile &file) save(); /* TODO error message */ - if (document->loadXournalFile(file)) { + QByteArray data = FileIO::readGZFileLocked(file); + if (document->loadXournalFile(data)) { currentFile.setFileName(file.fileName()); } } void MainWidget::saveFile(const QFile &file) { - document->saveXournalFile(file); + FileIO::writeGZFileLocked(file, document->toXournalXMLFormat()); currentFile.setFileName(file.fileName()); } @@ -235,6 +239,7 @@ void MainWidget::open() touchActive = false; FileBrowser fileBrowser(this); + /* TODO save last path */ QString path = fileBrowser.showLoadFile(currentFile.fileName()); if (path.isEmpty()) return; @@ -243,6 +248,15 @@ void MainWidget::open() touchActive = true; } +void MainWidget::save() +{ + if (!currentFile.fileName().isEmpty()) { + asyncWriter->stopWriting(); + /* save timeout cannot occur now since this is the same thread */ + saveFile(currentFile); + } +} + void MainWidget::saveAs() { QString file = QFileDialog::getSaveFileName(this, "Save Scribble File", @@ -251,6 +265,7 @@ void MainWidget::saveAs() if (!file.isEmpty()) { saveFile(QFile(file)); } + /* TODO set current filename */ } void MainWidget::updateProgressBar(int currentPage, int maxPages, int currentLayer, int maxLayers) @@ -263,8 +278,10 @@ void MainWidget::setPage(int percentage, int page) document->setCurrentPage(page - 1); } -void MainWidget::save() +void MainWidget::saveAsynchronously() { - if (!currentFile.fileName().isEmpty()) - document->saveXournalFile(currentFile); + if (!currentFile.fileName().isEmpty() && document->hasChangedSinceLastSave()) { + document->setSaved(); + asyncWriter->writeData(document->getPagesCopy(), currentFile); + } } diff --git a/mainwidget.h b/mainwidget.h index 23eb33b..76926db 100644 --- a/mainwidget.h +++ b/mainwidget.h @@ -25,6 +25,7 @@ #include "onyx/ui/status_bar.h" +#include "asyncwriter.h" #include "scribblearea.h" #include "scribble_document.h" @@ -37,9 +38,10 @@ class MainWidget : public QWidget void saveFile(const QFile&); signals: + void saveToGZFileAsynchronously(const QFile &file, const QByteArray &data); public slots: - void save(); + void saveAsynchronously(); private slots: void touchEventDataReceived(TouchData &); @@ -47,6 +49,7 @@ private slots: void mouseMoveEvent(QMouseEvent *ev); void mouseReleaseEvent(QMouseEvent *ev); + void save(); void saveAs(); void open(); @@ -63,6 +66,7 @@ private slots: bool touchActive; + AsyncWriter *asyncWriter; QFile currentFile; ScribbleArea *scribbleArea; ScribbleDocument *document; diff --git a/scribble.pro b/scribble.pro index c056b91..88160c2 100644 --- a/scribble.pro +++ b/scribble.pro @@ -5,9 +5,11 @@ SOURCES += scribble.cpp \ scribblearea.cpp \ scribble_document.cpp \ filebrowser.cpp \ - tree_view.cpp + tree_view.cpp \ + fileio.cpp \ + asyncwriter.cpp -LIBS += -L /usr/local/lib -lz -lonyxapp -lonyx_base -lonyx_ui -lonyx_screen -lonyx_sys -lonyx_wpa -lonyx_wireless -lonyx_data -lonyx_touch -lonyx_cms +LIBS += -lz -lonyxapp -lonyx_base -lonyx_ui -lonyx_screen -lonyx_sys -lonyx_wpa -lonyx_wireless -lonyx_data -lonyx_cms INCLUDEPATH += /opt/onyx/arm/include @@ -16,6 +18,9 @@ HEADERS += \ scribblearea.h \ scribble_document.h \ filebrowser.h \ - tree_view.h + tree_view.h \ + filelocker.h \ + fileio.h \ + asyncwriter.h RESOURCES += diff --git a/scribble_document.cpp b/scribble_document.cpp index 7e13786..18215a6 100644 --- a/scribble_document.cpp +++ b/scribble_document.cpp @@ -24,7 +24,7 @@ #include #include -#include "zlib.h" +#include "fileio.h" bool ScribbleStroke::segmentIntersects(int i, const ScribbleStroke &o) const { @@ -48,6 +48,59 @@ void ScribbleStroke::updateBoundingRect() boundingRect.adjust(-a, -a, a, a); } +QByteArray ScribblePage::getXmlRepresentation() const +{ + /* TODO Can we cache this? During autosave, it is computed in the + * other thread. Getting the information back would save + * quite some CPU. */ + QString output; + output += QString().sprintf("\n", + size.width(), + size.height()); + + ScribbleXournalBackground back = background; + if (back.type.isNull()) { + /* default background style */ + back.type = "solid"; + back.style = "plain"; + back.color = "white"; + } + + output += QString().sprintf("> 24); + output += QString().sprintf("", + color, stroke.getPen().widthF()); + foreach (const QPointF &point, stroke.getPoints()) { + output += QString().sprintf("%.2f %.2f ", point.x(), point.y()); + } + output += QString().sprintf("\n\n"); + /* TODO error for text items */ + } + output += "\n"; + } + output += "\n"; + return output.toUtf8(); +} + + + /* ---------------------------------------------------------------- */ XournalXMLHandler::XournalXMLHandler() @@ -245,29 +298,8 @@ ScribbleDocument::ScribbleDocument(QObject *parent) : initAfterLoad(); } -bool ScribbleDocument::loadXournalFile(const QFile &file) +bool ScribbleDocument::loadXournalFile(QByteArray data) { - /* TODO do we have to free anything during file name conversion? */ - gzFile f = gzopen(file.fileName().toLocal8Bit().constData(), "r"); - if (f == 0) { - return false; - } - - QByteArray data; - - { - char buffer[1024]; - while (!gzeof(f)) { - int len = gzread(f, buffer, 1024); - if (len < 0) { - gzclose(f); - return false; - } - data.append(buffer, len); - } - gzclose(f); - } - QBuffer dataBuffer(&data); QXmlInputSource source(&dataBuffer); QXmlSimpleReader reader; @@ -290,76 +322,34 @@ bool ScribbleDocument::loadXournalFile(const QFile &file) return true; } -bool ScribbleDocument::saveXournalFile(const QFile &file) +QByteArray ScribbleDocument::toXournalXMLFormat() { - /* TODO do we have to free anything during file name conversion? */ - gzFile f = gzopen(file.fileName().toLocal8Bit().constData(), "w"); - if (f == 0) { - return false; - } + return toXournalXMLFormat(pages); +} +QByteArray ScribbleDocument::toXournalXMLFormat(const QList &pages) +{ setlocale(LC_NUMERIC, "C"); - gzprintf(f, "\n" + QByteArray output = "\n" "\n" - "Scribble document - see https://github.com/peter-x/scribble\n"); - foreach (const ScribblePage &page, pages) { - gzprintf(f, "\n", - page.size.width(), - page.size.height()); - - ScribbleXournalBackground background = page.background; - if (background.type.isNull()) { - /* default background style */ - background.type = "solid"; - background.style = "plain"; - background.color = "white"; - } - - gzprintf(f, "\n"); - - foreach (const ScribbleLayer &layer, page.layers) { - gzprintf(f, "\n"); - foreach (const ScribbleStroke &stroke, layer.items) { - quint32 color = stroke.getPen().color().rgba(); - /* alpha channel is msB in Qt and lsB in Xournal */ - color = (color << 8) | (color >> 24); - gzprintf(f, "", - color, stroke.getPen().widthF()); - foreach (const QPointF &point, stroke.getPoints()) { - gzprintf(f, "%.2f %.2f ", point.x(), point.y()); - } - gzprintf(f, "\n\n"); - /* TODO error for text items */ - } - gzprintf(f, "\n"); - } - gzprintf(f, "\n"); + "Scribble document - see https://github.com/peter-x/scribble\n"; + for (int i = 0; i < pages.length(); i ++) { + output += pages[i].getXmlRepresentation(); } - gzprintf(f, "\n"); - gzclose(f); + output += "\n"; setlocale(LC_NUMERIC, ""); - return true; - } + return output; +} void ScribbleDocument::initAfterLoad() { if (pages.length() == 0) { pages.append(ScribblePage()); pages[0].layers.append(ScribbleLayer()); + pages[0].invalidate(); } currentPage = 0; currentLayer = getCurrentPage().layers.length() - 1; @@ -370,6 +360,8 @@ void ScribbleDocument::initAfterLoad() currentPen.setColor(QColor(0, 0, 0)); currentPen.setWidth(1); + changedSinceLastSave = false; + emit pageOrLayerNumberChanged(currentPage, pages.length(), currentLayer, getCurrentPage().layers.length()); emit pageOrLayerChanged(getCurrentPage(), currentLayer); } @@ -425,6 +417,8 @@ void ScribbleDocument::layerUp() ScribblePage &p = pages[currentPage]; if (currentLayer + 1 >= p.layers.length()) { p.layers.append(ScribbleLayer()); + p.invalidate(); + changedSinceLastSave = true; } currentLayer += 1; emit pageOrLayerNumberChanged(currentPage, pages.length(), currentLayer, getCurrentPage().layers.length()); @@ -451,6 +445,8 @@ void ScribbleDocument::mousePressEvent(QMouseEvent *event) l.items.append(ScribbleStroke(currentPen, QPolygonF())); currentStroke = &l.items.last(); currentStroke->appendPoint(event->posF()); + pages[currentPage].invalidate(); + changedSinceLastSave = true; emit strokePointAdded(*currentStroke); } else { eraseAt(event->pos()); @@ -462,6 +458,8 @@ void ScribbleDocument::mouseMoveEvent(QMouseEvent *event) if (!sketching) return; if (currentMode == PEN) { currentStroke->appendPoint(event->posF()); + pages[currentPage].invalidate(); + changedSinceLastSave = true; emit strokePointAdded(*currentStroke); } else { eraseAt(event->pos()); @@ -474,6 +472,8 @@ void ScribbleDocument::mouseReleaseEvent(QMouseEvent *event) if (currentMode == PEN) { currentStroke->appendPoint(event->posF()); + pages[currentPage].invalidate(); + changedSinceLastSave = true; emit strokePointAdded(*currentStroke); } else { eraseAt(event->pos()); @@ -519,6 +519,9 @@ void ScribbleDocument::eraseAt(const QPointF &point) } } - if (!removedStrokes.isEmpty()) + if (!removedStrokes.isEmpty()) { + pages[currentPage].invalidate(); + changedSinceLastSave = true; emit strokesChanged(getCurrentPage(), currentLayer, removedStrokes); + } } diff --git a/scribble_document.h b/scribble_document.h index 4b4957b..8dd94a2 100644 --- a/scribble_document.h +++ b/scribble_document.h @@ -84,6 +84,11 @@ class ScribblePage QList layers; QSizeF size; ScribbleXournalBackground background; + + void invalidate() const { + /* TODO could be used to invalide cached XML representation */ + } + QByteArray getXmlRepresentation() const; }; class EraserContext @@ -148,12 +153,18 @@ class ScribbleDocument : public QObject Q_OBJECT public: explicit ScribbleDocument(QObject *parent = 0); - bool loadXournalFile(const QFile &file); - bool saveXournalFile(const QFile &file); + bool loadXournalFile(QByteArray data); + QByteArray toXournalXMLFormat(); + static QByteArray toXournalXMLFormat(const QList &pages); + /* TODO this will cause deep copies to occur upon the first + * change (i.e. first mouse move) */ + QList getPagesCopy() const { return pages; } int getNumPages() const { return pages.length(); } const ScribblePage &getCurrentPage() const { return pages[currentPage]; } int getCurrentLayer() const { return currentLayer; } + bool hasChangedSinceLastSave() const { return changedSinceLastSave; } + void setSaved() { changedSinceLastSave = false; } signals: void pageOrLayerNumberChanged(int currentPage, int maxPages, int currentLayer, int maxLayers); @@ -212,6 +223,8 @@ public slots: /* shortcut to last item of current layer (if it exists) */ ScribbleStroke *currentStroke; + + bool changedSinceLastSave; };