diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | afdnotebook.cc | 399 | ||||
-rw-r--r-- | afdnotebook.h | 119 | ||||
-rw-r--r-- | bitreader.cc | 45 | ||||
-rw-r--r-- | bitreader.h | 25 | ||||
-rw-r--r-- | main.cc | 11 | ||||
-rw-r--r-- | mainwindow.cc | 66 | ||||
-rw-r--r-- | mainwindow.h | 37 | ||||
-rw-r--r-- | mainwindow.ui | 354 | ||||
-rw-r--r-- | notebookmodel.cc | 323 | ||||
-rw-r--r-- | notebookmodel.h | 49 | ||||
-rw-r--r-- | notebookview.cc | 194 | ||||
-rw-r--r-- | notebookview.h | 59 | ||||
-rw-r--r-- | pageitem.cc | 64 | ||||
-rw-r--r-- | pageitem.h | 32 | ||||
-rw-r--r-- | scribiu.pro | 35 | ||||
-rw-r--r-- | smartpen.cc | 424 | ||||
-rw-r--r-- | smartpen.h | 78 | ||||
-rw-r--r-- | smartpenmanager.cc | 69 | ||||
-rw-r--r-- | smartpenmanager.h | 42 | ||||
-rw-r--r-- | smartpensyncer.cc | 214 | ||||
-rw-r--r-- | smartpensyncer.h | 39 | ||||
-rw-r--r-- | stfgraphicsitem.cc | 67 | ||||
-rw-r--r-- | stfgraphicsitem.h | 20 | ||||
-rw-r--r-- | stfreader.cc | 336 | ||||
-rw-r--r-- | stfreader.h | 51 | ||||
-rw-r--r-- | xmlutils.cc | 13 | ||||
-rw-r--r-- | xmlutils.h | 8 |
28 files changed, 3174 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75c107b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pro.user diff --git a/afdnotebook.cc b/afdnotebook.cc new file mode 100644 index 0000000..38f1fdc --- /dev/null +++ b/afdnotebook.cc @@ -0,0 +1,399 @@ +#include <cmath> +#include <QtCore/QDebug> +#include <QtGui/QPixmapCache> +#include "xmlutils.h" +#include "smartpen.h" +#include "afdnotebook.h" + +AfdNotebook::AfdNotebook(QObject *parent) + : QObject(parent) +{ +} + +AfdNotebook::~AfdNotebook() +{ +} + +bool AfdNotebook::open(const QString &path) +{ + _dir.setPath(path); + if (!_dir.exists()) { + qWarning() << "Directory" << _dir.absolutePath() << "does not exist"; + return false; + } + if (!parseMainInfo()) { + return false; + } + if (!parseMainDocument()) { + return false; + } + if (!findPenData()) { + return false; + } + return true; +} + +void AfdNotebook::close() +{ + _title.clear(); + _lastPage = _firstPage = PageAddress(0, 0, 0, 0, 0); + _pagesPerBook = 0; + _gfx.clear(); + _pages.clear(); + _penData.clear(); + _dir.setPath(QString()); +} + +QString AfdNotebook::title() const +{ + return _title; +} + +int AfdNotebook::numPages() const +{ + return _pages.size(); +} + +QString AfdNotebook::getPageBackgroundName(int page) const +{ + const Page& p = _pages.at(page); + return p.gfx->basename; +} + +QPixmap AfdNotebook::getPageBackground(int page) +{ + const Page& p = _pages.at(page); + QPixmap pix; + if (QPixmapCache::find(p.gfx->basename, &pix)) { + return pix; + } + + const QString file = QString("userdata/lsac_data/%1.png").arg(p.gfx->basename); + QImage img; + + if (!img.load(_dir.filePath(file), "PNG")) { + qWarning() << "Could not load background file:" << _dir.absoluteFilePath(file); + return pix; + } + + QRect cropRect = img.rect(); + QRect trim = getPageTrim(page); + QPointF scale(cropRect.width() / double(p.size.width()), + cropRect.height() / double(p.size.height())); + + cropRect.adjust(lround(trim.x() * scale.x()), lround(trim.y() * scale.y()), + lround(-(p.size.width() - trim.bottomRight().x()) * scale.x()), + lround(-(p.size.height() - trim.bottomRight().y()) * scale.y())); + + qDebug() << "Cropping image from" << img.rect() << "to" << cropRect; + + pix = QPixmap::fromImage(img.copy(cropRect)); + QPixmapCache::insert(p.gfx->basename, pix); + + return pix; +} + +QSize AfdNotebook::getPageSize(int page) const +{ + const Page &p = _pages.at(page); + return p.size; +} + +QRect AfdNotebook::getPageTrim(int page) const +{ + const Page &p = _pages.at(page); + QRect trim(QPoint(0, 0), p.size); + if (p.size.width() > SMARTPEN_BLEED_X * 2 && p.size.height() > SMARTPEN_BLEED_Y * 2) { + trim.adjust(SMARTPEN_BLEED_X, SMARTPEN_BLEED_Y, -SMARTPEN_BLEED_X, -SMARTPEN_BLEED_Y); + } + return trim; +} + +QStringList AfdNotebook::penSerials() const +{ + return _penData.keys(); +} + +QList<int> AfdNotebook::pagesWithStrokes(const QString &penSerial) const +{ + if (_penData.contains(penSerial)) { + const PenData &data = _penData[penSerial]; + return data.strokes.uniqueKeys(); + } else { + return QList<int>(); + } +} + +QStringList AfdNotebook::strokeFiles(const QString &penSerial, int page) const +{ + QStringList l; + if (!_penData.contains(penSerial)) return l; + + const PenData &data = _penData[penSerial]; + QMultiMap<int, StrokeData>::const_iterator it = data.strokes.find(page); + while (it != data.strokes.end() && it.key() == page) { + const StrokeData &stroke = it.value(); + l.append(_dir.filePath(stroke.file)); + ++it; + } + + return l; +} + +bool AfdNotebook::readStrokes(const QString &penSerial, int page, StfReader::StrokeHandler *handler) +{ + if (!_penData.contains(penSerial)) return true; + + StfReader stf; + stf.setStrokeHandler(handler); + + const PenData &data = _penData[penSerial]; + QMultiMap<int, StrokeData>::const_iterator it = data.strokes.find(page); + while (it != data.strokes.end() && it.key() == page) { + const StrokeData &stroke = it.value(); + qDebug() << "Reading strokes from" << stroke.file; + if (!stf.parse(_dir.filePath(stroke.file))) { + qWarning() << "Could not parse stroke file" << stroke.file; + return false; + } + ++it; + } + + return true; +} + +AfdNotebook::PageAddress::PageAddress(const QString &str) +{ + QStringList parts = str.split('.'); + if (parts.count() == 5) { + series = parts[0].toUInt(); + shelf = parts[1].toUInt(); + segment = parts[2].toUInt(); + book = parts[3].toUInt(); + page = parts[4].toUInt(); + } else if (parts.count() == 4) { + series = 0; + shelf = parts[0].toUInt(); + segment = parts[1].toUInt(); + book = parts[2].toUInt(); + page = parts[3].toUInt(); + } else { + qWarning() << "Unknown page address syntax:" << str; + } +} + +QString AfdNotebook::PageAddress::toString() const +{ + QStringList l; + l.reserve(5); + + if (series) { + l.append(QString::number(series)); + } + + l.append(QString::number(shelf)); + l.append(QString::number(segment)); + l.append(QString::number(book)); + l.append(QString::number(page)); + + return l.join("."); +} + +QMap<QString, QString> AfdNotebook::parsePropertyList(QIODevice *dev) +{ + QMap<QString, QString> result; + QByteArray line; + + while (!(line = dev->readLine()).isEmpty()) { + int sep = line.indexOf(':'); + QString key = QString::fromLatin1(line.constData(), sep); + result[key] = QString::fromUtf8(line.constData() + sep + 1).trimmed(); + } + + return result; +} + +QMap<QString, QString> AfdNotebook::parsePropertyList(const QString &relativePath) const +{ + QFile f(_dir.filePath(relativePath)); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return QMap<QString, QString>(); + } + return parsePropertyList(&f); +} + +bool AfdNotebook::parseMainInfo() +{ + QMap<QString, QString> info = parsePropertyList("main.info"); + + if (info.isEmpty()) { + qWarning() << "Empty main.info"; + return false; + } + + _title = info["title"]; + _firstPage = PageAddress(info["pagestart"]); + _lastPage = PageAddress(info["pagestop"]); + _pagesPerBook = info.value("segment-pages-per-book", "108").toUInt(); + + return true; +} + +bool AfdNotebook::parseMainDocument() +{ + QFile f(_dir.filePath("main.document")); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return false; + } + + QXmlStreamReader r(&f); + + Q_ASSERT(_pages.isEmpty()); + Q_ASSERT(_gfx.isEmpty()); + + advanceToFirstChildElement(r, "document"); + while (r.readNextStartElement()) { + if (r.name() == "page") { + QXmlStreamAttributes attrs = r.attributes(); + QString gfxfile; + gfxfile += attrs.value("basepath"); + gfxfile += '/'; + gfxfile += attrs.value("gfxfile_ref"); + + Page p; + p.gfx = &_gfx[gfxfile]; + p.size.setWidth(attrs.value("width").toString().toInt()); + p.size.setHeight(attrs.value("height").toString().toInt()); + _pages.append(p); + + r.skipCurrentElement(); + } else { + qWarning() << "Ignoring unknown element" << r.name() << "in main.document"; + } + } + + if (_pages.isEmpty()) { + qWarning() << "Notebook has no pages"; + return false; + } + + foreach (const QString &gfxfile, _gfx.keys()) { + if (!parseGfx(gfxfile)) { + return false; + } + } + + return true; +} + +bool AfdNotebook::parseGfx(const QString &file) +{ + QFile f(_dir.filePath(file)); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return false; + } + + QXmlStreamReader r(&f); + + Gfx &gfx = _gfx[file]; + Q_ASSERT(gfx.basename.isEmpty()); + + advanceToFirstChildElement(r, "graphics"); + advanceToFirstChildElement(r, "setbase"); + advanceToFirstChildElement(r, "image"); + if (!r.atEnd()) { + QXmlStreamAttributes attrs = r.attributes(); + QString imageSrc = attrs.value("src").toString(); + qDebug() << "image src" << imageSrc; + int lastSlash = imageSrc.lastIndexOf('/'); + int lastDot = imageSrc.lastIndexOf('.'); + if (lastSlash >= 0 && lastDot > lastSlash) { + gfx.basename = imageSrc.mid(lastSlash + 1, lastDot - lastSlash - 1); + qDebug() << "Got gfx" << gfx.basename; + } + } + + return true; +} + +bool AfdNotebook::findPenData() +{ + QDir dir(_dir.filePath("data")); + if (!dir.exists()) { + return false; + } + + QStringList pageDirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + foreach (QString pageName, pageDirs) { + pageName.remove('/'); + qDebug() << " page data" << pageName; + int pageNum = getPageNumber(PageAddress(pageName)); + if (pageNum < 0) continue; + + QDir pageDir(dir.filePath(pageName)); + if (!pageDir.exists()) continue; + QStringList penDirs = pageDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + foreach (QString penName, penDirs) { + penName.remove('/'); + qDebug() << " pen data" << penName; + QDir penDir(pageDir.filePath(penName)); + if (!penDir.exists()) continue; + + QStringList strokeFiles = penDir.entryList(QStringList("*.stf"), QDir::Files); + foreach (const QString &strokeFile, strokeFiles) { + qDebug() << " stroke data" << strokeFile; + + if (strokeFile.length() != 25) { + qWarning() << "Invalid stroke filename format" << strokeFile << endl; + continue; + } + + StrokeData stroke; + stroke.file = penDir.filePath(strokeFile); + bool ok = true; + + qDebug() << " " << strokeFile.mid(2, 8) << strokeFile.mid(13, 8); + qDebug() << " " << strokeFile.mid(2, 8).toUInt(0, 16) << strokeFile.mid(13, 8).toUInt(0, 16); + + if (ok) stroke.begin = Smartpen::fromPenTime(strokeFile.mid(2, 8).toLongLong(&ok, 16) * 1000ULL); + if (ok) stroke.end = Smartpen::fromPenTime(strokeFile.mid(13, 8).toLongLong(&ok, 16) * 1000ULL); + + if (!ok) { + qWarning() << "Invalid stroke filename format" << strokeFile << endl; + continue; + } + + qDebug() << " from" << stroke.begin << "to" << stroke.end; + + _penData[penName].strokes.insert(pageNum, stroke); + } + } + } + + return true; +} + +AfdNotebook::PageAddress AfdNotebook::getPageAddress(int page) const +{ + PageAddress addr = _firstPage; + Q_ASSERT(page >= 0); + uint new_page = addr.page + page; + addr.book += new_page / _pagesPerBook; + addr.page = new_page % _pagesPerBook; + return addr; +} + +int AfdNotebook::getPageNumber(const PageAddress &addr) +{ + // series(0), shelf(0), segment(0), book(0), page(0) + if (addr.series == _firstPage.series && addr.shelf == _firstPage.shelf && addr.segment == _firstPage.segment) { + int firstPage = (_firstPage.book * _pagesPerBook) + _firstPage.page; + int page = (addr.book * _pagesPerBook) + addr.page - firstPage; + if (page >= 0 && page < _pages.size()) { + return page; + } + } + + qWarning() << "Invalid address for notebook" << _title; + return -1; +} diff --git a/afdnotebook.h b/afdnotebook.h new file mode 100644 index 0000000..ea90661 --- /dev/null +++ b/afdnotebook.h @@ -0,0 +1,119 @@ +#ifndef AFDNOTEBOOK_H +#define AFDNOTEBOOK_H + +#include <QtCore/QDateTime> +#include <QtCore/QDir> +#include <QtCore/QMap> +#include <QtGui/QImage> +#include "stfreader.h" + +class AfdNotebook : public QObject +{ + Q_OBJECT + +public: + AfdNotebook(QObject *parent = 0); + ~AfdNotebook(); + + bool open(const QString &path); + void close(); + + QString title() const; + + int numPages() const; + + QString getPageBackgroundName(int page) const; + QPixmap getPageBackground(int page); + + QSize getPageSize(int page) const; + QRect getPageTrim(int page) const; + + QStringList penSerials() const; + QList<int> pagesWithStrokes(const QString &penSerial) const; + + QStringList strokeFiles(const QString &penSerial, int page) const; + + bool readStrokes(const QString &penSerial, int page, StfReader::StrokeHandler *handler); + +private: + struct PageAddress { + PageAddress(); + explicit PageAddress(uint shelf, uint segment, uint book, uint page); + explicit PageAddress(uint series, uint shelf, uint segment, uint book, uint page); + explicit PageAddress(const QString &str); + + QString toString() const; + + bool operator<(const PageAddress& o) const; + bool operator==(const PageAddress& o) const; + + uint series : 12; + uint shelf : 12; + uint segment : 16; + uint book : 12; + uint page : 12; + }; + + struct Gfx { + QString basename; + }; + + struct Page { + Gfx *gfx; + QSize size; + }; + + struct StrokeData { + QString file; + QDateTime begin; + QDateTime end; + }; + + struct PenData { + QMultiMap<int, StrokeData> strokes; + }; + +private: + static QMap<QString, QString> parsePropertyList(QIODevice *dev); + QMap<QString, QString> parsePropertyList(const QString &relativePath) const; + + bool parseMainInfo(); + bool parseMainDocument(); + bool parseGfx(const QString &file); + bool findPenData(); + + PageAddress getPageAddress(int page) const; + int getPageNumber(const PageAddress &addr); + +private: + QDir _dir; + QString _title; + PageAddress _firstPage, _lastPage; + uint _pagesPerBook; + QMap<QString, Gfx> _gfx; + QList<Page> _pages; + QMap<QString, PenData> _penData; +}; + +inline AfdNotebook::PageAddress::PageAddress() + : series(0), shelf(0), segment(0), book(0), page(0) +{ +} + +inline AfdNotebook::PageAddress::PageAddress(uint shelf, uint segment, uint book, uint page) + : series(0), shelf(shelf), segment(segment), book(book), page(page) +{ +} + +inline AfdNotebook::PageAddress::PageAddress(uint series, uint shelf, uint segment, uint book, uint page) + : series(series), shelf(shelf), segment(segment), book(book), page(page) +{ +} + +inline bool AfdNotebook::PageAddress::operator ==(const PageAddress &o) const +{ + return series == o.series && shelf == o.shelf && segment == o.segment && + book == o.book && page == o.page; +} + +#endif diff --git a/bitreader.cc b/bitreader.cc new file mode 100644 index 0000000..dd85f74 --- /dev/null +++ b/bitreader.cc @@ -0,0 +1,45 @@ +#include "bitreader.h" + +BitReader::BitReader(QIODevice *device) + : child(device), buf(0), avail(0) +{ +} + +BitReader::~BitReader() +{ +} + +quint64 BitReader::readBits(int n) +{ + quint64 x = peekBits(n); + + Q_ASSERT(avail >= n); + buf -= x << (avail - n); + avail -= n; + + return x; +} + +quint64 BitReader::peekBits(int n) +{ + while (n > avail) { + char c; + child->getChar(&c); + buf = (buf << 8) | (quint8)(c); + avail += 8; + } + quint64 x = buf >> (avail - n); + + return x; +} + +void BitReader::skipUntilNextByte() +{ + int skip = avail % 8; + readBits(skip); +} + +bool BitReader::atEnd() +{ + return avail == 0 && child->atEnd(); +} diff --git a/bitreader.h b/bitreader.h new file mode 100644 index 0000000..c4b9207 --- /dev/null +++ b/bitreader.h @@ -0,0 +1,25 @@ +#ifndef BITREADER_H +#define BITREADER_H + +#include <QtCore/QIODevice> + +class BitReader +{ +public: + BitReader(QIODevice * device); + ~BitReader(); + + quint64 readBits(int n); + quint64 peekBits(int n); + + void skipUntilNextByte(); + + bool atEnd(); + +private: + QIODevice *child; + quint64 buf; + int avail; +}; + +#endif // BITREADER_H @@ -0,0 +1,11 @@ +#include "mainwindow.h" +#include <QApplication> + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/mainwindow.cc b/mainwindow.cc new file mode 100644 index 0000000..793a722 --- /dev/null +++ b/mainwindow.cc @@ -0,0 +1,66 @@ +#include <QtCore/QDebug> +#include "mainwindow.h" +#include "ui_mainwindow.h" + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow), + _notebooks(new NotebookModel(this)), + _manager(new SmartpenManager(this)) +{ + ui->setupUi(this); + ui->notebookTree->setModel(_notebooks); + ui->notebookTree->header()->setResizeMode(0, QHeaderView::Stretch); + ui->notebookTree->header()->setResizeMode(1, QHeaderView::Fixed); + ui->notebookTree->header()->setResizeMode(2, QHeaderView::Fixed); + ui->notebookTree->expandAll(); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::closeNotebook() +{ + _curPenName.clear(); + _curNotebookName.clear(); + ui->notebookView->setNotebook(QString()); +} + +void MainWindow::openNotebook(const QString &pen, const QString ¬ebook) +{ + if (_curPenName == pen && _curNotebookName == notebook) return; + + _curPenName = pen; + _curNotebookName = notebook; + + QString nbDir = _notebooks->notebookDirectory(_curPenName, _curNotebookName); + + qDebug() << "Opening notebook" << _curPenName << _curNotebookName << nbDir; + + ui->notebookView->setNotebook(nbDir); +} + +void MainWindow::handleNotebookSelected(const QModelIndex &index) +{ + if (!index.isValid()) { + closeNotebook(); + return; + } + QModelIndex parent = index.parent(); + if (!parent.isValid()) { + closeNotebook(); + return; + } + + QModelIndex child = parent.child(index.row(), 0); + + openNotebook(_notebooks->data(parent, Qt::DisplayRole).toString(), + _notebooks->data(child, Qt::DisplayRole).toString()); +} + +void MainWindow::handleCurPageChanged() +{ + ui->pageEdit->setText(QString::number(ui->notebookView->curPage() + 1)); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..4072ffa --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,37 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include <QtGui/QMainWindow> +#include "notebookmodel.h" +#include "smartpenmanager.h" + +namespace Ui { +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +public slots: + void closeNotebook(); + void openNotebook(const QString &pen, const QString ¬ebook); + +private slots: + void handleNotebookSelected(const QModelIndex &index); + void handleCurPageChanged(); + +private: + Ui::MainWindow *ui; + NotebookModel *_notebooks; + SmartpenManager *_manager; + + QString _curPenName; + QString _curNotebookName; +}; + +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..dceeadc --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,354 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>718</width> + <height>386</height> + </rect> + </property> + <property name="windowTitle"> + <string>Scribiu</string> + </property> + <widget class="QWidget" name="centralWidget"> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>3</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>3</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>3</number> + </property> + <item> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QTreeView" name="notebookTree"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="indentation"> + <number>10</number> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>false</bool> + </property> + <attribute name="headerVisible"> + <bool>false</bool> + </attribute> + <attribute name="headerDefaultSectionSize"> + <number>27</number> + </attribute> + <attribute name="headerStretchLastSection"> + <bool>false</bool> + </attribute> + </widget> + <widget class="QWidget" name="verticalLayoutWidget"> + <layout class="QVBoxLayout" name="pane2"> + <property name="spacing"> + <number>2</number> + </property> + <item> + <layout class="QHBoxLayout" name="viewpaneTools"> + <item> + <widget class="QToolButton" name="prevButton"> + <property name="maximumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset theme="go-previous"> + <normaloff/> + </iconset> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="pageEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>50</width> + <height>16777215</height> + </size> + </property> + <property name="maxLength"> + <number>4</number> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="nextButton"> + <property name="maximumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="go-next"> + <normaloff/> + </iconset> + </property> + </widget> + </item> + <item> + <spacer name="viewpaneToolsSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="zoomLabel"> + <property name="text"> + <string>Zoom:</string> + </property> + <property name="textFormat"> + <enum>Qt::PlainText</enum> + </property> + </widget> + </item> + <item> + <widget class="QSlider" name="zoomSlider"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="maximum"> + <number>200</number> + </property> + <property name="singleStep"> + <number>10</number> + </property> + <property name="pageStep"> + <number>50</number> + </property> + <property name="value"> + <number>100</number> + </property> + <property name="tracking"> + <bool>true</bool> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="invertedAppearance"> + <bool>false</bool> + </property> + <property name="invertedControls"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="NotebookView" name="notebookView"> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOn</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOn</enum> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menuBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>718</width> + <height>23</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>&File</string> + </property> + <addaction name="actionQuit"/> + </widget> + <addaction name="menuFile"/> + </widget> + <widget class="QStatusBar" name="statusBar"/> + <action name="actionQuit"> + <property name="icon"> + <iconset theme="application-quit"> + <normaloff/> + </iconset> + </property> + <property name="text"> + <string>&Quit</string> + </property> + <property name="shortcut"> + <string>Ctrl+Q</string> + </property> + <property name="menuRole"> + <enum>QAction::QuitRole</enum> + </property> + </action> + </widget> + <layoutdefault spacing="6" margin="11"/> + <customwidgets> + <customwidget> + <class>NotebookView</class> + <extends>QGraphicsView</extends> + <header>notebookview.h</header> + <slots> + <signal>curPageChanged()</signal> + <slot>setZoom(int)</slot> + <slot>prevPage()</slot> + <slot>nextPage()</slot> + </slots> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>notebookTree</sender> + <signal>activated(QModelIndex)</signal> + <receiver>MainWindow</receiver> + <slot>handleNotebookSelected(QModelIndex)</slot> + <hints> + <hint type="sourcelabel"> + <x>159</x> + <y>193</y> + </hint> + <hint type="destinationlabel"> + <x>358</x> + <y>192</y> + </hint> + </hints> + </connection> + <connection> + <sender>notebookTree</sender> + <signal>clicked(QModelIndex)</signal> + <receiver>MainWindow</receiver> + <slot>handleNotebookSelected(QModelIndex)</slot> + <hints> + <hint type="sourcelabel"> + <x>159</x> + <y>193</y> + </hint> + <hint type="destinationlabel"> + <x>358</x> + <y>192</y> + </hint> + </hints> + </connection> + <connection> + <sender>zoomSlider</sender> + <signal>valueChanged(int)</signal> + <receiver>notebookView</receiver> + <slot>setZoom(int)</slot> + <hints> + <hint type="sourcelabel"> + <x>671</x> + <y>42</y> + </hint> + <hint type="destinationlabel"> + <x>521</x> + <y>210</y> + </hint> + </hints> + </connection> + <connection> + <sender>notebookView</sender> + <signal>curPageChanged()</signal> + <receiver>MainWindow</receiver> + <slot>handleCurPageChanged()</slot> + <hints> + <hint type="sourcelabel"> + <x>521</x> + <y>210</y> + </hint> + <hint type="destinationlabel"> + <x>358</x> + <y>192</y> + </hint> + </hints> + </connection> + <connection> + <sender>prevButton</sender> + <signal>clicked()</signal> + <receiver>notebookView</receiver> + <slot>prevPage()</slot> + <hints> + <hint type="sourcelabel"> + <x>341</x> + <y>42</y> + </hint> + <hint type="destinationlabel"> + <x>521</x> + <y>210</y> + </hint> + </hints> + </connection> + <connection> + <sender>nextButton</sender> + <signal>clicked()</signal> + <receiver>notebookView</receiver> + <slot>nextPage()</slot> + <hints> + <hint type="sourcelabel"> + <x>433</x> + <y>42</y> + </hint> + <hint type="destinationlabel"> + <x>521</x> + <y>210</y> + </hint> + </hints> + </connection> + </connections> + <slots> + <slot>handleNotebookSelected(QModelIndex)</slot> + <slot>handleZoomChanged(int)</slot> + <slot>handleCurPageChanged()</slot> + </slots> +</ui> diff --git a/notebookmodel.cc b/notebookmodel.cc new file mode 100644 index 0000000..1d8228c --- /dev/null +++ b/notebookmodel.cc @@ -0,0 +1,323 @@ +#include <QtCore/QDebug> +#include <QtGui/QApplication> +#include <QtGui/QIcon> +#include <QtGui/QDesktopServices> +#include <QtGui/QStyle> +#include "notebookmodel.h" + +#define NUM_COLUMNS 3 +#define PEN_INDEX_ID 0xFFFFFFFFU + +NotebookModel::NotebookModel(QObject *parent) : + QAbstractItemModel(parent), + _dataDir(QDesktopServices::storageLocation(QDesktopServices::DataLocation)), + _watcher() +{ + if (!_dataDir.exists()) { + if (!_dataDir.mkpath(".")) { + qWarning() << "Cannot create my data directory:" << _dataDir.absolutePath(); + } + } + _watcher.addPath(_dataDir.absolutePath()); + connect(&_watcher, SIGNAL(directoryChanged(QString)), SLOT(handleChangedDirectory(QString))); + refresh(); +} + +QString NotebookModel::penDirectory(const QString &name) const +{ + return _dataDir.filePath(name + ".pen"); +} + +QString NotebookModel::notebookDirectory(const QString &penName, const QString &nbName) const +{ + return _dataDir.filePath(penName + ".pen" + "/" + nbName + ".afd"); +} + +QString NotebookModel::notebookDirectory(const QModelIndex &index) const +{ + if (!index.isValid()) return QString(); + quint32 id = index.internalId(); + if (id != PEN_INDEX_ID) { + const QString &penName = _pens[id]; + const QStringList ¬ebooks = _notebooks[penName]; + const QString ¬ebookName = notebooks.at(index.row()); + return notebookDirectory(penName, notebookName); + } + return QString(); +} + +QVariant NotebookModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) return QVariant(); + quint32 id = index.internalId(); + if (id == PEN_INDEX_ID) { + int penIndex = index.row(); + if (penIndex < 0 || penIndex >= _pens.size()) return QVariant(); + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case 0: + return _pens[penIndex]; + } + break; + } + } else { + const QString &penName = _pens[id]; + const QStringList ¬ebooks = _notebooks[penName]; + const QString ¬ebookName = notebooks.at(index.row()); + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case 0: + return notebooks.at(index.row()); + case 1: + return 0; + } + break; + case Qt::DecorationRole: + switch (index.column()) { + case 0: + return getNotebookIcon(penName, notebookName); + case 1: + return QVariant(); + case 2: + if (isNotebookLocked(penName, notebookName)) { + return QApplication::style()->standardIcon(QStyle::SP_BrowserReload); + } + } + case Qt::TextAlignmentRole: + switch (index.column()) { + case 0: + return Qt::AlignLeft; + case 1: + case 2: + return Qt::AlignCenter; + } + } + } + return QVariant(); +} + +Qt::ItemFlags NotebookModel::flags(const QModelIndex &index) const +{ + switch (index.column()) { + case 0: + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + default: + return Qt::ItemIsSelectable; + } +} + +QModelIndex NotebookModel::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid()) { + quint32 id = parent.internalId(); + if (id == PEN_INDEX_ID) { + // Pen index + const quint32 penIndex = parent.row(); + const QString &penName = _pens.at(penIndex); + const QStringList ¬ebooks = _notebooks[penName]; + if (row >= 0 && row < notebooks.size() && column >= 0 && column < NUM_COLUMNS) { + return createIndex(row, column, penIndex); + } else { + return QModelIndex(); + } + } else { + // Notebook index + return QModelIndex(); // Nothing inside notebooks + } + } else if (row >= 0 && row < _pens.size() && column >= 0 && column < NUM_COLUMNS) { + return createIndex(row, column, PEN_INDEX_ID); + } else { + return QModelIndex(); + } +} + +QModelIndex NotebookModel::parent(const QModelIndex &child) const +{ + if (child.isValid()) { + quint32 id = child.internalId(); + if (id == PEN_INDEX_ID) { + return QModelIndex(); // Pens have no parent + } else { + return createIndex(id, 0, PEN_INDEX_ID); + } + } else { + return QModelIndex(); + } +} + +int NotebookModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + quint32 id = parent.internalId(); + if (id == PEN_INDEX_ID) { + const quint32 penIndex = parent.row(); + const QString &penName = _pens.at(penIndex); + const QStringList ¬ebooks = _notebooks[penName]; + return notebooks.size(); + } else { + return 0; // Nothing inside notebooks + } + } else { + return _pens.size(); + } +} + +int NotebookModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return NUM_COLUMNS; +} + +void NotebookModel::refresh() +{ + QStringList pens = _dataDir.entryList(QStringList("*.pen"), QDir::Dirs, QDir::Name); + for (int i = 0; i < pens.size(); i++) { + pens[i].chop(4); + } + + int i = 0, j = 0; + while (i < _pens.size() && j < pens.size()) { + int comp = QString::compare(_pens[i], pens[j], Qt::CaseInsensitive); + if (comp == 0) { + ++i; + ++j; + } else if (comp > 0) { /* _pens[i] > pens[j] */ + beginInsertRows(QModelIndex(), i, i); + _pens.insert(i, pens[j]); + endInsertRows(); + ++i; + ++j; + } else { /* _pens[i] < pens[j] */ + beginRemoveRows(QModelIndex(), i, i); + _notebooks.remove(_pens[i]); + _pens.removeAt(i); + endRemoveRows(); + } + } + if (i < _pens.size()) { + Q_ASSERT(j == pens.size()); + beginRemoveRows(QModelIndex(), i, _pens.size() - 1); + while (i < _pens.size()) { + _notebooks.remove(_pens[i]); + _pens.removeAt(i); + } + endRemoveRows(); + } else if (j < pens.size()) { + Q_ASSERT(i == _pens.size()); + beginInsertRows(QModelIndex(), i, i + (pens.size() - j)); + _pens.append(pens.mid(j)); + endInsertRows(); + } + + foreach (const QString &pen, _pens) { + refreshPen(pen); + } +} + +void NotebookModel::refreshPen(const QString &name) +{ + QDir penDir(penDirectory(name)); + if (!penDir.exists()) { + return; + } + + _watcher.addPath(penDir.canonicalPath()); + + QStringList &curNotebooks = _notebooks[name]; + QStringList diskNotebooks = penDir.entryList(QStringList("*.afd"), QDir::Dirs, QDir::Name); + for (int i = 0; i < diskNotebooks.size(); i++) { + diskNotebooks[i].chop(4); + } + + QModelIndex penIndex = index(indexOfPen(name), 0, QModelIndex()); + + int i = 0, j = 0; + while (i < curNotebooks.size() && j < diskNotebooks.size()) { + int comp = QString::compare(curNotebooks[i], diskNotebooks[j], Qt::CaseInsensitive); + if (comp == 0) { + ++i; + ++j; + } else if (comp > 0) { /* _pens[i] > pens[j] */ + beginInsertRows(penIndex, i, i); + curNotebooks.insert(i, diskNotebooks[j]); + endInsertRows(); + ++i; + ++j; + } else { /* _pens[i] < pens[j] */ + beginRemoveRows(penIndex, i, i); + curNotebooks.removeAt(i); + endRemoveRows(); + } + } + if (i < curNotebooks.size()) { + Q_ASSERT(j == diskNotebooks.size()); + beginRemoveRows(penIndex, i, curNotebooks.size() - 1); + curNotebooks.erase(curNotebooks.begin() + i, curNotebooks.end()); + endRemoveRows(); + } else if (j < diskNotebooks.size()) { + Q_ASSERT(i == curNotebooks.size()); + beginInsertRows(penIndex, i, i + (diskNotebooks.size() - j)); + curNotebooks.append(diskNotebooks.mid(j)); + endInsertRows(); + } + + qDebug() << "Found" << curNotebooks.size() << "notebook for pen" << name; +} + +int NotebookModel::indexOfPen(const QString &name) +{ + QStringList::const_iterator it = qBinaryFind(_pens, name); + if (it == _pens.end()) { + return -1; + } else { + return it - _pens.begin(); + } +} + +QDir NotebookModel::notebookDir(const QString &pen, const QString ¬ebook) const +{ + return QDir(_dataDir.filePath("%1.pen/%2.afd").arg(pen, notebook)); +} + +QIcon NotebookModel::getNotebookIcon(const QString &pen, const QString ¬ebook) const +{ + static QStringList candidates; + if (candidates.isEmpty()) { + candidates << "userdata/icon/Notebook.png" + << "userdata/icon/active_64x64.png" + << "userdata/icon/active_32x32.png" + << "userdata/icon/active_16x16.png"; + } + QIcon icon; + + QDir dir = notebookDir(pen, notebook); + if (dir.exists()) { + foreach (const QString &candidate, candidates) { + if (dir.exists(candidate)) { + icon.addFile(dir.filePath(candidate)); + } + } + } + + return icon; +} + +bool NotebookModel::isNotebookLocked(const QString &pen, const QString ¬ebook) const +{ + QDir dir = notebookDir(pen, notebook); + if (dir.exists(".sync.lck")) { + return true; // TODO check if stale + } else { + return false; + } +} + +void NotebookModel::handleChangedDirectory(const QString &path) +{ + qDebug() << "changed" << path; + if (path == _dataDir.absolutePath()) { + refresh(); + } +} diff --git a/notebookmodel.h b/notebookmodel.h new file mode 100644 index 0000000..83bb004 --- /dev/null +++ b/notebookmodel.h @@ -0,0 +1,49 @@ +#ifndef NOTEBOOKMODEL_H +#define NOTEBOOKMODEL_H + +#include <QtCore/QAbstractItemModel> +#include <QtCore/QDir> +#include <QtCore/QFileSystemWatcher> + +class NotebookModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit NotebookModel(QObject *parent = 0); + + QString penDirectory(const QString &name) const; + QString notebookDirectory(const QString &penName, const QString &nbName) const; + QString notebookDirectory(const QModelIndex &index) const; + + QVariant data(const QModelIndex &index, int role) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + QModelIndex index(int row, int column, const QModelIndex &parent) const; + QModelIndex parent(const QModelIndex &child) const; + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + +signals: + +public slots: + void refresh(); + void refreshPen(const QString &name); + +private: + +private: + int indexOfPen(const QString &name); + QDir notebookDir(const QString &pen, const QString ¬ebook) const; + QIcon getNotebookIcon(const QString &pen, const QString ¬ebook) const; + bool isNotebookLocked(const QString &pen, const QString ¬ebook) const; + +private slots: + void handleChangedDirectory(const QString &path); + +private: + QDir _dataDir; + QFileSystemWatcher _watcher; + QStringList _pens; + QHash<QString, QStringList> _notebooks; +}; + +#endif // NOTEBOOKMODEL_H diff --git a/notebookview.cc b/notebookview.cc new file mode 100644 index 0000000..2605792 --- /dev/null +++ b/notebookview.cc @@ -0,0 +1,194 @@ +#include <QtCore/QDebug> +#include <QtGui/QResizeEvent> +#include "notebookview.h" + +#define VIEW_MARGIN 2 +#define PAGE_SEPARATION 100 + +NotebookView::NotebookView(QWidget *parent) : + QGraphicsView(parent), _nb(new AfdNotebook(this)), + _zoom(100), _curPage(0) +{ + setScene(new QGraphicsScene(this)); + setTransformationAnchor(AnchorUnderMouse); + setDragMode(ScrollHandDrag); + setRenderHints(QPainter::Antialiasing); +} + +void NotebookView::setNotebook(const QString &path) +{ + removePages(); + if (!path.isEmpty()) { + if (_nb->open(path)) { + _nbPath = path; + if (_zoom > 100) { + _zoom = 100; + emit zoomChanged(); + } + createPages(); + emit pageNumbersChanged(); + if (!_pages.isEmpty()) { + _curPage = _pages.begin().key(); + centerOn(_pages[_curPage]); + emit curPageChanged(); + } + } else { + qWarning() << "Could not open notebook:" << _nbPath; + } + } +} + +QString NotebookView::notebook() const +{ + return _nbPath; +} + +QList<int> NotebookView::pageNumbers() const +{ + return _pages.keys(); +} + +int NotebookView::curPage() const +{ + return _curPage; +} + +void NotebookView::setCurPage(int page) +{ + if (page != _curPage) { + _curPage = page; + if (_zoom > 100) { + setZoom(100); + } + if (_pages.contains(_curPage)) { + centerOn(_pages[_curPage]); + } + emit curPageChanged(); + } +} + +int NotebookView::zoom() const +{ + return _zoom; +} + +void NotebookView::setZoom(int zoom) +{ + if (zoom != _zoom) { + _zoom = zoom; + calculateScale(); + } +} + +void NotebookView::clear() +{ + removePages(); + emit pageNumbersChanged(); +} + +void NotebookView::prevPage() +{ + QMap<int, PageItem*>::iterator it = _pages.lowerBound(_curPage); + if (it != _pages.end() && it != _pages.begin()) { + --it; + setCurPage(it.key()); + } +} + +void NotebookView::nextPage() +{ + QMap<int, PageItem*>::iterator it = _pages.upperBound(_curPage); + if (it != _pages.end()) { + setCurPage(it.key()); + } +} + +void NotebookView::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + calculateScale(); +} + +void NotebookView::scrollContentsBy(int dx, int dy) +{ + QGraphicsView::scrollContentsBy(dx, dy); + QGraphicsItem *item = itemAt(size().width() / 2, size().height() / 2); + while (item && item->type() != PageItem::Type) { + item = item->parentItem(); + } + if (item && item->type() == PageItem::Type) { + PageItem * page = static_cast<PageItem*>(item); + int centerPage = page->pageNum(); + if (centerPage != _curPage) { + _curPage = centerPage; + emit curPageChanged(); + } + } +} + +void NotebookView::removePages() +{ + _pages.clear(); + scene()->clear(); + scene()->setSceneRect(QRectF()); + _nb->close(); + _nbPath.clear(); + _maxPageSize.setWidth(0); + _maxPageSize.setHeight(0); + resetTransform(); +} + +void NotebookView::createPages() +{ + QStringList pens = _nb->penSerials(); + if (pens.isEmpty()) return; + + QList<int> pagesWithStrokes = _nb->pagesWithStrokes(pens.first()); + Q_ASSERT(_pages.isEmpty()); + + _maxPageSize.setWidth(0); + _maxPageSize.setHeight(0); + foreach (int pageNum, pagesWithStrokes) { + PageItem *page = new PageItem(_nb, pageNum); + QRectF box = page->boundingRect(); + if (box.width() > _maxPageSize.width()) { + _maxPageSize.setWidth(box.width()); + } + if (box.height() > _maxPageSize.height()) { + _maxPageSize.setHeight(box.height()); + } + _pages.insert(pageNum, page); + } + + calculateScale(); + + qreal curY = 0; + foreach (PageItem *page, _pages) { + QRectF box = page->boundingRect(); + page->setPos((_maxPageSize.width() - box.width()) / 2.0, curY); + curY += box.height(); + curY += PAGE_SEPARATION; + + scene()->addItem(page); + } + + scene()->setSceneRect(0, 0, _maxPageSize.width(), curY); +} + +void NotebookView::calculateScale() +{ + if (_pages.isEmpty() || _maxPageSize.isEmpty()) return; + const int margin = VIEW_MARGIN; + QRectF viewRect = viewport()->rect().adjusted(margin, margin, -margin, margin); + qreal baseScale = qMin(viewRect.width() / _maxPageSize.width(), + viewRect.height() / _maxPageSize.height()); + resetTransform(); + scale(baseScale, baseScale); + if (_zoom < 100) { + qreal s = 0.25 + ((_zoom / 100.0) * 0.75); + scale(s, s); + } else if (_zoom > 100) { + qreal s = 1.0 + (_zoom - 100) * 0.015; + scale(s, s); + } +} diff --git a/notebookview.h b/notebookview.h new file mode 100644 index 0000000..e51afa3 --- /dev/null +++ b/notebookview.h @@ -0,0 +1,59 @@ +#ifndef NOTEBOOKVIEW_H +#define NOTEBOOKVIEW_H + +#include <QtGui/QGraphicsView> +#include <QtGui/QGraphicsItem> +#include "afdnotebook.h" +#include "pageitem.h" + +class NotebookView : public QGraphicsView +{ + Q_OBJECT + Q_PROPERTY(QString notebook WRITE setNotebook READ notebook) + Q_PROPERTY(QList<int> pageNumbers READ pageNumbers NOTIFY pageNumbersChanged) + Q_PROPERTY(int curPage READ curPage WRITE setCurPage NOTIFY curPageChanged) + Q_PROPERTY(int zoom READ zoom WRITE setZoom NOTIFY zoomChanged) + +public: + explicit NotebookView(QWidget *parent = 0); + + void setNotebook(const QString &path); + QString notebook() const; + + QList<int> pageNumbers() const; + + int curPage() const; + void setCurPage(int page); + + int zoom() const; + +signals: + void pageNumbersChanged(); + void curPageChanged(); + void zoomChanged(); + +public slots: + void clear(); + void setZoom(int zoom); + void prevPage(); + void nextPage(); + +protected: + void resizeEvent(QResizeEvent *event); + void scrollContentsBy(int dx, int dy); + +private: + void removePages(); + void createPages(); + void calculateScale(); + +private: + AfdNotebook *_nb; + QString _nbPath; + QMap<int, PageItem*> _pages; + QSizeF _maxPageSize; + int _zoom; + int _curPage; +}; + +#endif // NOTEBOOKVIEW_H diff --git a/pageitem.cc b/pageitem.cc new file mode 100644 index 0000000..f938eeb --- /dev/null +++ b/pageitem.cc @@ -0,0 +1,64 @@ +#include <QtCore/QDebug> +#include <QtGui/QPen> +#include "pageitem.h" +#include "stfgraphicsitem.h" + +PageItem::PageItem(AfdNotebook *nb, int pageNum, QGraphicsItem *parent) : + QGraphicsItem(parent), _nb(nb), _pageNum(pageNum), _strokesLoaded(false) +{ + _pageSize = _nb->getPageSize(pageNum); + _pageTrim = _nb->getPageTrim(pageNum); + + QGraphicsRectItem *border = new QGraphicsRectItem(_pageTrim, this); + border->setPen(QPen(Qt::black)); + + QPixmap bgPix = _nb->getPageBackground(_pageNum); + QGraphicsPixmapItem *bg = new QGraphicsPixmapItem(bgPix, this); + bg->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); + bg->setTransformationMode(Qt::SmoothTransformation); + QRectF bgRect = bg->boundingRect(); + bg->scale(_pageTrim.width() / bgRect.width(), _pageTrim.height() / bgRect.height()); + bg->setPos(_pageTrim.topLeft()); +} + +QRectF PageItem::boundingRect() const +{ + return QRectF(QPointF(0, 0), _pageSize); +} + +void PageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(painter); + Q_UNUSED(option); + Q_UNUSED(widget); + if (!_strokesLoaded) { + createStrokes(); + _strokesLoaded = true; + } +} + +int PageItem::type() const +{ + return Type; +} + +int PageItem::pageNum() const +{ + return _pageNum; +} + +void PageItem::createStrokes() +{ + QStringList pens = _nb->penSerials(); + if (pens.isEmpty()) return; + QStringList strokeFiles = _nb->strokeFiles(pens.first(), _pageNum); + foreach (const QString &strokeFile, strokeFiles) { + QFile f(strokeFile); + if (!f.open(QIODevice::ReadOnly)) { + qWarning() << "Could not open stroke file:" << strokeFile; + continue; + } + new StfGraphicsItem(&f, this); + } + qDebug() << "strokes loaded for page" << _pageNum; +} diff --git a/pageitem.h b/pageitem.h new file mode 100644 index 0000000..2e6059a --- /dev/null +++ b/pageitem.h @@ -0,0 +1,32 @@ +#ifndef PAGEITEM_H +#define PAGEITEM_H + +#include <QtGui/QGraphicsItem> + +#include "afdnotebook.h" + +class PageItem : public QGraphicsItem +{ +public: + explicit PageItem(AfdNotebook *nb, int pageNum, QGraphicsItem *parent = 0); + + enum { Type = UserType + 'p' }; + + QRectF boundingRect() const; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + int type() const; + + int pageNum() const; + +private: + void createStrokes(); + +private: + AfdNotebook *_nb; + int _pageNum; + QSize _pageSize; + QRect _pageTrim; + bool _strokesLoaded; +}; + +#endif // PAGEITEM_H diff --git a/scribiu.pro b/scribiu.pro new file mode 100644 index 0000000..641f286 --- /dev/null +++ b/scribiu.pro @@ -0,0 +1,35 @@ +QT += core gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = scribiu +TEMPLATE = app + +CONFIG += link_pkgconfig +PKGCONFIG += libudev libusb openobex +LIBS += -lquazip + +SOURCES += main.cc\ + mainwindow.cc \ + smartpenmanager.cc \ + notebookmodel.cc \ + smartpensyncer.cc \ + smartpen.cc bitreader.cc stfreader.cc \ + xmlutils.cc \ + notebookview.cc \ + afdnotebook.cc \ + pageitem.cc \ + stfgraphicsitem.cc + +HEADERS += mainwindow.h \ + smartpenmanager.h \ + notebookmodel.h \ + smartpensyncer.h \ + smartpen.h bitreader.h stfreader.h \ + xmlutils.h \ + notebookview.h \ + afdnotebook.h \ + pageitem.h \ + stfgraphicsitem.h + +FORMS += mainwindow.ui diff --git a/smartpen.cc b/smartpen.cc new file mode 100644 index 0000000..6df3fd1 --- /dev/null +++ b/smartpen.cc @@ -0,0 +1,424 @@ +#include <QtCore/QDateTime> +#include <QtCore/QDebug> +#include <QtCore/QtEndian> +#include <usb.h> +#include "xmlutils.h" +#include "smartpen.h" + +#define PEN_EPOCH (1289335960000LL) +#define PEN_MTU 900 +#define PEN_TIMEOUT_SECONDS 10 + +#define INVALID_CID 0xFFFFFFFFU + +/* Terrible hack comes now: */ +struct obex_usb_intf_transport_t { + struct obex_usb_intf_transport_t *prev, *next; /* Next and previous interfaces in the list */ + struct usb_device *device; /* USB device that has the interface */ +}; + +Smartpen::Smartpen(QObject *parent) : + QObject(parent), _obex(0), _connId(INVALID_CID) +{ +} + +Smartpen::~Smartpen() +{ + if (_connId != INVALID_CID || _obex) { + disconnectFromPen(); + } +} + +bool Smartpen::isConnected() const +{ + return _obex && _connId != INVALID_CID; +} + +QByteArray Smartpen::getObject(const QString &name) +{ + obex_object_t *obj = OBEX_ObjectNew(_obex, OBEX_CMD_GET); + Q_ASSERT(obj); + + addConnHeader(obj); + + obex_headerdata_t hd; + QByteArray encodedName = encodeUtf16(name); + hd.bs = reinterpret_cast<const uint8_t*>(encodedName.constData()); + if (OBEX_ObjectAddHeader(_obex, obj, OBEX_HDR_NAME, hd, encodedName.size(), 0) < 0) { + qCritical("Could not add name header"); + OBEX_ObjectDelete(_obex, obj); + return QByteArray(); + } + + qDebug() << "Getting object" << name; + + if (OBEX_Request(_obex, obj) < 0) { + qWarning() << "Get object request failed"; + return QByteArray(); + } + + QDateTime start = QDateTime::currentDateTimeUtc(); + QDateTime now; + do { + OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); + now = QDateTime::currentDateTimeUtc(); + } while (_inBuf.isEmpty() && start.secsTo(now) < PEN_TIMEOUT_SECONDS); + + if (_inBuf.isEmpty()) { + qWarning() << "Did not receive any data in" << start.secsTo(now) << "seconds"; + } + + QByteArray result; + qSwap(_inBuf, result); + + return result; +} + +QString Smartpen::getParameter(Parameters parameter) +{ + QString objectName = QString("ppdata?key=pp%1").arg(int(parameter), 4); + QByteArray data = getObject(objectName); + QXmlStreamReader r(data); + + advanceToFirstChildElement(r, "xml"); + advanceToFirstChildElement(r, "parameter"); + + if (!r.atEnd()) { + QXmlStreamAttributes attrs = r.attributes(); + return attrs.value("value").toString(); + } + + return QString(); +} + +QString Smartpen::getPenName() +{ + QString name = getParameter(PenName); + if (name.isEmpty()) { + return name; + } + + QByteArray hex = QByteArray::fromHex(name.mid(2).toLatin1()); + return QString::fromUtf8(hex); +} + +QVariantMap Smartpen::getPenInfo() +{ + QVariantMap result; + QByteArray data = getObject("peninfo"); + QXmlStreamReader r(data); + + advanceToFirstChildElement(r, "xml"); + advanceToFirstChildElement(r, "peninfo"); + + if (!r.atEnd()) { + Q_ASSERT(r.isStartElement() && r.name() == "peninfo"); + QString penId = r.attributes().value("penid").toString(); + result["penid"] = penId; + result["penserial"] = toPenSerial(penId.mid(2).toULongLong(0, 16)); + + while (r.readNextStartElement()) { + if (r.name() == "battery") { + result["battery"] = r.attributes().value("level").toString(); + r.skipCurrentElement(); + } else if (r.name() == "time") { + result["time"] = r.attributes().value("absolute").toString(); + r.skipCurrentElement(); + } else { + r.skipCurrentElement(); + } + } + } else { + qWarning() << "Could not parse peninfo XML"; + } + + return result; +} + +QList<Smartpen::ChangeReport> Smartpen::getChangeList(const QDateTime &from) +{ + QList<ChangeReport> result; + QByteArray data = getObject(QString("changelist?start_time=%1").arg(toPenTime(from))); + QXmlStreamReader r(data); + + advanceToFirstChildElement(r, "xml"); + advanceToFirstChildElement(r, "changelist"); + + if (!r.atEnd()) { + Q_ASSERT(r.isStartElement() && r.name() == "changelist"); + + while (r.readNextStartElement()) { + if (r.name() == "lsp") { + QXmlStreamAttributes attrs = r.attributes(); + ChangeReport report; + if (attrs.hasAttribute("guid")) { + report.guid = attrs.value("guid").toString(); + report.title = attrs.value("title").toString(); + result.append(report); + } + r.skipCurrentElement(); + } else { + r.skipCurrentElement(); + } + } + + } else { + qWarning() << "Could not parse changelist XML"; + } + + return result; +} + +QByteArray Smartpen::getLspData(const QString &name, const QDateTime &from) +{ + return getObject(QString("lspdata?name=%1&start_time=%2").arg(name).arg(toPenTime(from))); +} + +qint64 Smartpen::toPenTime(const QDateTime &dt) +{ + if (dt.isValid()) { + return dt.toMSecsSinceEpoch() - PEN_EPOCH; + } else { + return 0; + } +} + +QDateTime Smartpen::fromPenTime(qint64 t) +{ + if (t) { + return QDateTime::fromMSecsSinceEpoch(t + PEN_EPOCH).toLocalTime(); + } else { + return QDateTime(); + } +} + +QString Smartpen::toPenSerial(quint64 id) +{ + QString serial; + serial.reserve(3 + 1 + 3 + 1 + 3 + 1 + 2 + 1); + + serial.append(toPenSerialSegment(id >> 32, 3)); + serial.append('-'); + serial.append(toPenSerialSegment(id, 6).mid(0, 3)); + serial.append('-'); + serial.append(toPenSerialSegment(id, 6).mid(3, 3)); + serial.append('-'); + serial.append(toPenSerialSegment(id % 0x36D, 2)); + + return serial; +} + +bool Smartpen::connectToPen(const Address &addr) +{ + if (_obex) { + qWarning() << "Already connected"; + return false; + } + + _obex = OBEX_Init(OBEX_TRANS_USB, obexEventCb, 0); + Q_ASSERT(_obex); + + OBEX_SetUserData(_obex, this); + OBEX_SetTransportMTU(_obex, PEN_MTU, PEN_MTU); + + obex_interface_t *interfaces, *ls_interface = 0; + int count = OBEX_FindInterfaces(_obex, &interfaces); + for (int i = 0; i < count; i++) { + if (interfaces[i].usb.intf->device->bus->location == addr.first && + interfaces[i].usb.intf->device->devnum == addr.second) { + ls_interface = &interfaces[i]; + } + } + + if (!ls_interface) { + qWarning() << "Could not find Lightscribe interface on device:" << addr; + return false; + } + + usb_dev_handle *handle = usb_open(ls_interface->usb.intf->device); + if (handle) { + qDebug() << "resetting usb device"; + usb_reset(handle); + usb_close(handle); + } else { + qWarning() << "could not open usb device for resetting"; + } + + qDebug() << "connecting to" << ls_interface->usb.product; + + if (OBEX_InterfaceConnect(_obex, ls_interface) < 0) { + qWarning() << "Could not connect to Livescribe interface"; + return false; + } + + static const char * livescribe_service = "LivescribeService"; + obex_object_t *object = OBEX_ObjectNew(_obex, OBEX_CMD_CONNECT); + obex_headerdata_t hd; + int hd_len; + + Q_ASSERT(object); + + hd.bs = reinterpret_cast<const quint8*>(livescribe_service); + hd_len = strlen(livescribe_service) + 1; + if (OBEX_ObjectAddHeader(_obex, object, OBEX_HDR_TARGET, hd, hd_len, 0) < 0) { + qWarning() << "Failed to add Target header"; + OBEX_ObjectDelete(_obex, object); + return false; + } + + if (OBEX_Request(_obex, object) < 0) { + qWarning() << "Failed to make connection request"; + OBEX_ObjectDelete(_obex, object); + return false; + } + + qDebug() << "Connection in progress"; + + OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); + + return _connId != INVALID_CID; +} + +void Smartpen::disconnectFromPen() +{ + if (_connId != INVALID_CID) { + if (_obex) { + obex_object_t *object = OBEX_ObjectNew(_obex, OBEX_CMD_DISCONNECT); + Q_ASSERT(object); + addConnHeader(object); + OBEX_Request(_obex, object); + OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); + } + _connId = INVALID_CID; + } + if (_obex) { + OBEX_Cleanup(_obex); + _obex = 0; + } + _inBuf.clear(); +} + +void Smartpen::obexEventCb(obex_t *handle, obex_object_t *obj, + int mode, int event, int obex_cmd, int obex_rsp) +{ + Smartpen *smartpen = static_cast<Smartpen*>(OBEX_GetUserData(handle)); + Q_UNUSED(mode); + smartpen->handleObexEvent(obj, event, obex_cmd, obex_rsp); +} + +void Smartpen::handleObexEvent(obex_object_t *object, + int event, int obex_cmd, int obex_rsp) +{ + + switch (event) { + case OBEX_EV_PROGRESS: + if (obex_cmd == OBEX_CMD_GET) { + // It seems that the pen wants us to add this header on every continue response + addConnHeader(object); + } + break; + case OBEX_EV_REQDONE: + qDebug() << "event reqdone cmd=" << obex_cmd << " rsp=" << OBEX_ResponseToString(obex_rsp); + handleObexRequestDone(object, obex_cmd, obex_rsp); + break; + case OBEX_EV_LINKERR: + qWarning() << "link error cmd=" << obex_cmd; + emit error(); + break; + default: + qDebug() << "event" << event << obex_cmd << obex_rsp; + break; + } +} + +void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int obex_rsp) +{ + quint8 header_id; + obex_headerdata_t hdata; + quint32 hlen; + + switch (obex_cmd & ~OBEX_FINAL) { + case OBEX_CMD_CONNECT: + switch (obex_rsp) { + case OBEX_RSP_SUCCESS: + while (OBEX_ObjectGetNextHeader(_obex, object, &header_id, &hdata, &hlen)) { + if (header_id == OBEX_HDR_CONNECTION) { + Q_ASSERT(_connId == INVALID_CID); + _connId = hdata.bq4; + qDebug() << "Connection established, id:" << _connId; + } + } + break; + default: + qWarning() << "Failed connection request:" << OBEX_ResponseToString(obex_rsp); + emit error(); + break; + } + break; + case OBEX_CMD_DISCONNECT: + switch (obex_rsp) { + case OBEX_RSP_SUCCESS: + qDebug() << "Disconnected succesfully"; + _connId = INVALID_CID; + break; + default: + qWarning() << "Failed disconnection request:" << OBEX_ResponseToString(obex_rsp); + _connId = INVALID_CID; + break; + } + + break; + case OBEX_CMD_GET: + switch (obex_rsp) { + case OBEX_RSP_SUCCESS: + qDebug() << "GET request succesful"; + while (OBEX_ObjectGetNextHeader(_obex, object, &header_id, &hdata, &hlen)) { + if (header_id == OBEX_HDR_BODY || header_id == OBEX_HDR_BODY_END) { + _inBuf = QByteArray(reinterpret_cast<const char*>(hdata.bs), hlen); + } + } + break; + default: + qWarning() << "Failed GET request:" << OBEX_ResponseToString(obex_rsp); + break; + } + + break; + } +} + +QString Smartpen::toPenSerialSegment(quint32 id, int len) +{ + static const char chars[] = "ABCDEFGHJKMNPQRSTUWXYZ23456789"; + static const unsigned int num_chars = sizeof(chars) - 1; + + QString segment(len, Qt::Uninitialized); + + for (int i = 0; i < len; i++) { + segment[len - (i + 1)] = chars[id % num_chars]; + id /= num_chars; + } + + return segment; +} + +QByteArray Smartpen::encodeUtf16(const QString &s) +{ + const int size = s.size(); + QByteArray data((size + 1) * sizeof(quint16), Qt::Uninitialized); + quint16 *p = reinterpret_cast<quint16*>(data.data()); + for (int i = 0; i < size; i++) { + p[i] = qToBigEndian(s.at(i).unicode()); + } + p[size] = 0; + return data; +} + +void Smartpen::addConnHeader(obex_object_t *obj) const +{ + obex_headerdata_t hd; + hd.bq4 = _connId; + if (OBEX_ObjectAddHeader(_obex, obj, OBEX_HDR_CONNECTION, hd, sizeof(hd.bq4), 0) < 0) { + qCritical() << "Could not add connection header"; + } +} diff --git a/smartpen.h b/smartpen.h new file mode 100644 index 0000000..c93e016 --- /dev/null +++ b/smartpen.h @@ -0,0 +1,78 @@ +#ifndef SMARTPEN_H +#define SMARTPEN_H + +#include <QtCore/QObject> +#include <QtCore/QDateTime> +#include <QtCore/QVariantMap> +#include <openobex/obex.h> + +// TODO: These values are mostly random. +#define SMARTPEN_DPI_X (800.0) +#define SMARTPEN_DPI_Y (800.0) + +#define SMARTPEN_BLEED_X 333.3 +#define SMARTPEN_BLEED_Y 333.3 + +class Smartpen : public QObject +{ + Q_OBJECT + +public: + explicit Smartpen(QObject *parent = 0); + ~Smartpen(); + + typedef QPair<int, int> Address; + + bool isConnected() const; + + enum Parameters { + PenName = 8011 + }; + + QByteArray getObject(const QString& name); + QString getParameter(Parameters parameter); + + QString getPenName(); + QVariantMap getPenInfo(); + + struct ChangeReport { + QString guid; + QString title; + }; + + QList<ChangeReport> getChangeList(const QDateTime &from = QDateTime()); + + QByteArray getLspData(const QString &name, const QDateTime &from = QDateTime()); + QByteArray getPaperReplay(); + + static qint64 toPenTime(const QDateTime &dt); + static QDateTime fromPenTime(qint64 t); + + static QString toPenSerial(quint64 id); + +public slots: + bool connectToPen(const Address &addr); + void disconnectFromPen(); + +signals: + void error(); + +private: + static void obexEventCb(obex_t *handle, obex_object_t *obj, + int mode, int event, int obex_cmd, int obex_rsp); + void handleObexEvent(obex_object_t *object, + int event, int obex_cmd, int obex_rsp); + void handleObexRequestDone(obex_object_t *object, int obex_cmd, int obex_rsp); + + static QString toPenSerialSegment(quint32 id, int len); + + static QByteArray encodeUtf16(const QString &s); + void addConnHeader(obex_object_t *object) const; + +private: + obex_t * _obex; + quint32 _connId; + QByteArray _inBuf; +}; + +#endif // SMARTPEN_H diff --git a/smartpenmanager.cc b/smartpenmanager.cc new file mode 100644 index 0000000..d560b5b --- /dev/null +++ b/smartpenmanager.cc @@ -0,0 +1,69 @@ +#include <QtCore/QDebug> +#include <libudev.h> + +#include "smartpenmanager.h" + +SmartpenManager::SmartpenManager(QObject *parent) + : QObject(parent), _udev(udev_new()), _monitor(udev_monitor_new_from_netlink(_udev, "udev")), + _notifier(new QSocketNotifier(udev_monitor_get_fd(_monitor), QSocketNotifier::Read)) +{ + udev_monitor_filter_add_match_tag(_monitor, "livescribe-pen"); + + connect(_notifier, SIGNAL(activated(int)), SLOT(handleMonitorActivity())); + + udev_monitor_enable_receiving(_monitor); + + udev_enumerate *scan = udev_enumerate_new(_udev); + udev_enumerate_add_match_tag(scan, "livescribe-pen"); + + if (udev_enumerate_scan_devices(scan) == 0) { + udev_list_entry *l = udev_enumerate_get_list_entry(scan), *i; + udev_list_entry_foreach(i, l) { + const char *path = udev_list_entry_get_name(i); + udev_device *dev = udev_device_new_from_syspath(_udev, path); + processDevice(dev); + udev_device_unref(dev); + } + } else { + qWarning() << "Failed to scan for devices"; + } + + udev_enumerate_unref(scan); +} + +SmartpenManager::~SmartpenManager() +{ + delete _notifier; + udev_monitor_unref(_monitor); + udev_unref(_udev); +} + +void SmartpenManager::handleMonitorActivity() +{ + qDebug() << "udev activity"; + udev_device *dev = udev_monitor_receive_device(_monitor); + udev_device_unref(dev); +} + +void SmartpenManager::handleSyncerFinished() +{ + SmartpenSyncer *syncer = static_cast<SmartpenSyncer*>(sender()); + Smartpen::Address addr = syncer->penAddress(); + qDebug() << "Finished synchronization with pen with address:" << addr; + _syncers.remove(addr); + syncer->deleteLater(); +} + +void SmartpenManager::processDevice(udev_device *dev) +{ + uint busnum = atol(udev_device_get_sysattr_value(dev, "busnum")); + uint devnum = atol(udev_device_get_sysattr_value(dev, "devnum")); + + Smartpen::Address addr(busnum, devnum); + if (!_syncers.contains(addr)) { + SmartpenSyncer *syncer = new SmartpenSyncer(addr, this); + _syncers.insert(addr, syncer); + connect(syncer, SIGNAL(finished()), SLOT(handleSyncerFinished())); + syncer->start(); + } +} diff --git a/smartpenmanager.h b/smartpenmanager.h new file mode 100644 index 0000000..d1b4800 --- /dev/null +++ b/smartpenmanager.h @@ -0,0 +1,42 @@ +#ifndef SMARTPENMANAGER_H +#define SMARTPENMANAGER_H + +#include <QtCore/QObject> +#include <QtCore/QSocketNotifier> +#include <QtCore/QMap> +#include <QtCore/QPair> +#include "smartpensyncer.h" + +struct udev; +struct udev_monitor; +struct udev_device; + +class SmartpenManager : public QObject +{ + Q_OBJECT + +public: + explicit SmartpenManager(QObject *parent = 0); + ~SmartpenManager(); + +signals: + void syncComplete(const QString &penName); + +public slots: + +private slots: + void handleMonitorActivity(); + void handleSyncerFinished(); + +private: + void processDevice(udev_device *dev); + +private: + udev *_udev; + udev_monitor *_monitor; + QSocketNotifier *_notifier; + QMap<QPair<int, int>, SmartpenSyncer*> _syncers; + +}; + +#endif // SMARTPENMANAGER_H diff --git a/smartpensyncer.cc b/smartpensyncer.cc new file mode 100644 index 0000000..50304a3 --- /dev/null +++ b/smartpensyncer.cc @@ -0,0 +1,214 @@ +#include <QtCore/QBuffer> +#include <QtCore/QScopedArrayPointer> +#include <QtCore/QThread> +#include <QtCore/QDebug> +#include <QtGui/QDesktopServices> +#include <quazip/quazipfile.h> +#include "smartpensyncer.h" + +#define BUFFER_SIZE 16 * 1024 + +namespace { +static QString cleanFilename(QString s) +{ + static const QRegExp re("[^0-9A-Za-z-_]+"); + return s.replace(re, "_"); +} + +static QDateTime getTimestampFileDate(const QString &path) +{ + QFileInfo info(path); + qDebug() << "Checking timestamp" << info.filePath(); + if (info.exists()) { + return info.lastModified(); + } else { + return QDateTime(); + } +} + +static void setTimestampFileDate(const QString &path) +{ + QFile f(path); + if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qWarning() << "Could not set timestamp file:" << path; + return; + } + f.close(); +} + +void removeTimestampFile(const QString &path) +{ + QFile f(path); + if (!f.remove()) { + qWarning() << "Cannot remove timestamp file:" << path; + } +} +} + +SmartpenSyncer::SmartpenSyncer(const Smartpen::Address &addr, QObject *parent) : + QThread(parent), _addr(addr), _pen(new Smartpen(this)), _errored(false), _aborted(false) +{ +} + +SmartpenSyncer::~SmartpenSyncer() +{ + if (isRunning()) { + _aborted = true; + wait(); + } +} + +Smartpen::Address SmartpenSyncer::penAddress() const +{ + return _addr; +} + +void SmartpenSyncer::abort() +{ + _aborted = true; +} + +void SmartpenSyncer::run() +{ + if (!_pen->connectToPen(_addr)) { + qWarning() << "Could not connect to pen with USB address: " << _addr; + _errored = true; + return; + } + + _penName = _pen->getPenName(); + qDebug() << "got pen name:" << _penName; + + QVariantMap penInfo = _pen->getPenInfo(); + if (penInfo.isEmpty()) { + qWarning() << "Could not get pen info"; + _errored = true; + return; + } + + _penSerial = penInfo["penserial"].toString(); + + _penDataDir.setPath(QDesktopServices::storageLocation(QDesktopServices::DataLocation) + "/" + _penName + ".pen"); + if (!_penDataDir.exists()) { + if (!_penDataDir.mkpath(".")) { + qWarning() << "Cannot create pen data directory:" << _penDataDir.absolutePath(); + } + } + + if (!syncPen()) { + _errored = true; + } + + _pen->disconnectFromPen(); +} + +bool SmartpenSyncer::syncPen() +{ + QDateTime lastSyncTime = getTimestampFileDate(_penDataDir.filePath(".lastsync")); + QList<Smartpen::ChangeReport> changes = _pen->getChangeList(lastSyncTime); + + foreach(const Smartpen::ChangeReport &change, changes) { + qDebug() << "Synchronizing guid: " << change.guid << change.title; + if (!syncNotebook(change)) { + return false; + } + } + + setTimestampFileDate(_penDataDir.filePath(".lastsync")); + + return true; +} + +bool SmartpenSyncer::syncNotebook(const Smartpen::ChangeReport &change) +{ + QDir notebookDir(_penDataDir.filePath(change.title + ".afd")); + if (!notebookDir.exists()) { + if (!notebookDir.mkpath(".")) { + qWarning() << "Cannot create notebook data directory:" << notebookDir.absolutePath(); + } + } + + setTimestampFileDate(notebookDir.filePath(".sync.lck")); + + QDateTime lastSyncTime = getTimestampFileDate(notebookDir.filePath(".lastsync")); + QByteArray lspData = _pen->getLspData(change.guid, lastSyncTime); + + if (!extractZip(lspData, notebookDir)) { + return false; + } + + setTimestampFileDate(notebookDir.filePath(".lastsync")); + removeTimestampFile(notebookDir.filePath(".sync.lck")); + + return true; +} + +bool SmartpenSyncer::extractZip(QByteArray &zipData, QDir &dir) +{ + QBuffer zipBuffer(&zipData); + QuaZip zip(&zipBuffer); + QuaZipFile zipFile(&zip); + + if (!zip.open(QuaZip::mdUnzip)) { + qWarning() << "Could not open zip file"; + return false; + } + + QScopedArrayPointer<char> buffer(new char[BUFFER_SIZE]); + + for (bool more=zip.goToFirstFile(); more; more=zip.goToNextFile()) { + QString zipName = zip.getCurrentFileName(); + if (!dir.absoluteFilePath(zipName).startsWith(dir.absolutePath())) { + qWarning() << "broken zip filename:" << zipName; + continue; + } + QFileInfo finfo(dir.filePath(zipName)); + if (!dir.mkpath(finfo.path())) { + qWarning() << "cannot mkpath for:" << finfo.absoluteFilePath(); + } + + if (zipName.endsWith('/')) { + // Nothing to do + continue; + } + + if (!zipFile.open(QIODevice::ReadOnly)) { + qWarning() << "cannot open zip file for reading:" << zipName; + continue; + } + + QFile file(finfo.filePath()); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qWarning() << "cannot open for writing:" << finfo.absoluteFilePath(); + zipFile.close(); + continue; + } + + while (!zipFile.atEnd()) { + qint64 read = zipFile.read(buffer.data(), BUFFER_SIZE); + if (read <= 0) { + qWarning() << "short read on:" << zipName; + zipFile.close(); + continue; + } + qint64 written = file.write(buffer.data(), read); + if (written != read) { + qWarning() << "short write on:" << file.fileName(); + zipFile.close(); + continue; + } + } + + file.close(); + zipFile.close(); + } + + buffer.reset(); + + if (zip.getZipError() == UNZ_OK) { + return true; + } else { + qWarning() << "Error while decompressing"; + return false; + } +} diff --git a/smartpensyncer.h b/smartpensyncer.h new file mode 100644 index 0000000..6279485 --- /dev/null +++ b/smartpensyncer.h @@ -0,0 +1,39 @@ +#ifndef SMARTPENSYNCER_H +#define SMARTPENSYNCER_H + +#include <QtCore/QThread> +#include <QtCore/QDir> +#include "smartpen.h" + +class SmartpenSyncer : public QThread +{ + Q_OBJECT +public: + explicit SmartpenSyncer(const Smartpen::Address &addr, QObject *parent = 0); + ~SmartpenSyncer(); + + Smartpen::Address penAddress() const; + +signals: + +public slots: + void abort(); + +private: + void run(); + bool syncPen(); + bool syncNotebook(const Smartpen::ChangeReport &change); + bool extractZip(QByteArray &zipData, QDir &dir); + +private: + Smartpen::Address _addr; + Smartpen *_pen; + bool _errored; + bool _aborted; + + QString _penSerial; + QString _penName; + QDir _penDataDir; +}; + +#endif // SMARTPENSYNCER_H diff --git a/stfgraphicsitem.cc b/stfgraphicsitem.cc new file mode 100644 index 0000000..865a029 --- /dev/null +++ b/stfgraphicsitem.cc @@ -0,0 +1,67 @@ +#include <QtGui/QPainterPath> + +#include "stfreader.h" +#include "stfgraphicsitem.h" + +class StfToGraphicsPathItems : public StfReader::StrokeHandler { + QGraphicsItem *parent; + QPainterPath path; + QRectF bound; + +public: + StfToGraphicsPathItems(QGraphicsItem* parent) + : parent(parent), path(), bound(0.0, 0.0, 1.0, 1.0) { + } + + ~StfToGraphicsPathItems() { + } + + bool startStroke(const QPoint& p, int, quint64) { + path = QPainterPath(QPointF(p)); + return true; + } + + bool strokePoint(const QPoint& p, int, quint64) { + path.lineTo(QPointF(p)); + return true; + } + + bool endStroke() { + bound |= path.boundingRect(); + new QGraphicsPathItem(path, parent); + /* Parent will take the child down with him when deleted. */ + return true; + } + + const QRectF& boundingRect() { + return bound; + } +}; + +StfGraphicsItem::StfGraphicsItem(QIODevice *dev, QGraphicsItem *parent) : + QGraphicsItem(parent) +{ + setFlags(ItemHasNoContents); + + StfToGraphicsPathItems h(this); + StfReader r; + r.setStrokeHandler(&h); + if (r.parse(dev)) { + bbox = h.boundingRect(); + } +} + +QRectF StfGraphicsItem::boundingRect() const +{ + return bbox; +} + +void StfGraphicsItem::paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) +{ + /* Intentionally empty; ItemHasNoContents; children will do the painting. */ +} + +int StfGraphicsItem::type() const +{ + return Type; +} diff --git a/stfgraphicsitem.h b/stfgraphicsitem.h new file mode 100644 index 0000000..ed62ede --- /dev/null +++ b/stfgraphicsitem.h @@ -0,0 +1,20 @@ +#ifndef STFGRAPHICSITEM_H +#define STFGRAPHICSITEM_H + +#include <QtGui/QGraphicsItem> + +class StfGraphicsItem : public QGraphicsItem +{ + QRectF bbox; + +public: + explicit StfGraphicsItem(QIODevice *stf, QGraphicsItem *parent = 0); + + enum { Type = UserType + 's' }; + + QRectF boundingRect() const; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + int type() const; +}; + +#endif // STFGRAPHICSITEM_H diff --git a/stfreader.cc b/stfreader.cc new file mode 100644 index 0000000..c5edff0 --- /dev/null +++ b/stfreader.cc @@ -0,0 +1,336 @@ +#include <QtCore/QDebug> +#include <QtCore/QFile> + +#include "stfreader.h" + +#define TABLE_v(i, ...) static qint8 tab_ ## i [] = { __VA_ARGS__ } +#define TABLE static CodeTable table[] = { +#define TABLE_i(i) {i, sizeof(tab_ ## i), tab_ ## i}, +#define END_TABLE }; static int table_size = sizeof(table) / sizeof(CodeTable); + +StfReader::StfReader() +: handler(0) +{ +} + +StfReader::~StfReader() +{ +} + +StfReader::StrokeHandler::~StrokeHandler() +{ +} + +bool StfReader::parseV1(BitReader& br) +{ + quint64 cur_time = 0; + + while (!br.atEnd()) { + syncV1(br); + quint8 header = br.readBits(8); + quint64 time; + QPoint p0, pa; + int f0; + + switch (header) { + case 0x80: /* End of file. */ + return true; + break; + case 0: + time = br.readBits(8); + break; + case 0x08: + time = br.readBits(16); + break; + case 0x10: + time = br.readBits(32); + break; + case 0x18: + time = br.readBits(64); + break; + default: + qWarning("Unknown header 0x%x", header); + break; + } + + /* Start of a stroke. */ + cur_time += time; + p0.setX(br.readBits(16)); + p0.setY(br.readBits(16)); + f0 = readForce(br); + + if (handler) { + bool res = handler->startStroke(p0, f0, cur_time); + if (!res) return false; + } + + while (!br.atEnd()) { + header = readHeader(br); + if (header == 0 || header == 1) { + time = readTime(br); + } else { + int header2 = readHeader2(br); + switch (header2) { + case 0: + time = br.readBits(8); + break; + case 1: + time = br.readBits(16); + break; + case 2: + time = br.readBits(32); + break; + default: + qWarning("Unknown stroke time header %d", header2); + } + } + + if (time == 0) { + if (handler) { + bool res = handler->endStroke(); + if (!res) return false; + } + break; + } + + bool do_delta; + QPoint delta; + qint8 deltaf; + + if (header > 0) { + bool have_len = br.readBits(1); + if (have_len) { + do_delta = false; + QPoint p1; + p1.setX(br.readBits(16)); + p1.setY(br.readBits(16)); + pa = p1 - p0; + } else { + do_delta = true; + delta.setX(br.readBits(8)); + delta.setY(br.readBits(8)); + } + } else { + do_delta = true; + delta.setX(readDeltaX(br)); + delta.setY(readDeltaY(br)); + } + + deltaf = readDeltaF(br); + + if (do_delta) { + pa = delta + (pa * static_cast<int>(time)) / 255; + } + + p0 += pa; + pa *= 256 / static_cast<int>(time); + f0 += deltaf; + + if (handler) { + bool res = handler->strokePoint(p0, f0, cur_time); + if (!res) return false; + } + } + + } + + return false; +} + +void StfReader::syncV1(BitReader &br) +{ + br.skipUntilNextByte(); + while (!br.atEnd() && (br.peekBits(8) & ~(0x80|0x10|0x08))) { + br.readBits(8); + } +} + +qint8 StfReader::decodeV1(BitReader& br, CodeTable* tab, int tab_size) +{ + int got_bits = 0; + int codeacc = 0; + int stream = 0; + + for (int i = 0; i < tab_size; i++) { + int get_bits = tab[i].bits - got_bits; + codeacc <<= get_bits; + codeacc += tab[i].size; + + stream <<= get_bits; + stream |= br.readBits(get_bits); + + got_bits += get_bits; + + int idx = stream - codeacc; + if (idx < 0) { + idx = tab[i].size + idx; + Q_ASSERT(idx >= 0 && idx < tab[i].size); + return tab[i].data[idx]; + } + } + + qWarning("Unknown code"); + return 0; +} + +qint8 StfReader::readForce(BitReader &br) +{ + TABLE_v(1, 0); + TABLE_v(6, 1,4,7,9,10,11,13,15,17,20,21,22,23,24,25,26,27,28,30); + TABLE_v(7, 2,3,5,6,8,12,14,16,18,19,29,31,32,33,34,35,36,49,52); + TABLE_v(8, 37,45,46,47,48,50,51,53,54); + TABLE_v(9, 38,39,40,41,44,55,56); + TABLE_v(10, 43,57,58); + TABLE_v(11, 42,59,60,61,62,63); + TABLE + TABLE_i(1) TABLE_i(6) TABLE_i(7) TABLE_i(8) TABLE_i(9) TABLE_i(10) TABLE_i(11) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readHeader(BitReader &br) +{ + TABLE_v(1, 0); + TABLE_v(2, 1, 2); + TABLE + TABLE_i(1) TABLE_i(2) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readHeader2(BitReader &br) +{ + TABLE_v(1, 0); + TABLE_v(2, 1, 3); + TABLE + TABLE_i(1) TABLE_i(2) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readHeader3(BitReader &br) +{ + TABLE_v(1, 0,1); + TABLE + TABLE_i(1) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readTime(BitReader &br) +{ + TABLE_v(1, 1); + TABLE_v(2, 2); + TABLE_v(4, 0,3,4); + TABLE_v(6, 5,6); + TABLE_v(7, 7,8); + TABLE_v(8, 9); + TABLE_v(9, 10,11); + TABLE_v(10, 12,13,14); + TABLE_v(11, 15); + TABLE_v(12, 16,17,18,19,21,22); + TABLE_v(13, 20,23,24,25,26,27); + TABLE_v(14, 28,29,30,31,32,35,36,37,40); + TABLE_v(15, 38,39,41,44,45,48,50,53,59,60,67,73,82,91,98,99,102); + TABLE_v(16, 33,34,42,43,46,47,49,51,52,54,55,56,57,58,61,62,63,64,65,66,68,69,70,71,72,74,75,76,77,78,79,80,81,83,84,85,86,87,88,89,90,92,93,94,95,96,97,100,101,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127); + TABLE + TABLE_i(1) TABLE_i(2) TABLE_i(4) TABLE_i(6) TABLE_i(7) TABLE_i(8) + TABLE_i(9) TABLE_i(10) TABLE_i(11) TABLE_i(12) TABLE_i(13) TABLE_i(14) + TABLE_i(15) TABLE_i(16) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readDeltaX(BitReader &br) +{ + TABLE_v(4, 3,4,5); + TABLE_v(5, 0,1,2,6,7,8,9,-8,-7,-6,-5,-4,-3,-2,-1); + TABLE_v(6, 10,11,12,13,-13,-12,-11,-10,-9); + TABLE_v(7, 14,15,16,17,18,19,-18,-17,-16,-15,-14); + TABLE_v(8, 20,21,22,23,24,26,-25,-24,-23,-22,-21,-20,-19); + TABLE_v(9, 25,27,28,29,30,31,32,33,34,36,38,-36,-34,-33,-32,-31,-30,-29,-28,-27,-26); + TABLE_v(10, 35,37,39,40,41,42,43,44,45,46,47,49,55,-57,-50,-47,-46,-44,-43,-42,-41,-40,-39,-38,-37,-35); + TABLE + TABLE_i(4) TABLE_i(5) TABLE_i(6) TABLE_i(7) TABLE_i(8) TABLE_i(9) TABLE_i(10) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readDeltaY(BitReader &br) +{ + TABLE_v(5, 0,1,2,3,4,5,6,7,8,-9,-8,-7,-6,-5,-4,-3,-2,-1); + TABLE_v(6, 9,10,11,12,13,-14,-13,-12,-11,-10); + TABLE_v(7, 14,15,16,17,18,19,20,-20,-19,-18,-17,-16,-15); + TABLE_v(8, 21,22,23,24,25,26,27,28,29,30,31,-27,-26,-25,-24,-23,-22,-21); + TABLE_v(9, 32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,49,-45,-44,-43,-40,-39,-38,-37,-36,-35,-34,-33,-32,-31,-30,-29,-28); + TABLE_v(10, 47,48,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,66,67,68,69,71,72,73,78,85,-75,-69,-64,-62,-61,-60,-59,-58,-57,-56,-55,-54,-53,-52,-51,-50,-49,-48,-47,-46,-42,-41); + + TABLE + TABLE_i(5) TABLE_i(6) TABLE_i(7) TABLE_i(8) TABLE_i(9) TABLE_i(10) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readDeltaF(BitReader &br) +{ + TABLE_v(1, 0); + TABLE_v(3, 1); + TABLE_v(4, -1); + TABLE_v(5, 2,-2); + TABLE_v(6, 3,-3); + TABLE_v(7, 4,52,53,-4); + TABLE_v(8, 5,6,49,50,51,54,55,-17,-16,-15,-14,-13,-12,-7,-6,-5); + TABLE_v(9, 7,8,9,10,11,12,13,30,31,36,37,38,39,40,41,46,48,56,57,-56,-55,-54,-53,-52,-51,-50,-49,-40,-38,-21,-20,-19,-18,-11,-10,-9,-8); + TABLE_v(10, 14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,32,33,34,35,42,43,44,45,47,58,-57,-48,-47,-46,-45,-44,-43,-42,-41,-39,-37,-36,-35,-34,-33,-32,-31,-29,-28,-27,-26,-25,-24,-23,-22); + TABLE_v(11, -59,-58,-30); + TABLE_v(12, 59,-64); + TABLE_v(13, 60,61,62,63,-63,-62,-61,-60); + TABLE + TABLE_i(1) TABLE_i(3) TABLE_i(4) TABLE_i(5) TABLE_i(6) TABLE_i(7) + TABLE_i(8) TABLE_i(9) TABLE_i(10) TABLE_i(11) TABLE_i(12) TABLE_i(13) + END_TABLE + + return decodeV1(br, table, table_size); +} + +bool StfReader::parse(QIODevice *device) +{ + char magic; + if (!device->getChar(&magic)) return false; + if (magic != 0x1) return false; + if (!device->getChar(&magic)) return false; + if (magic != 0x0) return false; + + QString version(device->read(14)); + if (version != "Anoto STF v1.0") { + return false; + } + + BitReader br(device); + + speed = br.readBits(16); + + return parseV1(br); +} + +bool StfReader::parse(const QString &filename) +{ + QFile f(filename); + if (!f.open(QIODevice::ReadOnly)) { + qWarning() << "Could not open" << f.fileName(); + } + return parse(&f); +} + +void StfReader::setStrokeHandler(StrokeHandler *newHandler) +{ + Q_ASSERT(newHandler); + handler = newHandler; +} diff --git a/stfreader.h b/stfreader.h new file mode 100644 index 0000000..06013e1 --- /dev/null +++ b/stfreader.h @@ -0,0 +1,51 @@ +#ifndef STFREADER_H +#define STFREADER_H + +#include <QtCore/QPoint> +#include "bitreader.h" + +class StfReader +{ +public: + class StrokeHandler + { + public: + virtual ~StrokeHandler(); + virtual bool startStroke(const QPoint& p, int force, quint64 time) = 0; + virtual bool strokePoint(const QPoint& p, int force, quint64 time) = 0; + virtual bool endStroke() = 0; + }; + +protected: + struct CodeTable { + int bits; + int size; + qint8 * data; + }; + + StrokeHandler *handler; + int speed; + + bool parseV1(BitReader& br); + void syncV1(BitReader& br); + qint8 decodeV1(BitReader& br, CodeTable* tab, int tab_size); + + qint8 readForce(BitReader& br); + qint8 readHeader(BitReader& br); + qint8 readHeader2(BitReader& br); + qint8 readHeader3(BitReader& br); + qint8 readTime(BitReader& br); + qint8 readDeltaX(BitReader& br); + qint8 readDeltaY(BitReader& br); + qint8 readDeltaF(BitReader& br); + +public: + StfReader(); + ~StfReader(); + + bool parse(QIODevice *device); + bool parse(const QString &filename); + void setStrokeHandler(StrokeHandler *newHandler); +}; + +#endif // STFREADER_H diff --git a/xmlutils.cc b/xmlutils.cc new file mode 100644 index 0000000..350949c --- /dev/null +++ b/xmlutils.cc @@ -0,0 +1,13 @@ +#include "xmlutils.h" + +bool advanceToFirstChildElement(QXmlStreamReader &r, const char *tag) +{ + while (r.readNextStartElement()) { + if (r.name() == tag) { + return true; + } else { + r.skipCurrentElement(); + } + } + return false; +} diff --git a/xmlutils.h b/xmlutils.h new file mode 100644 index 0000000..561b136 --- /dev/null +++ b/xmlutils.h @@ -0,0 +1,8 @@ +#ifndef XMLUTILS_H +#define XMLUTILS_H + +#include <QtCore/QXmlStreamReader> + +bool advanceToFirstChildElement(QXmlStreamReader &r, const char *tag); + +#endif // XMLUTILS_H |