aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--README.md37
-rw-r--r--afdnotebook.cc60
-rw-r--r--afdnotebook.h13
-rw-r--r--main.cc2
-rw-r--r--mainwindow.cc183
-rw-r--r--mainwindow.h19
-rw-r--r--mainwindow.ui70
-rw-r--r--notebookmodel.cc26
-rw-r--r--notebookmodel.h7
-rw-r--r--notebookview.cc30
-rw-r--r--notebookview.h9
-rw-r--r--paperreplay.cc68
-rw-r--r--paperreplay.h56
-rw-r--r--paperreplaymodel.cc21
-rw-r--r--paperreplaymodel.h5
-rw-r--r--scribiu.pro16
-rw-r--r--smartpen.cc77
-rw-r--r--smartpen.h23
-rw-r--r--smartpenmanager.cc23
-rw-r--r--smartpenmanager.h1
-rw-r--r--smartpensyncer.cc40
-rw-r--r--stfexporter.cc178
-rw-r--r--stfexporter.h39
-rw-r--r--stfgraphicsitem.cc11
-rw-r--r--stfreader.cc19
-rw-r--r--stfreader.h2
-rw-r--r--stfstrokeitem.cc4
28 files changed, 770 insertions, 273 deletions
diff --git a/.gitignore b/.gitignore
index bebf921..44a5db4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
*.pro.user
+*.pro.user.*
+.qmake.stash
+Makefile
/build
+/build-*
diff --git a/README.md b/README.md
index acf0c50..9510f99 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Screenshots
-![](http://depot.javispedro.com/livescribe/scribiu1.png)
+![Screenshot](http://depot.javispedro.com/livescribe/scribiu-tutorial.png)
# About
@@ -10,22 +10,35 @@ It also allows you to export individual pages as PNG files or voice memos as AAC
# Requirements
-Scribiu requires Qt 5, including the core, gui, widgets, and svg modules.
-It also requires phonon, libudev, openobex (>=1.7), libusb (>=1.0) and QuaZip (1.0).
-Most of these should be packaged by your distribution.
+Scribiu requires Qt 5, including the core, gui, widgets, svg and multimedia modules.
+It also requires libudev, openobex (>=1.7.2), libusb (>=1.0) and QuaZip (1.0).
+Most of these should be packaged by your GNU/Linux distribution.
+
+For example, on Ubuntu, these correspond with packages:
+
+`
+qtbase5-dev
+libqt5svg5-dev
+qtmultimedia5-dev
+libqt5multimedia5-plugins
+libudev-dev
+libopenobex2-dev
+libusb-1.0-0-dev
+libquazip5-dev
+`
This program should work with the Livescribe Pulse as well as the Livescribe Echo.
-It does not work with the Livescribe 3.
+It does not work with the Livescribe 3, Aegir, Symphony or any of the newer Bluetooth pens.
It may work with the Livescribe Sky if you get it to work with Echo Desktop, but I have never tried.
# Install
-`qmake`, `make`, and `make install` should be enough.
+`qmake`, `make`, and (`sudo`) `make install` should be enough.
-A udev rule will be installed in `/etc/udev/60-livescribe.rules` that will take care of the proper permissions when a Smartpen is detected.
+A udev rule will be installed in `/lib/udev/rules.d/60-livescribe.rules` that will take care of the proper permissions when a Smartpen is detected.
You may need to reboot or reload the udev daemon in order for these changes to work.
-![Scribiu icon](https://git.javispedro.com/cgit/scribiu.git/plain/icons/48x48/scribiu.png)
+![Scribiu icon](http://depot.javispedro.com/livescribe/scribiu-icon-48.png)
Look for the Scribiu icon inside the Office menu.
It should automatically start synchronizing after connecting a Smartpen.
@@ -38,9 +51,9 @@ These directories contain the raw notebook, stroke, and voice data as received f
Therefore, even if Scribiu fails to display a certain notebook, you may be able to use other Livescribe programs in order to view the synchronized data.
-We thank the authors of [libsmartpen](https://github.com/srwalter/libsmartpen)
-and [LibreScribe](https://github.com/dylanmtaylor/LibreScribe) because of their
-protocol reverse engineering efforts, specially regarding the STF data format,
+Many thanks to the authors of [libsmartpen](https://github.com/srwalter/libsmartpen)
+and [LibreScribe](https://github.com/dylanmtaylor/LibreScribe) for their efforts
+reverse engineering the protocols, specially regarding the STF data format,
which has saved me a lot of time.
# Tasks
@@ -50,4 +63,4 @@ which has saved me a lot of time.
generate a new GUID for the archived notebook.
* Paper replay stroke animation.
* Export entire notebook as PDF.
-* Deleting stuff from the pen: for now it's best to do this from LS Desktop.
+* Deleting stuff from the pen: for now it's best to do this from Echo Desktop.
diff --git a/afdnotebook.cc b/afdnotebook.cc
index 913986e..8b90619 100644
--- a/afdnotebook.cc
+++ b/afdnotebook.cc
@@ -170,7 +170,7 @@ QList<int> AfdNotebook::pagesWithStrokes(const QString &penSerial) const
{
if (_penData.contains(penSerial)) {
const PenData &data = _penData[penSerial];
- return data.strokes.uniqueKeys();
+ return data.strokes.keys();
} else {
return QList<int>();
}
@@ -182,36 +182,16 @@ QStringList AfdNotebook::strokeFiles(const QString &penSerial, int page) const
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;
+ auto it = data.strokes.find(page);
+ if (it != data.strokes.end()) {
+ const QList<StrokeData>& strokes = it.value();
+ l.reserve(strokes.size());
+ foreach (const StrokeData &stroke, strokes) {
+ l.append(_dir.filePath(stroke.file));
}
- ++it;
}
- return true;
+ return l;
}
QMap<QString, QString> AfdNotebook::parsePropertyList(QIODevice *dev)
@@ -320,12 +300,12 @@ bool AfdNotebook::parseGfx(const QString &file)
if (!r.atEnd()) {
QXmlStreamAttributes attrs = r.attributes();
QString imageSrc = attrs.value("src").toString();
- qDebug() << "image src" << imageSrc;
+ 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;
+ qDebug() << " gfx" << gfx.basename;
}
}
@@ -355,6 +335,8 @@ bool AfdNotebook::findPenData()
QDir penDir(pageDir.filePath(penName));
if (!penDir.exists()) continue;
+ PenData& penData = _penData[penName];
+
QStringList strokeFiles = penDir.entryList(QStringList("*.stf"), QDir::Files);
foreach (const QString &strokeFile, strokeFiles) {
qDebug() << " stroke data" << strokeFile;
@@ -368,8 +350,8 @@ bool AfdNotebook::findPenData()
stroke.file = penDir.filePath(strokeFile);
bool ok = true;
- 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) stroke.begin = strokeFile.mid(2, 8).toLongLong(&ok, 16) * 1000ULL;
+ if (ok) stroke.end = strokeFile.mid(13, 8).toLongLong(&ok, 16) * 1000ULL;
if (!ok) {
qWarning() << "Invalid stroke filename format" << strokeFile;
@@ -378,7 +360,19 @@ bool AfdNotebook::findPenData()
qDebug() << " from" << stroke.begin << "to" << stroke.end;
- _penData[penName].strokes.insert(pageNum, stroke);
+ penData.strokes[pageNum].append(stroke);
+ }
+
+ // Sort all the stroke files in each page by starting time
+ auto it = penData.strokes.begin();
+ while (it != penData.strokes.end()) {
+ QList<StrokeData> &strokes = *it;
+ if (!strokes.isEmpty()) {
+ std::sort(strokes.begin(), strokes.end(), StrokeData::CompareByBeginTime);
+ ++it;
+ } else {
+ it = penData.strokes.erase(it);
+ }
}
}
}
diff --git a/afdnotebook.h b/afdnotebook.h
index 49d06b0..9f8886d 100644
--- a/afdnotebook.h
+++ b/afdnotebook.h
@@ -56,8 +56,6 @@ public:
QStringList strokeFiles(const QString &penSerial, int page) const;
- bool readStrokes(const QString &penSerial, int page, StfReader::StrokeHandler *handler);
-
private:
struct Gfx {
QString basename;
@@ -70,12 +68,17 @@ private:
struct StrokeData {
QString file;
- QDateTime begin;
- QDateTime end;
+ quint64 begin;
+ quint64 end;
+
+ static bool CompareByBeginTime(const StrokeData &a, const StrokeData &b) {
+ return a.begin < b.begin;
+ }
};
struct PenData {
- QMultiMap<int, StrokeData> strokes;
+ // pageNum -> List of Strokes
+ QMap<int, QList<StrokeData>> strokes;
};
private:
diff --git a/main.cc b/main.cc
index f33835a..f3d6371 100644
--- a/main.cc
+++ b/main.cc
@@ -29,7 +29,7 @@ int main(int argc, char *argv[])
app.setOrganizationName("scribiu");
app.setOrganizationDomain("com.javispedro.scribiu");
app.setApplicationName("scribiu");
- app.setApplicationVersion("1.3");
+ app.setApplicationVersion("1.5");
QPixmapCache::setCacheLimit(100 * 1024);
diff --git a/mainwindow.cc b/mainwindow.cc
index bba19cc..cf1ef1a 100644
--- a/mainwindow.cc
+++ b/mainwindow.cc
@@ -25,13 +25,14 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
+#define PAPER_REPLAY_SLIDER_SCALE 100LL /* in msec */
+
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
_notebooks(new NotebookModel(this)),
_manager(new SmartpenManager(this)),
- _media(new Phonon::MediaObject(this)),
- _mediaOutput(new Phonon::AudioOutput(this)),
+ _player(new QMediaPlayer(this)),
_replay(new PaperReplay(this)),
_replayModel(new PaperReplayModel(_replay, this)),
_statusLabel(new QLabel)
@@ -42,14 +43,15 @@ MainWindow::MainWindow(QWidget *parent) :
ui->notebookTree->header()->setSectionResizeMode(1, QHeaderView::Fixed);
ui->notebookTree->header()->setSectionResizeMode(2, QHeaderView::Fixed);
ui->notebookTree->expandAll();
+ ui->replaySlider->setSingleStep(5000 / PAPER_REPLAY_SLIDER_SCALE);
+ ui->replaySlider->setPageStep(30000 / PAPER_REPLAY_SLIDER_SCALE);
ui->paperReplayView->setModel(_replayModel);
ui->paperReplayView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
ui->paperReplayView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed);
- Phonon::createPath(_media, _mediaOutput);
- _media->setTickInterval(500);
- ui->replaySlider->setMediaObject(_media);
ui->pauseButton->setVisible(false);
ui->statusBar->addWidget(_statusLabel, 1);
+ _player->setAudioRole(QAudio::VideoRole);
+
connect(_notebooks, SIGNAL(rowsInserted(QModelIndex,int,int)),
this, SLOT(handleNotebookRowsInserted(QModelIndex,int,int)));
connect(_manager, SIGNAL(pensBeingSynchronizedChanged()),
@@ -58,12 +60,14 @@ MainWindow::MainWindow(QWidget *parent) :
this, SLOT(handlePenSyncComplete(QString)));
connect(_manager, SIGNAL(syncFailed(QString)),
this, SLOT(handlePenSyncFailed(QString)));
- connect(_media, SIGNAL(stateChanged(Phonon::State,Phonon::State)),
- this, SLOT(handleMediaStateChange(Phonon::State)));
- connect(_media, SIGNAL(totalTimeChanged(qint64)),
- this, SLOT(handleMediaTotalTimeChanged(qint64)));
- connect(_media, SIGNAL(tick(qint64)),
- this, SLOT(handleMediaTick(qint64)));
+ connect(_player, SIGNAL(stateChanged(QMediaPlayer::State)),
+ this, SLOT(handlePlayerStateChanged(QMediaPlayer::State)));
+ connect(_player, SIGNAL(durationChanged(qint64)),
+ this, SLOT(handlePlayerDurationChanged(qint64)));
+ connect(_player, SIGNAL(positionChanged(qint64)),
+ this, SLOT(handlePlayerPositionChanged(qint64)));
+ connect(_player, SIGNAL(seekableChanged(bool)),
+ this, SLOT(handlePlayerSeekableChanged(bool)));
QSettings settings;
settings.beginGroup("mainwindow");
@@ -96,10 +100,12 @@ void MainWindow::openNotebook(const QString &pen, const QString &notebook)
_curPenName = pen;
_curNotebookName = notebook;
+ Smartpen::PenTime userTime = _notebooks->penUserTime(pen);
+
if (_curNotebookName == PAPER_REPLAY) {
QString replayDir = _notebooks->paperReplayDirectory(_curPenName);
- _replay->open(replayDir, 0);
+ _replay->open(replayDir, PAPER_REPLAY_GUID, userTime);
_replayModel->refresh();
ui->pane2Stack->setCurrentWidget(ui->paperReplayView);
@@ -108,6 +114,7 @@ void MainWindow::openNotebook(const QString &pen, const QString &notebook)
qDebug() << "Opening notebook" << _curPenName << _curNotebookName << nbDir;
+ ui->notebookView->setPenUserTime(userTime);
ui->notebookView->setPaperReplay(_notebooks->paperReplayDirectory(_curPenName));
ui->notebookView->setNotebook(nbDir);
ui->pane2Stack->setCurrentWidget(ui->notebookView);
@@ -117,7 +124,7 @@ void MainWindow::openNotebook(const QString &pen, const QString &notebook)
void MainWindow::exportCurrentPageAsPng(const QString &file)
{
qDebug() << "Exporting current page" << ui->notebookView->curPage() << "to" << file;
- QImage image = ui->notebookView->exportPage(ui->notebookView->curPage());
+ QImage image = ui->notebookView->exportPageAsImage(ui->notebookView->curPage());
if (!image.save(file, "PNG")) {
QMessageBox::warning(this, tr("Export page"),
tr("Could not export current page to '%s'").arg(file));
@@ -139,10 +146,34 @@ void MainWindow::exportCurrentPageAsSvg(const QString &file)
painter.end();
}
+void MainWindow::exportCurrentPageAsTXYP(const QString &file, bool relativeTime)
+{
+ QFile f(file);
+ if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
+ QMessageBox::warning(this, tr("Export page"),
+ tr("Could not export current page to '%s'").arg(file));
+ return;
+ }
+ ui->notebookView->exportPageAsTXYP(&f, ui->notebookView->curPage(), relativeTime);
+ f.close();
+}
+
+void MainWindow::exportCurrentPageAsInkML(const QString &file)
+{
+ QFile f(file);
+ if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
+ QMessageBox::warning(this, tr("Export page"),
+ tr("Could not export current page to '%s'").arg(file));
+ return;
+ }
+ ui->notebookView->exportPageAsInkML(&f, ui->notebookView->curPage());
+ f.close();
+}
+
void MainWindow::exportCurrentPaperReplayAsAac(const QString &file)
{
- QString src = _media->currentSource().fileName();
- if (src.isEmpty()) {
+ QString src = currentPlayerMediaPath();
+ if (src.isNull()) {
QMessageBox::warning(this, tr("Export audio"),
tr("No audio file is selected"));
return;
@@ -202,71 +233,80 @@ void MainWindow::handlePaperReplayRequested(const QString &file, qint64 time)
QString filePath = finfo.canonicalFilePath();
- if (_media->currentSource().fileName() != filePath) {
- _media->setCurrentSource(QUrl::fromLocalFile(filePath));
+ if (currentPlayerMediaPath() != filePath) {
+ qDebug() << "requesting media " << filePath;
+ _player->setMedia(QUrl::fromLocalFile(filePath));
}
- switch (_media->state()) {
- case Phonon::PlayingState:
- case Phonon::BufferingState:
- case Phonon::PausedState:
+ qDebug() << "requesting media seek to" << time << "/" << _player->duration();
+
+ if (_player->isSeekable()) {
+ // Media is loaded and ready to go
_pendingSeek = 0;
- _media->seek(time);
- break;
- default:
+ _player->setPosition(time);
+ } else {
+ // Otherwise delay the seek until after media starts playing
_pendingSeek = time;
- break;
}
- _media->play();
+ _player->play();
}
void MainWindow::handlePaperReplayPlay()
{
- _media->play();
+ _player->play();
}
void MainWindow::handlePaperReplayPause()
{
- _media->pause();
+ _player->pause();
+}
+
+void MainWindow::handlePaperReplaySliderChanged(int value)
+{
+ _player->setPosition(value * PAPER_REPLAY_SLIDER_SCALE);
}
-void MainWindow::handleMediaStateChange(Phonon::State state)
+void MainWindow::handlePlayerStateChanged(QMediaPlayer::State state)
{
switch (state) {
- case Phonon::PlayingState:
+ case QMediaPlayer::PlayingState:
ui->playButton->setVisible(false);
ui->pauseButton->setVisible(true);
- if (_pendingSeek) {
- _media->seek(_pendingSeek);
- _pendingSeek = 0;
- }
- ui->mediaPosLabel->setText(formatDuration(_media->currentTime()));
- ui->mediaLenLabel->setText("/ " + formatDuration(_media->totalTime()));
break;
- case Phonon::PausedState:
+ case QMediaPlayer::PausedState:
ui->playButton->setVisible(true);
ui->pauseButton->setVisible(false);
- ui->mediaPosLabel->setText(formatDuration(_media->currentTime()));
- ui->mediaLenLabel->setText("/ " + formatDuration(_media->totalTime()));
break;
default:
ui->playButton->setVisible(true);
ui->pauseButton->setVisible(false);
- ui->mediaPosLabel->setText(QString());
- ui->mediaLenLabel->setText(QString());
break;
}
}
-void MainWindow::handleMediaTotalTimeChanged(qint64 time)
+void MainWindow::handlePlayerDurationChanged(qint64 time)
{
ui->mediaLenLabel->setText("/ " + formatDuration(time));
+ ui->replaySlider->setMaximum(time / PAPER_REPLAY_SLIDER_SCALE);
}
-void MainWindow::handleMediaTick(qint64 time)
+void MainWindow::handlePlayerPositionChanged(qint64 time)
{
ui->mediaPosLabel->setText(formatDuration(time));
+ if (!ui->replaySlider->isSliderDown()) {
+ QSignalBlocker blocker(ui->replaySlider);
+ ui->replaySlider->setValue(time / PAPER_REPLAY_SLIDER_SCALE);
+ }
+}
+
+void MainWindow::handlePlayerSeekableChanged(bool seekable)
+{
+ if (seekable && _pendingSeek) {
+ qDebug() << "requesting (pending) media seek to" << _pendingSeek << "/" << _player->duration();
+ _player->setPosition(_pendingSeek);
+ _pendingSeek = 0;
+ }
}
void MainWindow::handlePensBeingSynchronizedChanged()
@@ -291,13 +331,21 @@ void MainWindow::handlePenSyncFailed(const QString &penName)
void MainWindow::handleExport()
{
+ QSettings settings;
+
if (_curNotebookName == PAPER_REPLAY) {
+ settings.beginGroup("export");
+ settings.beginGroup("audio");
+
+ QString dir = settings.value("dir").toString();
+
QStringList filters;
filters << tr("Current audio as AAC (*.aac)");
QString filter;
- QString fileName = QFileDialog::getSaveFileName(this, tr("Export page"), QString(),
+ QString fileName = QFileDialog::getSaveFileName(this, tr("Export page"), dir,
filters.join(";;"), &filter);
if (fileName.isEmpty()) return;
+
int filterIndex = filters.indexOf(filter);
switch (filterIndex) {
case 0:
@@ -306,17 +354,32 @@ void MainWindow::handleExport()
}
exportCurrentPaperReplayAsAac(fileName);
break;
+ default:
+ Q_UNREACHABLE();
}
+
+ QFileInfo file(fileName);
+ settings.setValue("dir", file.absolutePath());
} else if (!_curNotebookName.isEmpty()) {
+ settings.beginGroup("export");
+ settings.beginGroup("page");
+
+ QString dir = settings.value("dir").toString();
+
QStringList filters;
filters << tr("Current page as PNG image (*.png)")
<< tr("Current page as SVG image (*.svg)")
+ << tr("Current page as TXYP (*.txyp)")
+ << tr("Current page as InkML (*.inkml)")
<< tr("Current audio as AAC (*.aac)");
- QString filter;
- QString fileName = QFileDialog::getSaveFileName(this, tr("Export page"), QString(),
+ int filterIndex = settings.value("filetype").toInt();
+ QString filter = filters.value(filterIndex);
+
+ QString fileName = QFileDialog::getSaveFileName(this, tr("Export page"), dir,
filters.join(";;"), &filter);
if (fileName.isEmpty()) return;
- int filterIndex = filters.indexOf(filter);
+
+ filterIndex = filters.indexOf(filter);
switch (filterIndex) {
case 0:
if (!fileName.endsWith(".png", Qt::CaseInsensitive)) {
@@ -331,12 +394,30 @@ void MainWindow::handleExport()
exportCurrentPageAsSvg(fileName);
break;
case 2:
+ if (!fileName.endsWith(".txyp", Qt::CaseInsensitive)) {
+ fileName.append(".txyp");
+ }
+ exportCurrentPageAsTXYP(fileName, settings.value("txyp_relative_t", true).toBool());
+ break;
+ case 3:
+ if (!fileName.endsWith(".inkml", Qt::CaseInsensitive)) {
+ fileName.append(".inkml");
+ }
+ exportCurrentPageAsInkML(fileName);
+ break;
+ case 4:
if (!fileName.endsWith(".aac", Qt::CaseInsensitive)) {
fileName.append(".aac");
}
exportCurrentPaperReplayAsAac(fileName);
break;
+ default:
+ Q_UNREACHABLE();
}
+
+ QFileInfo file(fileName);
+ settings.setValue("dir", file.absolutePath());
+ settings.setValue("filetype", filterIndex);
} else {
QMessageBox::warning(this, tr("Export page"),
tr("Open a notebook or audio in order to export"));
@@ -359,7 +440,7 @@ void MainWindow::closeEvent(QCloseEvent *event)
settings.endGroup();
}
-QString MainWindow::formatDuration(qint64 time)
+QString MainWindow::formatDuration(qint64 time) const
{
int secs = time / 1000;
int mins = secs / 60;
@@ -374,3 +455,9 @@ QString MainWindow::formatDuration(qint64 time)
return QString("%2:%3").arg(mins).arg(secs, 2, 10, fill);
}
}
+
+QT_WARNING_DISABLE_DEPRECATED
+QString MainWindow::currentPlayerMediaPath() const
+{
+ return _player->media().canonicalUrl().toLocalFile();
+}
diff --git a/mainwindow.h b/mainwindow.h
index f3f6128..03479a2 100644
--- a/mainwindow.h
+++ b/mainwindow.h
@@ -21,8 +21,7 @@
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QLabel>
-#include <phonon/MediaObject>
-#include <phonon/AudioOutput>
+#include <QtMultimedia/QMediaPlayer>
#include "notebookmodel.h"
#include "paperreplaymodel.h"
#include "smartpenmanager.h"
@@ -45,6 +44,8 @@ public slots:
void exportCurrentPageAsPng(const QString &file);
void exportCurrentPageAsSvg(const QString &file);
+ void exportCurrentPageAsTXYP(const QString &file, bool relativeTime);
+ void exportCurrentPageAsInkML(const QString &file);
void exportCurrentPaperReplayAsAac(const QString &file);
private slots:
@@ -55,9 +56,11 @@ private slots:
void handlePaperReplayRequested(const QString &file, qint64 time);
void handlePaperReplayPlay();
void handlePaperReplayPause();
- void handleMediaStateChange(Phonon::State state);
- void handleMediaTotalTimeChanged(qint64 time);
- void handleMediaTick(qint64 time);
+ void handlePaperReplaySliderChanged(int value);
+ void handlePlayerStateChanged(QMediaPlayer::State state);
+ void handlePlayerDurationChanged(qint64 time);
+ void handlePlayerPositionChanged(qint64 time);
+ void handlePlayerSeekableChanged(bool seekable);
void handlePensBeingSynchronizedChanged();
void handlePenSyncComplete(const QString &penName);
void handlePenSyncFailed(const QString &penName);
@@ -68,15 +71,15 @@ protected:
void closeEvent(QCloseEvent *event);
private:
- QString formatDuration(qint64 time);
+ QString formatDuration(qint64 time) const;
+ QString currentPlayerMediaPath() const;
private:
Ui::MainWindow *ui;
NotebookModel *_notebooks;
SmartpenManager *_manager;
- Phonon::MediaObject *_media;
- Phonon::AudioOutput *_mediaOutput;
+ QMediaPlayer *_player;
qint64 _pendingSeek;
QString _curPenName;
diff --git a/mainwindow.ui b/mainwindow.ui
index a56ffba..52a8891 100644
--- a/mainwindow.ui
+++ b/mainwindow.ui
@@ -56,7 +56,7 @@
<bool>false</bool>
</attribute>
<attribute name="headerDefaultSectionSize">
- <number>29</number>
+ <number>41</number>
</attribute>
<attribute name="headerStretchLastSection">
<bool>false</bool>
@@ -147,7 +147,7 @@
</property>
</spacer>
</item>
- <item>
+ <item alignment="Qt::AlignVCenter">
<widget class="QLabel" name="zoomLabel">
<property name="text">
<string>Zoom:</string>
@@ -157,7 +157,7 @@
</property>
</widget>
</item>
- <item>
+ <item alignment="Qt::AlignVCenter">
<widget class="QSlider" name="zoomSlider">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@@ -259,10 +259,27 @@
</widget>
</item>
<item>
- <widget class="Phonon::SeekSlider" name="replaySlider"/>
+ <widget class="QSlider" name="replaySlider">
+ <property name="maximum">
+ <number>0</number>
+ </property>
+ <property name="tracking">
+ <bool>false</bool>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="tickInterval">
+ <number>1</number>
+ </property>
+ </widget>
</item>
<item>
- <widget class="QLabel" name="mediaPosLabel"/>
+ <widget class="QLabel" name="mediaPosLabel">
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
</item>
<item>
<widget class="QLabel" name="mediaLenLabel">
@@ -285,7 +302,7 @@
<x>0</x>
<y>0</y>
<width>718</width>
- <height>27</height>
+ <height>23</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@@ -346,12 +363,6 @@
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
- <class>Phonon::SeekSlider</class>
- <extends>QWidget</extends>
- <header location="global">phonon/seekslider.h</header>
- <container>1</container>
- </customwidget>
- <customwidget>
<class>NotebookView</class>
<extends>QGraphicsView</extends>
<header>notebookview.h</header>
@@ -406,7 +417,7 @@
<slot>handlePaperReplayPlay()</slot>
<hints>
<hint type="sourcelabel">
- <x>357</x>
+ <x>331</x>
<y>356</y>
</hint>
<hint type="destinationlabel">
@@ -422,7 +433,7 @@
<slot>handlePaperReplayPause()</slot>
<hints>
<hint type="sourcelabel">
- <x>391</x>
+ <x>365</x>
<y>356</y>
</hint>
<hint type="destinationlabel">
@@ -502,8 +513,8 @@
<slot>prevPage()</slot>
<hints>
<hint type="sourcelabel">
- <x>347</x>
- <y>46</y>
+ <x>331</x>
+ <y>61</y>
</hint>
<hint type="destinationlabel">
<x>441</x>
@@ -518,8 +529,8 @@
<slot>nextPage()</slot>
<hints>
<hint type="sourcelabel">
- <x>436</x>
- <y>38</y>
+ <x>421</x>
+ <y>61</y>
</hint>
<hint type="destinationlabel">
<x>473</x>
@@ -566,8 +577,8 @@
<slot>handlePaperReplaySelected(QModelIndex)</slot>
<hints>
<hint type="sourcelabel">
- <x>367</x>
- <y>117</y>
+ <x>402</x>
+ <y>95</y>
</hint>
<hint type="destinationlabel">
<x>778</x>
@@ -586,7 +597,7 @@
<y>116</y>
</hint>
<hint type="destinationlabel">
- <x>613</x>
+ <x>715</x>
<y>62</y>
</hint>
</hints>
@@ -607,6 +618,22 @@
</hint>
</hints>
</connection>
+ <connection>
+ <sender>replaySlider</sender>
+ <signal>valueChanged(int)</signal>
+ <receiver>MainWindow</receiver>
+ <slot>handlePaperReplaySliderChanged(int)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>416</x>
+ <y>339</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>681</x>
+ <y>300</y>
+ </hint>
+ </hints>
+ </connection>
</connections>
<slots>
<slot>handleNotebookSelected(QModelIndex)</slot>
@@ -616,6 +643,7 @@
<slot>handlePaperReplayPlay()</slot>
<slot>handlePaperReplayPause()</slot>
<slot>handlePaperReplaySelected(QModelIndex)</slot>
+ <slot>handlePaperReplaySliderChanged(int)</slot>
<slot>handleExport()</slot>
<slot>handleAbout()</slot>
</slots>
diff --git a/notebookmodel.cc b/notebookmodel.cc
index 3898df7..9a51bf9 100644
--- a/notebookmodel.cc
+++ b/notebookmodel.cc
@@ -72,6 +72,21 @@ QString NotebookModel::penDirectory(const QString &name) const
return _dataDir.filePath(name);
}
+NotebookModel::PenTime NotebookModel::penUserTime(const QString &name) const
+{
+ QDir penDir = penDirectory(name);
+ QFile userTimeFile(penDir.filePath(PEN_USER_TIME_FILE));
+ if (userTimeFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ QString data = QString::fromUtf8(userTimeFile.readLine(32));
+ userTimeFile.close();
+ return data.toULongLong();
+ }
+
+ qWarning() << "Could not read last user time for pen" << name << "; shown dates are likely to be off";
+
+ return 0;
+}
+
QString NotebookModel::notebookDirectory(const QString &penName, const QString &nbName) const
{
return _dataDir.filePath(penName + "/" + nbName);
@@ -157,7 +172,7 @@ QVariant NotebookModel::data(const QModelIndex &index, int role) const
case Qt::TextAlignmentRole:
switch (index.column()) {
case 0:
- return Qt::AlignLeft;
+ return QVariant::fromValue<int>(Qt::AlignLeft | Qt::AlignVCenter);
case 1:
case 2:
return Qt::AlignCenter;
@@ -423,7 +438,7 @@ bool NotebookModel::isPenArchive(const QString &pen) const
bool NotebookModel::isPenLocked(const QString &pen) const
{
QDir dir = penDir(pen);
- if (dir.exists(".sync.lck")) {
+ if (dir.exists(PEN_SYNC_LOCK_FILE)) {
return true; // TODO check if stale
} else {
return false;
@@ -433,7 +448,7 @@ bool NotebookModel::isPenLocked(const QString &pen) const
bool NotebookModel::isNotebookLocked(const QString &pen, const QString &notebook) const
{
QDir dir = notebookDir(pen, notebook);
- if (dir.exists(".sync.lck")) {
+ if (dir.exists(PEN_SYNC_LOCK_FILE)) {
return true;
} else {
return false;
@@ -457,8 +472,9 @@ void NotebookModel::handleChangedDirectory(const QString &path)
qDebug() << "changed" << path;
if (path == _dataDir.absolutePath()) {
refresh();
- } else if (path.endsWith(".pen")) {
+ } else if (path.endsWith("." PEN_EXTENSION, Qt::CaseInsensitive)
+ || path.endsWith("." ARCHIVE_EXTENSION, Qt::CaseInsensitive)) {
QFileInfo finfo(path);
- refreshPen(finfo.baseName());
+ refreshPen(finfo.fileName());
}
}
diff --git a/notebookmodel.h b/notebookmodel.h
index 51148e5..3fda090 100644
--- a/notebookmodel.h
+++ b/notebookmodel.h
@@ -28,6 +28,10 @@
#define ARCHIVE_EXTENSION "archive"
#define AFD_NOTEBOOK_EXTENSION "afd"
+#define PEN_USER_TIME_FILE ".usertime"
+#define PEN_LAST_SYNC_FILE ".lastsync"
+#define PEN_SYNC_LOCK_FILE ".sync.lck"
+
class NotebookModel : public QAbstractItemModel
{
Q_OBJECT
@@ -35,6 +39,8 @@ class NotebookModel : public QAbstractItemModel
public:
explicit NotebookModel(QObject *parent = 0);
+ typedef quint64 PenTime;
+
enum Roles {
FileNameRole = Qt::UserRole
};
@@ -43,6 +49,7 @@ public:
static QString userDataDirectory();
QString penDirectory(const QString &name) const;
+ PenTime penUserTime(const QString &name) const;
QString notebookDirectory(const QString &penName, const QString &nbName) const;
QString notebookDirectory(const QModelIndex &index) const;
QString paperReplayDirectory(const QString &name) const;
diff --git a/notebookview.cc b/notebookview.cc
index 833f8e8..f8394c0 100644
--- a/notebookview.cc
+++ b/notebookview.cc
@@ -18,6 +18,7 @@
#include <QtCore/QDebug>
#include <QtGui/QResizeEvent>
+#include "stfexporter.h"
#include "notebookview.h"
#define VIEW_MARGIN 2
@@ -69,6 +70,16 @@ QString NotebookView::paperReplay() const
return _replayPath;
}
+void NotebookView::setPenUserTime(quint64 userTime)
+{
+ _penUserTime = userTime;
+}
+
+quint64 NotebookView::penUserTime() const
+{
+ return _penUserTime;
+}
+
QList<int> NotebookView::pageNumbers() const
{
return _pages.keys();
@@ -121,7 +132,7 @@ QRect NotebookView::getCurPageTrim() const
return _nb->getPageTrim(_curPage);
}
-QImage NotebookView::exportPage(int pageNum) const
+QImage NotebookView::exportPageAsImage(int pageNum) const
{
const QRect pageTrim = _nb->getPageTrim(pageNum);
QImage image(pageTrim.width() / 4, pageTrim.height() / 4, QImage::Format_RGB32);
@@ -139,6 +150,18 @@ void NotebookView::renderPage(QPainter *painter, int pageNum, const QRectF &targ
scene.render(painter, target, source, Qt::KeepAspectRatio);
}
+void NotebookView::exportPageAsTXYP(QIODevice *device, int pageNum, bool relativeTime)
+{
+ StfExporter writer(_nb);
+ writer.exportToTXYP(device, pageNum, relativeTime);
+}
+
+void NotebookView::exportPageAsInkML(QIODevice *device, int pageNum)
+{
+ StfExporter writer(_nb);
+ writer.exportToInkML(device, pageNum);
+}
+
void NotebookView::requestPaperReplay(const QString &file, qint64 time)
{
emit paperReplayRequested(file, time);
@@ -224,7 +247,7 @@ bool NotebookView::createPages()
if (pens.isEmpty()) return false;
// Failure to open paperreplay data is not fatal
- bool haveReplay = _replay->open(_replayPath, _nb->guid());
+ bool haveReplay = _replay->open(_replayPath, _nb->guid(), _penUserTime);
QList<int> pagesWithStrokes = _nb->pagesWithStrokes(pens.first());
Q_ASSERT(_pages.isEmpty());
@@ -277,7 +300,8 @@ void NotebookView::calculateScale()
void NotebookView::layoutPages()
{
- const int numRows = (_pages.size() + 1) / _numColumns;
+ const int numRows = _pages.size() / _numColumns
+ + (_pages.size() % _numColumns > 0 ? 1 : 0);
const QSizeF pageSpace( _maxPageSize.width() + PAGE_SEPARATION,
_maxPageSize.height() + PAGE_SEPARATION);
diff --git a/notebookview.h b/notebookview.h
index 720a1a2..7d62fab 100644
--- a/notebookview.h
+++ b/notebookview.h
@@ -43,6 +43,9 @@ public:
void setPaperReplay(const QString &path);
QString paperReplay() const;
+ void setPenUserTime(quint64 userTime);
+ quint64 penUserTime() const;
+
QList<int> pageNumbers() const;
int curPage() const;
@@ -53,9 +56,12 @@ public:
QSize getCurPageSize() const;
QRect getCurPageTrim() const;
- QImage exportPage(int pageNum) const;
+ QImage exportPageAsImage(int pageNum) const;
void renderPage(QPainter *painter, int pageNum, const QRectF &target = QRectF(), const QRectF &source = QRectF()) const;
+ void exportPageAsTXYP(QIODevice *device, int pageNum, bool relativeTime);
+ void exportPageAsInkML(QIODevice *device, int pageNum);
+
void requestPaperReplay(const QString &file, qint64 time);
signals:
@@ -86,6 +92,7 @@ private:
PaperReplay *_replay;
QString _nbPath;
QString _replayPath;
+ quint64 _penUserTime;
QMap<int, PageItem*> _pages;
QSizeF _maxPageSize;
int _numColumns;
diff --git a/paperreplay.cc b/paperreplay.cc
index bec39f1..9f0a7f6 100644
--- a/paperreplay.cc
+++ b/paperreplay.cc
@@ -20,6 +20,7 @@
#include <QtCore/QDataStream>
#include <QtCore/QDirIterator>
#include "smartpen.h"
+#include "afdpageaddress.h"
#include "paperreplay.h"
namespace
@@ -57,7 +58,7 @@ PaperReplay::Session::Session() : d()
{
}
-PaperReplay::Session::Session(quint64 id) : d(new SessionData)
+PaperReplay::Session::Session(SessionId id) : d(new SessionData)
{
d->id = id;
}
@@ -71,7 +72,7 @@ bool PaperReplay::Session::isValid() const
return d;
}
-quint64 PaperReplay::Session::id() const
+PaperReplay::SessionId PaperReplay::Session::id() const
{
return d ? d->id : 0;
}
@@ -86,17 +87,22 @@ QString PaperReplay::Session::fileName() const
return d->file;
}
-qint64 PaperReplay::Session::startTime() const
+PaperReplay::PenTime PaperReplay::Session::startTime() const
{
return d->start;
}
-qint64 PaperReplay::Session::endTime() const
+PaperReplay::PenTime PaperReplay::Session::endTime() const
{
return d->end;
}
-bool PaperReplay::Session::startTimeLess(const Session &a, const Session &b)
+PaperReplay::PenTime PaperReplay::Session::duration() const
+{
+ return d->end - d->start;
+}
+
+bool PaperReplay::Session::compareByStartTime(const Session &a, const Session &b)
{
return a.d->start < b.d->start;
}
@@ -105,16 +111,16 @@ PaperReplay::SessionList::SessionList()
{
}
-PaperReplay::SessionList::SessionList(const QMap<qint64, Session> &byTime)
+PaperReplay::SessionList::SessionList(const QMap<PenTime, Session> &byTime)
: _m(byTime)
{
}
-QList<PaperReplay::Session> PaperReplay::SessionList::sessionsDuringTime(qint64 time) const
+QList<PaperReplay::Session> PaperReplay::SessionList::sessionsDuringTime(PenTime time) const
{
QList<Session> sessions;
if (_m.isEmpty()) return sessions;
- QMap<qint64, Session>::const_iterator it = _m.lowerBound(time);
+ QMap<PenTime, Session>::const_iterator it = _m.lowerBound(time);
if (it == _m.end()) --it;
@@ -129,7 +135,7 @@ QList<PaperReplay::Session> PaperReplay::SessionList::sessionsDuringTime(qint64
return sessions;
}
-bool PaperReplay::open(const QString &path, quint64 notebookGuid)
+bool PaperReplay::open(const QString &path, quint64 notebookGuid, PenTime userTime)
{
QString penSerial = findPenSerial(path);
if (penSerial.isEmpty()) {
@@ -145,6 +151,8 @@ bool PaperReplay::open(const QString &path, quint64 notebookGuid)
return false;
}
+ _userTime = userTime;
+
QDirIterator iter(_dir.path(), QStringList("PRS-*"), QDir::Dirs | QDir::NoDotAndDotDot);
while (iter.hasNext()) {
bool ok;
@@ -185,6 +193,7 @@ void PaperReplay::close()
_byPageTime.clear();
_sessions.clear();
_dir.setPath(QString());
+ _userTime = 0;
}
QList<PaperReplay::Session> PaperReplay::sessions() const
@@ -192,12 +201,17 @@ QList<PaperReplay::Session> PaperReplay::sessions() const
return _sessions.values();
}
-PaperReplay::SessionList PaperReplay::sessions(quint64 pageAddress) const
+PaperReplay::SessionList PaperReplay::sessions(PageAddress pageAddress) const
{
return SessionList(_byPageTime[pageAddress]);
}
-bool PaperReplay::parseSessionInfo(SessionData *session, const QString &path)
+PaperReplay::PenTime PaperReplay::userTime() const
+{
+ return _userTime;
+}
+
+bool PaperReplay::parseSessionInfo(SessionData *session, const QString &path) const
{
QFile f(path);
if (f.open(QIODevice::ReadOnly)) {
@@ -207,7 +221,7 @@ bool PaperReplay::parseSessionInfo(SessionData *session, const QString &path)
}
}
-bool PaperReplay::parseSessionInfo(SessionData *session, QIODevice *dev)
+bool PaperReplay::parseSessionInfo(SessionData *session, QIODevice *dev) const
{
unsigned char magic[2];
if (dev->read(reinterpret_cast<char*>(magic), 2) != 2 ||
@@ -231,12 +245,12 @@ bool PaperReplay::parseSessionInfo(SessionData *session, QIODevice *dev)
}
}
-bool PaperReplay::parseSessionInfoV3(SessionData *session, QIODevice *dev)
+bool PaperReplay::parseSessionInfoV3(SessionData *session, QIODevice *dev) const
{
QDataStream s(dev);
if (s.skipRawData(5) != 5) return false;
- qint64 startTime, endTime, creationTime;
+ PenTime startTime, endTime, creationTime;
QString name;
s >> startTime >> endTime >> creationTime;
@@ -248,12 +262,14 @@ bool PaperReplay::parseSessionInfoV3(SessionData *session, QIODevice *dev)
session->start = startTime;
session->end = endTime;
- qDebug() << "Session:" << name << Smartpen::fromPenTime(session->start) << Smartpen::fromPenTime(session->end);
+ qDebug() << " paperreplay" << QString("PRS-%1").arg(session->id, 0, 16) << name
+ << Smartpen::fromPenTime(_userTime, session->start)
+ << Smartpen::fromPenTime(_userTime, session->end);
quint16 num_clips;
s >> num_clips;
- Q_ASSERT(num_clips == 1); // TODO: We do not yet know how to handle this scenario
+ Q_ASSERT(num_clips == 1); // TODO: We do not yet know how to handle this scenario (more than one audio file per session)
for (uint i = 0; i < num_clips; ++i) {
QString file;
@@ -262,14 +278,16 @@ bool PaperReplay::parseSessionInfoV3(SessionData *session, QIODevice *dev)
}
s >> startTime >> endTime;
- qDebug() << " Clip:" << file << Smartpen::fromPenTime(startTime) << Smartpen::fromPenTime(endTime);
+ qDebug() << " clip" << file
+ << Smartpen::fromPenTime(_userTime, startTime) << Smartpen::fromPenTime(_userTime, endTime);
session->file = file;
}
quint16 num_strokes;
s >> num_strokes;
- // TODO:
+ // We are not doing anything with the stroke data stored here,
+ // rather we match the paperreplay when we load the strokes from notebook files
for (uint i = 0; i < num_strokes; ++i) {
quint64 a, b, c;
quint32 d;
@@ -280,13 +298,13 @@ bool PaperReplay::parseSessionInfoV3(SessionData *session, QIODevice *dev)
return false;
}
s >> f >> g;
- qDebug() << " Stroke:" << a << b << c << d << nbGuid << f << g;
+ qDebug() << " stroke" << a << b << c << d << nbGuid << f << g;
}
return true;
}
-bool PaperReplay::parseSessionPages(SessionData *session, const QString &path)
+bool PaperReplay::parseSessionPages(SessionData *session, const QString &path) const
{
QFile f(path);
if (f.open(QIODevice::ReadOnly)) {
@@ -296,7 +314,7 @@ bool PaperReplay::parseSessionPages(SessionData *session, const QString &path)
}
}
-bool PaperReplay::parseSessionPages(SessionData *session, QIODevice *dev)
+bool PaperReplay::parseSessionPages(SessionData *session, QIODevice *dev) const
{
unsigned char magic[2];
if (dev->read(reinterpret_cast<char*>(magic), 2) != 2 ||
@@ -320,7 +338,7 @@ bool PaperReplay::parseSessionPages(SessionData *session, QIODevice *dev)
}
}
-bool PaperReplay::parseSessionPagesV1(SessionData *session, QIODevice *dev)
+bool PaperReplay::parseSessionPagesV1(SessionData *session, QIODevice *dev) const
{
QDataStream s(dev);
if (s.skipRawData(1) != 1) return false;
@@ -330,12 +348,12 @@ bool PaperReplay::parseSessionPagesV1(SessionData *session, QIODevice *dev)
session->pages.reserve(session->pages.size() + num_pages);
for (uint i = 0; i < num_pages; ++i) {
- quint64 address;
- qint64 time;
+ PageAddress address;
+ PenTime time;
s >> address >> time;
session->pages.append(address);
- qDebug() << " Page:" << address << time << Smartpen::fromPenTime(time);
+ qDebug() << " page" << AfdPageAddress(address).toString() << Smartpen::fromPenTime(_userTime, time);
}
return true;
diff --git a/paperreplay.h b/paperreplay.h
index 9e61af7..5427dee 100644
--- a/paperreplay.h
+++ b/paperreplay.h
@@ -24,18 +24,26 @@
#include <QtCore/QMap>
#include <QtCore/QVector>
-/** Name of the paper replay notebook */
+/** Name of the paper replay "fake" notebook.
+ * Contains all paper replay sessions not tied to another notebook. */
#define PAPER_REPLAY "Paper Replay"
+#define PAPER_REPLAY_GUID 0
class PaperReplay : public QObject
{
Q_OBJECT
+public:
+ typedef quint64 SessionId;
+ typedef quint64 PenTime;
+ typedef quint64 PageAddress;
+
+private:
struct SessionData : public QSharedData {
- quint64 id;
+ SessionId id;
QString name;
- qint64 start, end;
- QVector<quint64> pages;
+ PenTime start, end;
+ QVector<PageAddress> pages;
QString file;
};
@@ -49,21 +57,22 @@ public:
bool isValid() const;
- quint64 id() const;
+ SessionId id() const;
QString name() const;
- qint64 startTime() const;
- qint64 endTime() const;
+ PenTime startTime() const;
+ PenTime endTime() const;
+ PenTime duration() const;
QVector<quint64> pages() const;
QString fileName() const;
- static bool startTimeLess(const Session &a, const Session &b);
+ static bool compareByStartTime(const Session &a, const Session &b);
private:
- Session(quint64 id);
+ Session(SessionId id);
QSharedDataPointer<SessionData> d;
friend class PaperReplay;
@@ -74,35 +83,38 @@ public:
public:
SessionList();
- QList<Session> sessionsDuringTime(qint64 time) const;
+ QList<Session> sessionsDuringTime(PenTime time) const;
private:
- explicit SessionList(const QMap<qint64, Session>& byTime);
+ explicit SessionList(const QMap<PenTime, Session>& byTime);
- QMap<qint64, Session> _m;
+ QMap<PenTime, Session> _m;
friend class PaperReplay;
};
- bool open(const QString &path, quint64 notebookGuid = 0);
+ bool open(const QString &path, quint64 notebookGuid, PenTime userTime);
void close();
QList<Session> sessions() const;
- SessionList sessions(quint64 pageAddress) const;
+ SessionList sessions(PageAddress pageAddress) const;
+
+ PenTime userTime() const;
private:
- static bool parseSessionInfo(SessionData *session, const QString &path);
- static bool parseSessionInfo(SessionData *session, QIODevice *dev);
- static bool parseSessionInfoV3(SessionData *session, QIODevice *dev);
+ bool parseSessionInfo(SessionData *session, const QString &path) const;
+ bool parseSessionInfo(SessionData *session, QIODevice *dev) const;
+ bool parseSessionInfoV3(SessionData *session, QIODevice *dev) const;
- static bool parseSessionPages(SessionData *session, const QString &path);
- static bool parseSessionPages(SessionData *session, QIODevice *dev);
- static bool parseSessionPagesV1(SessionData *session, QIODevice *dev);
+ bool parseSessionPages(SessionData *session, const QString &path) const;
+ bool parseSessionPages(SessionData *session, QIODevice *dev) const;
+ bool parseSessionPagesV1(SessionData *session, QIODevice *dev) const;
private:
QDir _dir;
- QHash<quint64, Session> _sessions;
- QMap<quint64, QMap<qint64, Session> > _byPageTime;
+ QHash<PageAddress, Session> _sessions;
+ QMap<PageAddress, QMap<PenTime, Session> > _byPageTime;
+ PenTime _userTime;
};
#endif // PAPERREPLAY_H
diff --git a/paperreplaymodel.cc b/paperreplaymodel.cc
index 480968f..dcab5cf 100644
--- a/paperreplaymodel.cc
+++ b/paperreplaymodel.cc
@@ -98,34 +98,29 @@ void PaperReplayModel::refresh()
{
beginResetModel();
_sessions = _replay->sessions();
- std::sort(_sessions.begin(), _sessions.end(), PaperReplay::Session::startTimeLess);
+ std::sort(_sessions.begin(), _sessions.end(), PaperReplay::Session::compareByStartTime);
endResetModel();
}
-QString PaperReplayModel::getSessionName(const PaperReplay::Session &session)
+QString PaperReplayModel::getSessionName(const PaperReplay::Session &session) const
{
QString title = session.name();
if (title.isEmpty()) {
- QDateTime date = Smartpen::fromPenTime(session.startTime());
+ QDateTime date = Smartpen::fromPenTime(_replay->userTime(), session.startTime());
title = date.toString(Qt::DefaultLocaleLongDate);
}
return title;
}
-QString PaperReplayModel::getSessionDate(const PaperReplay::Session &session)
+QString PaperReplayModel::getSessionLength(const PaperReplay::Session &session) const
{
- QDateTime date = Smartpen::fromPenTime(session.startTime());
- return date.toString(Qt::DefaultLocaleShortDate);
-}
-
-QString PaperReplayModel::getSessionLength(const PaperReplay::Session &session)
-{
- int secs = Smartpen::fromPenTime(session.startTime()).secsTo(Smartpen::fromPenTime(session.endTime()));
- int mins = secs / 60;
+ quint64 msecs = session.duration();
+ uint secs = msecs / 1000;
+ uint mins = secs / 60;
secs %= 60;
- int hours = mins / 60;
+ uint hours = mins / 60;
mins %= 60;
const QChar fill('0');
diff --git a/paperreplaymodel.h b/paperreplaymodel.h
index dd59db3..e894d6c 100644
--- a/paperreplaymodel.h
+++ b/paperreplaymodel.h
@@ -41,9 +41,8 @@ public slots:
void refresh();
private:
- static QString getSessionName(const PaperReplay::Session &session);
- static QString getSessionDate(const PaperReplay::Session &session);
- static QString getSessionLength(const PaperReplay::Session &session);
+ QString getSessionName(const PaperReplay::Session &session) const;
+ QString getSessionLength(const PaperReplay::Session &session) const;
private:
PaperReplay *_replay;
diff --git a/scribiu.pro b/scribiu.pro
index 8735caa..0c68fa4 100644
--- a/scribiu.pro
+++ b/scribiu.pro
@@ -1,13 +1,21 @@
TARGET = scribiu
+VERSION = 1.5.3
TEMPLATE = app
-QT += core gui widgets svg
-QT += phonon4qt5
+QT += core gui widgets svg multimedia
CONFIG += c++11
CONFIG += link_pkgconfig
-PKGCONFIG += libudev libusb-1.0 openobex quazip1-qt5
+PKGCONFIG += libudev libusb-1.0 openobex
+
+packagesExist(quazip1-qt5) {
+ PKGCONFIG += quazip1-qt5
+} else {
+ # Some "distributions" forgot to distribute the pkg-config file...
+ LIBS += -lquazip5
+ INCLUDEPATH += /usr/include/quazip5
+}
SOURCES += main.cc \
mainwindow.cc \
@@ -15,6 +23,7 @@ SOURCES += main.cc \
notebookmodel.cc \
smartpensyncer.cc \
smartpen.cc bitreader.cc stfreader.cc \
+ stfexporter.cc \
xmlutils.cc \
notebookview.cc \
afdnotebook.cc \
@@ -30,6 +39,7 @@ HEADERS += mainwindow.h \
notebookmodel.h \
smartpensyncer.h \
smartpen.h bitreader.h stfreader.h \
+ stfexporter.h \
xmlutils.h \
notebookview.h \
afdnotebook.h \
diff --git a/smartpen.cc b/smartpen.cc
index ce26a31..12e2ac2 100644
--- a/smartpen.cc
+++ b/smartpen.cc
@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#include <QtCore/QDateTime>
+#include <QtCore/QDataStream>
#include <QtCore/QDebug>
#include <QtCore/QDeadlineTimer>
#include <QtCore/QThread>
@@ -26,7 +26,6 @@
#include "xmlutils.h"
#include "smartpen.h"
-#define PEN_EPOCH (1289335960000LL)
#define PEN_MTU OBEX_MAXIMUM_MTU
#define PEN_TIMEOUT_SECONDS 5
@@ -86,9 +85,9 @@ QByteArray Smartpen::getObject(const QString &name)
return result;
}
-QString Smartpen::getParameter(Parameters parameter)
+QByteArray Smartpen::getParameter(Parameter parameter)
{
- QString objectName = QString("ppdata?key=pp%1").arg(uint(parameter), 4, 16);
+ QString objectName = QString("ppdata?key=pp%1").arg(uint(parameter), 4, 16, QChar('0'));
QByteArray data = getObject(objectName);
QXmlStreamReader r(data);
@@ -97,21 +96,56 @@ QString Smartpen::getParameter(Parameters parameter)
if (!r.atEnd()) {
QXmlStreamAttributes attrs = r.attributes();
- return attrs.value("value").toString();
+ QString value = attrs.value("value").toString();
+ if (!value.isEmpty()) {
+ if (value.startsWith("0x")) {
+ return QByteArray::fromHex(value.mid(2).toLatin1());
+ } else {
+ qWarning() << "Unknown parameter return format: " << value;
+ }
+ }
}
- return QString();
+ return QByteArray();
}
-QString Smartpen::getPenName()
+Smartpen::PenId Smartpen::getPenId()
{
- QString name = getParameter(PenName);
- if (name.isEmpty()) {
- return name; // Empty string if unknown name
+ PenId id;
+ QByteArray value = getParameter(Parameter::Id);
+ if (value.isEmpty() || value.size() != 1 + sizeof(Smartpen::PenId)) {
+ qWarning() << "got invalid value for pen id: " << value.toHex();
+ return 0;
}
+ QDataStream ds(value);
+ ds.setByteOrder(QDataStream::BigEndian);
+ ds.skipRawData(1); // Unclear what first byte is
+ ds >> id;
+ return id;
+}
- QByteArray hex = QByteArray::fromHex(name.mid(2).toLatin1());
- return QString::fromUtf8(hex);
+QString Smartpen::getPenSerial()
+{
+ return toPenSerial(getPenId());
+}
+
+QString Smartpen::getPenName()
+{
+ return QString::fromUtf8(getParameter(Parameter::Name));
+}
+
+Smartpen::PenTime Smartpen::getPenTime(Parameter parameter)
+{
+ PenTime time;
+ QByteArray value = getParameter(parameter);
+ if (value.isEmpty() || value.size() != sizeof(Smartpen::PenTime)) {
+ qWarning() << "got invalid value for pen time: " << value.toHex();
+ return 0;
+ }
+ QDataStream ds(value);
+ ds.setByteOrder(QDataStream::LittleEndian);
+ ds >> time;
+ return time;
}
QVariantMap Smartpen::getPenInfo()
@@ -216,22 +250,11 @@ QByteArray Smartpen::getPaperReplay(PenTime from)
return getObject(QString("lspdata?name=com.livescribe.paperreplay.PaperReplay&start_time=%1&returnVersion=0.3&remoteCaller=WIN_LD_200").arg(from));
}
-qint64 Smartpen::toPenTime(const QDateTime &dt)
-{
- if (dt.isValid()) {
- return dt.toMSecsSinceEpoch() - PEN_EPOCH;
- } else {
- return 0;
- }
-}
-
-QDateTime Smartpen::fromPenTime(qint64 t)
+QDateTime Smartpen::fromPenTime(PenTime userTime, PenTime penTime)
{
- if (t) {
- return QDateTime::fromMSecsSinceEpoch(t + PEN_EPOCH).toLocalTime();
- } else {
- return QDateTime();
- }
+ QDateTime dt = QDateTime::fromMSecsSinceEpoch(penTime + userTime, Qt::UTC);
+ dt.setTimeSpec(Qt::LocalTime); // userTime is actually in LocalTime, so override tz conversion
+ return dt;
}
QString Smartpen::toPenSerial(quint64 id)
diff --git a/smartpen.h b/smartpen.h
index c75c494..e3dadad 100644
--- a/smartpen.h
+++ b/smartpen.h
@@ -27,7 +27,7 @@
#define SMARTPEN_DPI_X (677.3333)
#define SMARTPEN_DPI_Y (677.3333)
-// TODO: These values are mostly random.
+// TODO: These values are obtained by observation and may be wrong
#define SMARTPEN_BLEED_X 333.3
#define SMARTPEN_BLEED_Y 333.3
@@ -40,19 +40,29 @@ public:
~Smartpen();
typedef QPair<unsigned int, unsigned int> Address;
- typedef qint64 PenTime;
typedef quint64 PenId;
+ typedef quint64 PenTime;
bool isConnected() const;
- enum Parameters : quint16 {
- PenName = 0x8011
+ enum class Parameter : quint16 {
+ Id = 0x0000,
+ /// The offset between the RtcTime (see below) and the user's configured time. This value is fixed at setup time.
+ UserTime = 0x8003,
+ /// The current time as reported by the pen's rtc
+ RtcTime = 0x8004,
+ Type = 0x8006,
+ Name = 0x8011
};
QByteArray getObject(const QString& name);
- QString getParameter(Parameters parameter);
+ QByteArray getParameter(Parameter parameter);
+ PenId getPenId();
+ QString getPenSerial();
QString getPenName();
+ PenTime getPenTime(Parameter parameter);
+
QVariantMap getPenInfo();
struct ChangeReport {
@@ -67,8 +77,7 @@ public:
QByteArray getLspData(const QString &name, PenTime from = 0);
QByteArray getPaperReplay(PenTime from = 0);
- static PenTime toPenTime(const QDateTime &dt);
- static QDateTime fromPenTime(PenTime t);
+ static QDateTime fromPenTime(PenTime userTime, PenTime penTime);
static QString toPenSerial(PenId id);
static PenId toPenId(const QString &serial);
diff --git a/smartpenmanager.cc b/smartpenmanager.cc
index 1eeb685..d375fea 100644
--- a/smartpenmanager.cc
+++ b/smartpenmanager.cc
@@ -197,9 +197,8 @@ void SmartpenManager::handleTimerNextTry()
void SmartpenManager::processDeviceAdded(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);
+ Smartpen::Address addr = getDeviceAddress(dev);
+ if (!addr.first && !addr.second) return;
QString name = parseUdevEscapedString(udev_device_get_property_value(dev, "ID_MODEL_ENC"));
@@ -223,9 +222,8 @@ void SmartpenManager::processDeviceAdded(udev_device *dev)
void SmartpenManager::processDeviceRemoved(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);
+ Smartpen::Address addr = getDeviceAddress(dev);
+ if (!addr.first && !addr.second) return;
qDebug() << "Device removed with address:" << addr;
@@ -325,3 +323,16 @@ QString SmartpenManager::parseUdevEscapedString(const char *s)
}
return r;
}
+
+Smartpen::Address SmartpenManager::getDeviceAddress(udev_device *dev)
+{
+ const char *busnumS = udev_device_get_property_value(dev, "BUSNUM");
+ const char *devnumS = udev_device_get_property_value(dev, "DEVNUM");
+ if (!busnumS || !devnumS) {
+ qWarning() << "Cannot find busnum/devnum env var for udev device";
+ return Smartpen::Address();
+ }
+ uint busnum = atol(busnumS);
+ uint devnum = atol(devnumS);
+ return Smartpen::Address(busnum, devnum);
+}
diff --git a/smartpenmanager.h b/smartpenmanager.h
index a823709..6a5e39f 100644
--- a/smartpenmanager.h
+++ b/smartpenmanager.h
@@ -61,6 +61,7 @@ private:
void scheduleNextTry();
static QString parseUdevEscapedString(const char *s);
+ static Smartpen::Address getDeviceAddress(udev_device *dev);
private:
udev *_udev;
diff --git a/smartpensyncer.cc b/smartpensyncer.cc
index acc32ac..3556f77 100644
--- a/smartpensyncer.cc
+++ b/smartpensyncer.cc
@@ -20,13 +20,13 @@
#include <QtCore/QScopedArrayPointer>
#include <QtCore/QThread>
#include <QtCore/QDebug>
-#include <quazip5/quazipfile.h>
+#include <quazipfile.h>
#include <cstdio>
#include "paperreplay.h"
#include "notebookmodel.h"
#include "smartpensyncer.h"
-#define BUFFER_SIZE 16 * 1024
+#define BUFFER_SIZE 128 * 1024
namespace {
class LockFile
@@ -108,7 +108,7 @@ Smartpen::PenTime TimestampFile::get()
if (_f.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString data = QString::fromUtf8(_f.readLine(32));
_f.close();
- return data.toLongLong();
+ return data.toULongLong();
} else {
qWarning() << "Could not read timestamp file:" << _f.fileName();
}
@@ -181,6 +181,12 @@ void SmartpenSyncer::run()
_penName = _pen->getPenName();
qDebug() << "got pen name:" << _penName;
+
+ if (_penName.isEmpty()) {
+ _penName = _pen->getPenSerial();
+ qDebug() << "pen with no name, using pen serial instead:" << _penName;
+ }
+
emit gotPenName(_penName);
QVariantMap penInfo = _pen->getPenInfo();
@@ -219,10 +225,22 @@ bool SmartpenSyncer::syncPen()
return false;
}
- TimestampFile lastSyncFile(_penDataDir.filePath(".lastsync"));
+ // Get the current user time offset from the pen
+ // and store it so that we have it even when the pen is offline
+ TimestampFile userTimeFile(_penDataDir.filePath(PEN_USER_TIME_FILE));
+ Smartpen::PenTime userTime = _pen->getPenTime(Smartpen::Parameter::UserTime);
+ userTimeFile.set(userTime);
+ qDebug() << "pen time base:" << userTime << Smartpen::fromPenTime(userTime, 0);
+
+ Smartpen::PenTime penTime = _pen->getPenTime(Smartpen::Parameter::RtcTime);
+ qDebug() << "pen current time:" << penTime << Smartpen::fromPenTime(userTime, penTime);
+
+ // Read when is the last time we synchronized with this pen (in PenTime, not user time)
+ TimestampFile lastSyncFile(_penDataDir.filePath(PEN_LAST_SYNC_FILE));
Smartpen::PenTime lastSync = lastSyncFile.get();
if (lastSync != 0) lastSync += 1; // We want the changes _from_ the last sync
+ // Ask the pen for all the changes which happened since the last sync time
QList<Smartpen::ChangeReport> changes = _pen->getChangeList(lastSync);
Smartpen::PenTime changesEndTime = 0;
@@ -269,9 +287,9 @@ bool SmartpenSyncer::syncNotebook(Smartpen::PenTime lastSync, const Smartpen::Ch
}
}
- LockFile lock(notebookDir.filePath(".sync.lck"));
+ LockFile lock(notebookDir.filePath(PEN_SYNC_LOCK_FILE));
if (!lock.lock()) {
- qWarning() << "Notebook is already being synchronized; delete this file if it is not:" << notebookDir.absoluteFilePath(".sync.lck");
+ qWarning() << "Notebook is already being synchronized; delete this file if it is not:" << notebookDir.absoluteFilePath(PEN_SYNC_LOCK_FILE);
return false;
}
@@ -293,9 +311,9 @@ bool SmartpenSyncer::syncPaperReplay(Smartpen::PenTime lastSync, const Smartpen:
}
}
- LockFile lock(replayDir.filePath(".sync.lck"));
+ LockFile lock(replayDir.filePath(PEN_SYNC_LOCK_FILE));
if (!lock.lock()) {
- qWarning() << "Paper replay is already being synchronized; delete this file if it is not:" << replayDir.absoluteFilePath(".sync.lck");
+ qWarning() << "Paper replay is already being synchronized; delete this file if it is not:" << replayDir.absoluteFilePath(PEN_SYNC_LOCK_FILE);
return false;
}
@@ -319,7 +337,7 @@ bool SmartpenSyncer::extractZip(QByteArray &zipData, QDir &dir)
return false;
}
- QScopedArrayPointer<char> buffer(new char[BUFFER_SIZE]);
+ QByteArray buffer(BUFFER_SIZE, Qt::Uninitialized);
for (bool more=zip.goToFirstFile(); more; more=zip.goToNextFile()) {
QString zipName = zip.getCurrentFileName();
@@ -350,7 +368,7 @@ bool SmartpenSyncer::extractZip(QByteArray &zipData, QDir &dir)
}
while (!zipFile.atEnd()) {
- qint64 read = zipFile.read(buffer.data(), BUFFER_SIZE);
+ qint64 read = zipFile.read(buffer.data(), buffer.size());
if (read <= 0) {
qWarning() << "short read on:" << zipName;
zipFile.close();
@@ -368,8 +386,6 @@ bool SmartpenSyncer::extractZip(QByteArray &zipData, QDir &dir)
zipFile.close();
}
- buffer.reset();
-
if (zip.getZipError() == UNZ_OK) {
return true;
} else {
diff --git a/stfexporter.cc b/stfexporter.cc
new file mode 100644
index 0000000..1503ae4
--- /dev/null
+++ b/stfexporter.cc
@@ -0,0 +1,178 @@
+/*
+ * scribiu -- read notebooks and voice memos from Livescribe pens
+ * Copyright (C) 2021 Javier S. Pedro <javier@javispedro.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QtCore/QDebug>
+#include <QtCore/QTextStream>
+#include <QtCore/QXmlStreamWriter>
+
+#include "stfexporter.h"
+
+#define XMLNS_INK "http://www.w3.org/2003/InkML"
+
+class StfToTXYP : public StfReader::StrokeHandler {
+ QTextStream _out;
+ qint64 _startTime;
+ bool _relativeTime;
+
+public:
+ StfToTXYP(QIODevice *out, bool relativeTime)
+ : _out(out), _startTime(0), _relativeTime(relativeTime) {
+ _out << "T\tX\tY\tP\n";
+ }
+
+ bool startStroke(const QPoint& p, int force, qint64 time) {
+ if (_relativeTime && _startTime == 0) {
+ _startTime = time;
+ }
+ _out << (time - _startTime) << '\t' << p.x() << '\t' << p.y() << '\t' << force << '\n';
+ return true;
+ }
+
+ bool strokePoint(const QPoint& p, int force, qint64 time) {
+ _out << (time - _startTime) << '\t' << p.x() << '\t' << p.y() << '\t' << force << '\n';
+ return true;
+ }
+
+ bool endStroke(qint64 time) {
+ // Force == 0 is used to detect strokes
+ Q_UNUSED(time);
+ return true;
+ }
+};
+
+class StfToInkML : public StfReader::StrokeHandler {
+ QXmlStreamWriter *_out;
+ QPoint _lastP;
+ qint64 _startTime;
+
+public:
+ StfToInkML(QXmlStreamWriter *out)
+ : _out(out), _lastP(), _startTime(0) {
+ }
+
+ bool startStroke(const QPoint& p, int force, qint64 time) {
+ Q_UNUSED(force);
+ if (_startTime == 0) {
+ _startTime = time;
+ }
+
+ _out->writeStartElement(XMLNS_INK, "trace");
+ _out->writeAttribute("timeOffset", QString::number(time - _startTime));
+
+ _out->writeCharacters(QString("%1 %2").arg(p.x()).arg(p.y()));
+
+ _lastP = p;
+ return true;
+ }
+
+ bool strokePoint(const QPoint& p, int force, qint64 time) {
+ Q_UNUSED(force);
+ Q_UNUSED(time);
+ QPoint delta = p - _lastP;
+
+ _out->writeCharacters(QString(", %1 %2").arg(delta.x()).arg(delta.y()));
+
+ _lastP = p;
+ return true;
+ }
+
+ bool endStroke(qint64 time) {
+ Q_UNUSED(time);
+ _out->writeEndElement();
+ return true;
+ }
+};
+
+StfExporter::StfExporter(AfdNotebook *nb)
+ : _nb(nb)
+{
+}
+
+void StfExporter::exportToTXYP(QIODevice *out, int pageNum, bool relativeTime)
+{
+ QStringList pens = _nb->penSerials();
+ if (pens.isEmpty()) return;
+
+ StfToTXYP h(out, relativeTime);
+
+ exportPage(&h, pageNum);
+}
+
+void StfExporter::exportToInkML(QIODevice *out, int pageNum)
+{
+ QStringList pens = _nb->penSerials();
+ if (pens.isEmpty()) return;
+
+ QXmlStreamWriter writer(out);
+ writer.setAutoFormatting(true);
+ writer.writeStartDocument();
+ writer.writeDefaultNamespace(XMLNS_INK);
+ writer.writeStartElement(XMLNS_INK, "ink");
+
+#if 0 /* No need to write out inkSource element, since default trace format is OK for now */
+ writer.writeStartElement(XMLNS_INK, "inkSource");
+ writer.writeAttribute("manufacturer", "Livescribe");
+ writer.writeAttribute("description", "Dumped by Scribiu");
+ writer.writeStartElement(XMLNS_INK, "traceFormat");
+ writer.writeEmptyElement(XMLNS_INK, "channel");
+ writer.writeAttribute("name", "X");
+ writer.writeAttribute("type", "integer");
+ writer.writeEmptyElement(XMLNS_INK, "channel");
+ writer.writeAttribute("name", "Y");
+ writer.writeAttribute("type", "integer");
+ writer.writeEndElement();
+ writer.writeEmptyElement(XMLNS_INK, "sampleRate");
+ writer.writeAttribute("uniform", "true");
+ writer.writeAttribute("value", "75");
+ writer.writeEndElement();
+#endif
+
+ StfToInkML h(&writer);
+
+ exportPage(&h, pageNum);
+
+ writer.writeEndElement();
+ writer.writeEndDocument();
+}
+
+bool StfExporter::exportPage(StfReader::StrokeHandler *handler, int pageNum)
+{
+ QStringList pens = _nb->penSerials();
+ if (pens.isEmpty()) return true; // No pen wrote on this page
+
+ StfReader r;
+ r.setStrokeHandler(handler);
+
+ foreach (const QString &pen, pens) {
+ QStringList strokeFiles = _nb->strokeFiles(pen, pageNum);
+ foreach (const QString &strokeFile, strokeFiles) {
+ QFile in(strokeFile);
+ if (!in.open(QIODevice::ReadOnly)) {
+ qWarning() << "Could not open stroke file:" << strokeFile;
+ continue;
+ }
+
+ if (!r.parse(&in)) {
+ qWarning() << "Could not parse stroke file:" << strokeFile;
+ continue;
+ }
+ }
+ }
+
+ return true;
+}
diff --git a/stfexporter.h b/stfexporter.h
new file mode 100644
index 0000000..c67a75c
--- /dev/null
+++ b/stfexporter.h
@@ -0,0 +1,39 @@
+/*
+ * scribiu -- read notebooks and voice memos from Livescribe pens
+ * Copyright (C) 2021 Javier S. Pedro <javier@javispedro.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef STFEXPORTER_H
+#define STFEXPORTER_H
+
+#include "afdnotebook.h"
+
+class StfExporter
+{
+public:
+ explicit StfExporter(AfdNotebook *nb);
+
+ void exportToTXYP(QIODevice *out, int pageNum, bool relativeTime);
+ void exportToInkML(QIODevice *out, int pageNum);
+
+private:
+ bool exportPage(StfReader::StrokeHandler *handler, int pageNum);
+
+private:
+ AfdNotebook *_nb;
+};
+
+#endif // STFEXPORTER_H
diff --git a/stfgraphicsitem.cc b/stfgraphicsitem.cc
index 75c9dfa..7ba173f 100644
--- a/stfgraphicsitem.cc
+++ b/stfgraphicsitem.cc
@@ -28,7 +28,7 @@ class StfToGraphicsPathItems : public StfReader::StrokeHandler {
QGraphicsItem *parent;
PaperReplay::SessionList replays;
PaperReplay::Session replay;
- qint64 startTime, lastTime;
+ qint64 startTime;
QPainterPath path;
QRectF bound;
@@ -37,9 +37,6 @@ public:
: parent(parent), replays(replays), path(), bound(0.0, 0.0, 1.0, 1.0) {
}
- ~StfToGraphicsPathItems() {
- }
-
bool startStroke(const QPoint& p, int force, qint64 time) {
Q_UNUSED(force);
QList<PaperReplay::Session> sessions = replays.sessionsDuringTime(time);
@@ -49,7 +46,6 @@ public:
replay = PaperReplay::Session();
}
startTime = time;
- lastTime = time;
path = QPainterPath(QPointF(p));
return true;
}
@@ -57,14 +53,13 @@ public:
bool strokePoint(const QPoint& p, int force, qint64 time) {
Q_UNUSED(force);
Q_UNUSED(time);
- lastTime = time;
path.lineTo(QPointF(p));
return true;
}
- bool endStroke() {
+ bool endStroke(qint64 time) {
bound |= path.boundingRect();
- new StfStrokeItem(path, replay, startTime, lastTime, parent);
+ new StfStrokeItem(path, replay, startTime, time, parent);
/* Parent will take the child down with him when deleted. */
return true;
}
diff --git a/stfreader.cc b/stfreader.cc
index 09967cc..efe7412 100644
--- a/stfreader.cc
+++ b/stfreader.cc
@@ -45,12 +45,12 @@ StfReader::StrokeHandler::~StrokeHandler()
bool StfReader::parseV1(BitReader& br)
{
- qint64 cur_time = 0;
+ quint64 stroke_time = 0;
while (!br.atEnd()) {
syncV1(br);
quint8 header = br.readBits(8);
- qint64 time;
+ quint64 time;
QPoint p0, pa;
int f0;
@@ -76,16 +76,18 @@ bool StfReader::parseV1(BitReader& br)
}
/* Start of a stroke. */
- cur_time += time;
+ stroke_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);
+ bool res = handler->startStroke(p0, f0, stroke_time);
if (!res) return false;
}
+ quint64 point_time = 0;
+
while (!br.atEnd()) {
header = readHeader(br);
if (header == 0 || header == 1) {
@@ -110,7 +112,7 @@ bool StfReader::parseV1(BitReader& br)
if (time == 0) {
if (handler) {
- bool res = handler->endStroke();
+ bool res = handler->endStroke(stroke_time + ((point_time * 1000ULL) / speed));
if (!res) return false;
}
break;
@@ -142,19 +144,20 @@ bool StfReader::parseV1(BitReader& br)
deltaf = readDeltaF(br);
if (do_delta) {
- pa = delta + (pa * static_cast<int>(time)) / 255;
+ pa = delta + (pa * static_cast<int>(time)) / 256;
}
p0 += pa;
pa *= 256 / static_cast<int>(time);
f0 += deltaf;
+ point_time += time;
+
if (handler) {
- bool res = handler->strokePoint(p0, f0, cur_time);
+ bool res = handler->strokePoint(p0, f0, stroke_time + ((point_time * 1000ULL) / speed));
if (!res) return false;
}
}
-
}
return false;
diff --git a/stfreader.h b/stfreader.h
index ec7c306..45e7376 100644
--- a/stfreader.h
+++ b/stfreader.h
@@ -31,7 +31,7 @@ public:
virtual ~StrokeHandler();
virtual bool startStroke(const QPoint& p, int force, qint64 time) = 0;
virtual bool strokePoint(const QPoint& p, int force, qint64 time) = 0;
- virtual bool endStroke() = 0;
+ virtual bool endStroke(qint64 time) = 0;
};
protected:
diff --git a/stfstrokeitem.cc b/stfstrokeitem.cc
index 335e166..a9ca693 100644
--- a/stfstrokeitem.cc
+++ b/stfstrokeitem.cc
@@ -51,9 +51,11 @@ void StfStrokeItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
if (event->button() == Qt::LeftButton && hasPaperReplay()) {
QGraphicsView *view = scene()->views().first();
if (NotebookView *nbview = qobject_cast<NotebookView*>(view)) {
- qint64 time = _startTime - _session.startTime();
+ PaperReplay::PenTime time = _startTime - _session.startTime();
if (time < 10) time = 0;
+ qDebug() << "requesting paper replay at time" << time << "/" << _session.duration();
+
nbview->requestPaperReplay(_session.fileName(), time);
event->accept();
return;