aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--afdnotebook.cc399
-rw-r--r--afdnotebook.h119
-rw-r--r--bitreader.cc45
-rw-r--r--bitreader.h25
-rw-r--r--main.cc11
-rw-r--r--mainwindow.cc66
-rw-r--r--mainwindow.h37
-rw-r--r--mainwindow.ui354
-rw-r--r--notebookmodel.cc323
-rw-r--r--notebookmodel.h49
-rw-r--r--notebookview.cc194
-rw-r--r--notebookview.h59
-rw-r--r--pageitem.cc64
-rw-r--r--pageitem.h32
-rw-r--r--scribiu.pro35
-rw-r--r--smartpen.cc424
-rw-r--r--smartpen.h78
-rw-r--r--smartpenmanager.cc69
-rw-r--r--smartpenmanager.h42
-rw-r--r--smartpensyncer.cc214
-rw-r--r--smartpensyncer.h39
-rw-r--r--stfgraphicsitem.cc67
-rw-r--r--stfgraphicsitem.h20
-rw-r--r--stfreader.cc336
-rw-r--r--stfreader.h51
-rw-r--r--xmlutils.cc13
-rw-r--r--xmlutils.h8
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
diff --git a/main.cc b/main.cc
new file mode 100644
index 0000000..a080bdd
--- /dev/null
+++ b/main.cc
@@ -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 &notebook)
+{
+ 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 &notebook);
+
+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>&amp;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>&amp;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 &notebooks = _notebooks[penName];
+ const QString &notebookName = 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 &notebooks = _notebooks[penName];
+ const QString &notebookName = 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 &notebooks = _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 &notebooks = _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 &notebook) const
+{
+ return QDir(_dataDir.filePath("%1.pen/%2.afd").arg(pen, notebook));
+}
+
+QIcon NotebookModel::getNotebookIcon(const QString &pen, const QString &notebook) 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 &notebook) 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 &notebook) const;
+ QIcon getNotebookIcon(const QString &pen, const QString &notebook) const;
+ bool isNotebookLocked(const QString &pen, const QString &notebook) 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