aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--README.md37
-rw-r--r--afdnotebook.cc66
-rw-r--r--afdnotebook.h13
-rw-r--r--main.cc5
-rw-r--r--mainwindow.cc196
-rw-r--r--mainwindow.h19
-rw-r--r--mainwindow.ui328
-rw-r--r--notebookmodel.cc137
-rw-r--r--notebookmodel.h24
-rw-r--r--notebookview.cc89
-rw-r--r--notebookview.h12
-rw-r--r--pageitem.cc13
-rw-r--r--pageitem.h3
-rw-r--r--paperreplay.cc69
-rw-r--r--paperreplay.h55
-rw-r--r--paperreplaymodel.cc21
-rw-r--r--paperreplaymodel.h5
-rw-r--r--scribiu.pro22
-rw-r--r--smartpen.cc79
-rw-r--r--smartpen.h23
-rw-r--r--smartpenmanager.cc23
-rw-r--r--smartpenmanager.h1
-rw-r--r--smartpensyncer.cc42
-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.cc17
-rw-r--r--stfstrokeitem.h6
31 files changed, 1073 insertions, 485 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 bee62c2..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.
-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 eaff949..8b90619 100644
--- a/afdnotebook.cc
+++ b/afdnotebook.cc
@@ -121,6 +121,8 @@ QPixmap AfdNotebook::getPageBackground(int page)
const QString file = QString("userdata/lsac_data/%1.png").arg(p.gfx->basename);
QImage img;
+ qDebug() << "Loading page background " << file;
+
if (!img.load(_dir.filePath(file), "PNG")) {
qWarning() << "Could not load background file:" << _dir.absoluteFilePath(file);
return pix;
@@ -168,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>();
}
@@ -180,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)
@@ -318,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;
}
}
@@ -353,12 +335,14 @@ 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;
if (strokeFile.length() != 25) {
- qWarning() << "Invalid stroke filename format" << strokeFile << endl;
+ qWarning() << "Invalid stroke filename format" << strokeFile;
continue;
}
@@ -366,17 +350,29 @@ 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 << endl;
+ qWarning() << "Invalid stroke filename format" << strokeFile;
continue;
}
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 406a729..f3d6371 100644
--- a/main.cc
+++ b/main.cc
@@ -18,6 +18,7 @@
#include "mainwindow.h"
#include <QtWidgets/QApplication>
+#include <QtGui/QPixmapCache>
int main(int argc, char *argv[])
{
@@ -28,7 +29,9 @@ int main(int argc, char *argv[])
app.setOrganizationName("scribiu");
app.setOrganizationDomain("com.javispedro.scribiu");
app.setApplicationName("scribiu");
- app.setApplicationVersion("1.2");
+ app.setApplicationVersion("1.5");
+
+ QPixmapCache::setCacheLimit(100 * 1024);
MainWindow w;
w.show();
diff --git a/mainwindow.cc b/mainwindow.cc
index 5c27145..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,16 +43,15 @@ MainWindow::MainWindow(QWidget *parent) :
ui->notebookTree->header()->setSectionResizeMode(1, QHeaderView::Fixed);
ui->notebookTree->header()->setSectionResizeMode(2, QHeaderView::Fixed);
ui->notebookTree->expandAll();
- ui->notebookView->setVisible(false);
+ 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);
- ui->paperReplayView->setVisible(false);
- 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()),
@@ -60,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");
@@ -85,9 +87,8 @@ void MainWindow::closeNotebook()
_curPenName.clear();
_curNotebookName.clear();
_replay->close();
+ ui->pane2Stack->setCurrentWidget(ui->notebookView);
ui->notebookView->setNotebook(QString());
- ui->notebookView->setVisible(false);
- ui->paperReplayView->setVisible(false);
}
void MainWindow::openNotebook(const QString &pen, const QString &notebook)
@@ -99,28 +100,31 @@ 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->paperReplayView->setVisible(true);
+ ui->pane2Stack->setCurrentWidget(ui->paperReplayView);
} else {
QString nbDir = _notebooks->notebookDirectory(_curPenName, _curNotebookName);
qDebug() << "Opening notebook" << _curPenName << _curNotebookName << nbDir;
+ ui->notebookView->setPenUserTime(userTime);
ui->notebookView->setPaperReplay(_notebooks->paperReplayDirectory(_curPenName));
ui->notebookView->setNotebook(nbDir);
- ui->notebookView->setVisible(true);
+ ui->pane2Stack->setCurrentWidget(ui->notebookView);
}
}
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));
@@ -142,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;
@@ -173,8 +201,8 @@ void MainWindow::handleNotebookSelected(const QModelIndex &index)
// Get column 0, which corresponds to notebook name
QModelIndex nb = _notebooks->index(index.row(), 0, parent);
- openNotebook(_notebooks->data(parent, Qt::DisplayRole).toString(),
- _notebooks->data(nb, Qt::DisplayRole).toString());
+ openNotebook(_notebooks->data(parent, NotebookModel::FileNameRole).toString(),
+ _notebooks->data(nb, NotebookModel::FileNameRole).toString());
}
void MainWindow::handleNotebookRowsInserted(const QModelIndex &index, int start, int end)
@@ -205,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()
@@ -294,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:
@@ -309,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)) {
@@ -334,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"));
@@ -362,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;
@@ -377,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 c04ff30..52a8891 100644
--- a/mainwindow.ui
+++ b/mainwindow.ui
@@ -56,7 +56,7 @@
<bool>false</bool>
</attribute>
<attribute name="headerDefaultSectionSize">
- <number>28</number>
+ <number>41</number>
</attribute>
<attribute name="headerStretchLastSection">
<bool>false</bool>
@@ -129,7 +129,7 @@
<string>...</string>
</property>
<property name="icon">
- <iconset theme="document-save-as">
+ <iconset theme="document-send">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
@@ -147,7 +147,7 @@
</property>
</spacer>
</item>
- <item>
+ <item alignment="Qt::AlignVCenter">
<widget class="QLabel" name="zoomLabel">
<property name="text">
<string>Zoom:</string>
@@ -157,10 +157,10 @@
</property>
</widget>
</item>
- <item>
+ <item alignment="Qt::AlignVCenter">
<widget class="QSlider" name="zoomSlider">
<property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@@ -200,53 +200,36 @@
</layout>
</item>
<item>
- <layout class="QHBoxLayout" name="pane2Mid">
- <property name="spacing">
+ <widget class="QStackedWidget" name="pane2Stack">
+ <property name="currentIndex">
<number>0</number>
</property>
- <item>
- <widget class="NotebookView" name="notebookView">
- <property name="verticalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOn</enum>
- </property>
- <property name="horizontalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOn</enum>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QTableView" name="paperReplayView">
- <property name="selectionMode">
- <enum>QAbstractItemView::SingleSelection</enum>
- </property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
- </property>
- <property name="showGrid">
- <bool>false</bool>
- </property>
- <attribute name="horizontalHeaderHighlightSections">
- <bool>false</bool>
- </attribute>
- <attribute name="verticalHeaderVisible">
- <bool>false</bool>
- </attribute>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
+ <widget class="NotebookView" name="notebookView">
+ <property name="verticalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOn</enum>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAsNeeded</enum>
+ </property>
+ </widget>
+ <widget class="QTableView" name="paperReplayView">
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="showGrid">
+ <bool>false</bool>
+ </property>
+ <attribute name="horizontalHeaderHighlightSections">
+ <bool>false</bool>
+ </attribute>
+ <attribute name="verticalHeaderVisible">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </widget>
</item>
<item>
<layout class="QHBoxLayout" name="paperReplayTools">
@@ -276,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">
@@ -302,7 +302,7 @@
<x>0</x>
<y>0</y>
<width>718</width>
- <height>29</height>
+ <height>23</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@@ -340,12 +340,15 @@
</action>
<action name="actionExport">
<property name="icon">
- <iconset theme="document-save-as">
+ <iconset theme="document-send">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>&amp;Export...</string>
</property>
+ <property name="toolTip">
+ <string>Export</string>
+ </property>
</action>
<action name="actionAbout">
<property name="icon">
@@ -360,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>
@@ -388,12 +385,12 @@
<slot>handleNotebookSelected(QModelIndex)</slot>
<hints>
<hint type="sourcelabel">
- <x>159</x>
- <y>193</y>
+ <x>131</x>
+ <y>204</y>
</hint>
<hint type="destinationlabel">
- <x>358</x>
- <y>192</y>
+ <x>356</x>
+ <y>218</y>
</hint>
</hints>
</connection>
@@ -414,78 +411,78 @@
</hints>
</connection>
<connection>
- <sender>zoomSlider</sender>
- <signal>valueChanged(int)</signal>
- <receiver>notebookView</receiver>
- <slot>setZoom(int)</slot>
+ <sender>playButton</sender>
+ <signal>clicked()</signal>
+ <receiver>MainWindow</receiver>
+ <slot>handlePaperReplayPlay()</slot>
<hints>
<hint type="sourcelabel">
- <x>671</x>
- <y>42</y>
+ <x>331</x>
+ <y>356</y>
</hint>
<hint type="destinationlabel">
- <x>479</x>
- <y>272</y>
+ <x>332</x>
+ <y>267</y>
</hint>
</hints>
</connection>
<connection>
- <sender>notebookView</sender>
- <signal>curPageChanged()</signal>
+ <sender>pauseButton</sender>
+ <signal>clicked()</signal>
<receiver>MainWindow</receiver>
- <slot>handleCurPageChanged()</slot>
+ <slot>handlePaperReplayPause()</slot>
<hints>
<hint type="sourcelabel">
- <x>479</x>
- <y>272</y>
+ <x>365</x>
+ <y>356</y>
</hint>
<hint type="destinationlabel">
- <x>358</x>
- <y>192</y>
+ <x>373</x>
+ <y>294</y>
</hint>
</hints>
</connection>
<connection>
- <sender>prevButton</sender>
- <signal>clicked()</signal>
- <receiver>notebookView</receiver>
- <slot>prevPage()</slot>
+ <sender>actionQuit</sender>
+ <signal>triggered()</signal>
+ <receiver>MainWindow</receiver>
+ <slot>deleteLater()</slot>
<hints>
<hint type="sourcelabel">
- <x>273</x>
- <y>57</y>
+ <x>-1</x>
+ <y>-1</y>
</hint>
<hint type="destinationlabel">
- <x>479</x>
- <y>272</y>
+ <x>358</x>
+ <y>192</y>
</hint>
</hints>
</connection>
<connection>
- <sender>nextButton</sender>
- <signal>clicked()</signal>
- <receiver>notebookView</receiver>
- <slot>nextPage()</slot>
+ <sender>actionExport</sender>
+ <signal>triggered()</signal>
+ <receiver>MainWindow</receiver>
+ <slot>handleExport()</slot>
<hints>
<hint type="sourcelabel">
- <x>365</x>
- <y>57</y>
+ <x>-1</x>
+ <y>-1</y>
</hint>
<hint type="destinationlabel">
- <x>479</x>
- <y>272</y>
+ <x>358</x>
+ <y>192</y>
</hint>
</hints>
</connection>
<connection>
- <sender>notebookView</sender>
- <signal>paperReplayRequested(QString,qint64)</signal>
+ <sender>actionAbout</sender>
+ <signal>triggered()</signal>
<receiver>MainWindow</receiver>
- <slot>handlePaperReplayRequested(QString,qint64)</slot>
+ <slot>handleAbout()</slot>
<hints>
<hint type="sourcelabel">
- <x>479</x>
- <y>255</y>
+ <x>-1</x>
+ <y>-1</y>
</hint>
<hint type="destinationlabel">
<x>358</x>
@@ -494,126 +491,126 @@
</hints>
</connection>
<connection>
- <sender>playButton</sender>
- <signal>clicked()</signal>
- <receiver>MainWindow</receiver>
- <slot>handlePaperReplayPlay()</slot>
+ <sender>zoomSlider</sender>
+ <signal>valueChanged(int)</signal>
+ <receiver>notebookView</receiver>
+ <slot>setZoom(int)</slot>
<hints>
<hint type="sourcelabel">
- <x>273</x>
- <y>358</y>
+ <x>663</x>
+ <y>46</y>
</hint>
<hint type="destinationlabel">
- <x>358</x>
- <y>192</y>
+ <x>550</x>
+ <y>132</y>
</hint>
</hints>
</connection>
<connection>
- <sender>pauseButton</sender>
+ <sender>prevButton</sender>
<signal>clicked()</signal>
- <receiver>MainWindow</receiver>
- <slot>handlePaperReplayPause()</slot>
+ <receiver>notebookView</receiver>
+ <slot>prevPage()</slot>
<hints>
<hint type="sourcelabel">
- <x>309</x>
- <y>358</y>
+ <x>331</x>
+ <y>61</y>
</hint>
<hint type="destinationlabel">
- <x>358</x>
- <y>192</y>
+ <x>441</x>
+ <y>141</y>
</hint>
</hints>
</connection>
<connection>
- <sender>paperReplayView</sender>
- <signal>activated(QModelIndex)</signal>
- <receiver>MainWindow</receiver>
- <slot>handlePaperReplaySelected(QModelIndex)</slot>
+ <sender>nextButton</sender>
+ <signal>clicked()</signal>
+ <receiver>notebookView</receiver>
+ <slot>nextPage()</slot>
<hints>
<hint type="sourcelabel">
- <x>597</x>
- <y>193</y>
+ <x>421</x>
+ <y>61</y>
</hint>
<hint type="destinationlabel">
- <x>358</x>
- <y>192</y>
+ <x>473</x>
+ <y>87</y>
</hint>
</hints>
</connection>
<connection>
- <sender>paperReplayView</sender>
- <signal>doubleClicked(QModelIndex)</signal>
+ <sender>notebookView</sender>
+ <signal>curPageChanged()</signal>
<receiver>MainWindow</receiver>
- <slot>handlePaperReplaySelected(QModelIndex)</slot>
+ <slot>handleCurPageChanged()</slot>
<hints>
<hint type="sourcelabel">
- <x>597</x>
- <y>193</y>
+ <x>368</x>
+ <y>161</y>
</hint>
<hint type="destinationlabel">
- <x>358</x>
- <y>192</y>
+ <x>486</x>
+ <y>27</y>
</hint>
</hints>
</connection>
<connection>
- <sender>exportButton</sender>
- <signal>clicked()</signal>
+ <sender>notebookView</sender>
+ <signal>paperReplayRequested(QString,qint64)</signal>
<receiver>MainWindow</receiver>
- <slot>handleExport()</slot>
+ <slot>handlePaperReplayRequested(QString,qint64)</slot>
<hints>
<hint type="sourcelabel">
- <x>402</x>
- <y>42</y>
+ <x>349</x>
+ <y>86</y>
</hint>
<hint type="destinationlabel">
- <x>358</x>
- <y>192</y>
+ <x>387</x>
+ <y>165</y>
</hint>
</hints>
</connection>
<connection>
- <sender>actionQuit</sender>
- <signal>triggered()</signal>
+ <sender>paperReplayView</sender>
+ <signal>activated(QModelIndex)</signal>
<receiver>MainWindow</receiver>
- <slot>deleteLater()</slot>
+ <slot>handlePaperReplaySelected(QModelIndex)</slot>
<hints>
<hint type="sourcelabel">
- <x>-1</x>
- <y>-1</y>
+ <x>402</x>
+ <y>95</y>
</hint>
<hint type="destinationlabel">
- <x>358</x>
- <y>192</y>
+ <x>778</x>
+ <y>310</y>
</hint>
</hints>
</connection>
<connection>
- <sender>actionExport</sender>
- <signal>triggered()</signal>
- <receiver>MainWindow</receiver>
- <slot>handleExport()</slot>
+ <sender>notebookView</sender>
+ <signal>zoomChanged(int)</signal>
+ <receiver>zoomSlider</receiver>
+ <slot>setValue(int)</slot>
<hints>
<hint type="sourcelabel">
- <x>-1</x>
- <y>-1</y>
+ <x>576</x>
+ <y>116</y>
</hint>
<hint type="destinationlabel">
- <x>358</x>
- <y>192</y>
+ <x>715</x>
+ <y>62</y>
</hint>
</hints>
</connection>
<connection>
- <sender>actionAbout</sender>
- <signal>triggered()</signal>
+ <sender>exportButton</sender>
+ <signal>clicked()</signal>
<receiver>MainWindow</receiver>
- <slot>handleAbout()</slot>
+ <slot>handleExport()</slot>
<hints>
<hint type="sourcelabel">
- <x>-1</x>
- <y>-1</y>
+ <x>457</x>
+ <y>46</y>
</hint>
<hint type="destinationlabel">
<x>358</x>
@@ -622,18 +619,18 @@
</hints>
</connection>
<connection>
- <sender>notebookView</sender>
- <signal>zoomChanged(int)</signal>
- <receiver>zoomSlider</receiver>
- <slot>setValue(int)</slot>
+ <sender>replaySlider</sender>
+ <signal>valueChanged(int)</signal>
+ <receiver>MainWindow</receiver>
+ <slot>handlePaperReplaySliderChanged(int)</slot>
<hints>
<hint type="sourcelabel">
- <x>361</x>
- <y>193</y>
+ <x>416</x>
+ <y>339</y>
</hint>
<hint type="destinationlabel">
- <x>671</x>
- <y>42</y>
+ <x>681</x>
+ <y>300</y>
</hint>
</hints>
</connection>
@@ -646,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 b9c897e..9a51bf9 100644
--- a/notebookmodel.cc
+++ b/notebookmodel.cc
@@ -29,6 +29,9 @@
#define NUM_COLUMNS 3
#define PEN_INDEX_ID 0xFFFFFFFFU
+#define PEN_EXTENSION "pen"
+#define ARCHIVE_EXTENSION "archive"
+
NotebookModel::NotebookModel(QObject *parent) :
QAbstractItemModel(parent),
_dataDir(userDataDirectory()),
@@ -66,12 +69,27 @@ QString NotebookModel::userDataDirectory()
QString NotebookModel::penDirectory(const QString &name) const
{
- return _dataDir.filePath(name + ".pen");
+ 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 + ".pen" + "/" + nbName + ".afd");
+ return _dataDir.filePath(penName + "/" + nbName);
}
QString NotebookModel::notebookDirectory(const QModelIndex &index) const
@@ -89,7 +107,7 @@ QString NotebookModel::notebookDirectory(const QModelIndex &index) const
QString NotebookModel::paperReplayDirectory(const QString &name) const
{
- return _dataDir.filePath(name + ".pen" + "/"+ PAPER_REPLAY);
+ return _dataDir.filePath(name + "/" + PAPER_REPLAY);
}
QVariant NotebookModel::data(const QModelIndex &index, int role) const
@@ -99,22 +117,28 @@ QVariant NotebookModel::data(const QModelIndex &index, int role) const
if (id == PEN_INDEX_ID) {
int penIndex = index.row();
if (penIndex < 0 || penIndex >= _pens.size()) return QVariant();
+ const QString &penName = _pens[penIndex];
switch (role) {
case Qt::DisplayRole:
switch (index.column()) {
case 0:
- return _pens[penIndex];
+ return penDisplayName(penName);
}
break;
case Qt::DecorationRole:
switch (index.column()) {
+ case 0:
+ return penIcon(penName);
case 2:
- if (isPenLocked(_pens[penIndex])) {
+ if (isPenLocked(penName)) {
return QApplication::style()->standardIcon(QStyle::SP_BrowserReload);
}
break;
}
break;
+ case FileNameRole:
+ return penName;
+ break;
}
} else {
const QString &penName = _pens[id];
@@ -124,7 +148,7 @@ QVariant NotebookModel::data(const QModelIndex &index, int role) const
case Qt::DisplayRole:
switch (index.column()) {
case 0:
- return notebooks.at(index.row());
+ return notebookDisplayName(penName, notebookName);
case 1:
if (notebookName != PAPER_REPLAY) {
return estimatePagesOfNotebook(penName, notebookName);
@@ -135,18 +159,12 @@ QVariant NotebookModel::data(const QModelIndex &index, int role) const
case Qt::DecorationRole:
switch (index.column()) {
case 0:
- return getNotebookIcon(penName, notebookName);
+ return notebookIcon(penName, notebookName);
case 1:
return QVariant();
case 2:
- if (notebookName != PAPER_REPLAY) {
- if (isNotebookLocked(penName, notebookName)) {
- return QApplication::style()->standardIcon(QStyle::SP_BrowserReload);
- }
- } else {
- if (isPaperReplayLocked(penName)) {
- return QApplication::style()->standardIcon(QStyle::SP_BrowserReload);
- }
+ if (isNotebookLocked(penName, notebookName)) {
+ return QApplication::style()->standardIcon(QStyle::SP_BrowserReload);
}
break;
}
@@ -154,12 +172,14 @@ 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;
}
break;
+ case FileNameRole:
+ return notebookName;
}
}
return QVariant();
@@ -239,10 +259,7 @@ int NotebookModel::columnCount(const QModelIndex &parent) const
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); // Remove .pen extension
- }
+ QStringList pens = _dataDir.entryList(QStringList{"*." PEN_EXTENSION, "*." ARCHIVE_EXTENSION}, QDir::Dirs, QDir::Name);
// Insert/remove new/deleted pens
int i = 0, j = 0;
@@ -291,13 +308,11 @@ void NotebookModel::refreshPen(const QString &name)
return;
}
+ _iconCache.clear();
_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);
- }
+ QStringList diskNotebooks = penDir.entryList(QStringList("*." AFD_NOTEBOOK_EXTENSION), QDir::Dirs, QDir::Name);
if (penDir.exists(PAPER_REPLAY)) diskNotebooks.append(PAPER_REPLAY);
QModelIndex penIndex = index(indexOfPen(name), 0, QModelIndex());
@@ -357,33 +372,73 @@ QDir NotebookModel::notebookDir(const QString &pen, const QString &notebook) con
return QDir(notebookDirectory(pen, notebook));
}
-QIcon NotebookModel::getNotebookIcon(const QString &pen, const QString &notebook) const
+QString NotebookModel::penDisplayName(const QString &pen) 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";
+ if (pen.endsWith("." PEN_EXTENSION, Qt::CaseInsensitive)) {
+ return pen.chopped(strlen("." PEN_EXTENSION));
+ } else if (pen.endsWith("." ARCHIVE_EXTENSION, Qt::CaseInsensitive)) {
+ return pen.chopped(strlen("." ARCHIVE_EXTENSION));
+ } else {
+ return pen;
+ }
+}
+
+QString NotebookModel::notebookDisplayName(const QString &pen, const QString &notebook) const
+{
+ Q_UNUSED(pen);
+ if (notebook.endsWith("." AFD_NOTEBOOK_EXTENSION, Qt::CaseInsensitive)) {
+ return notebook.chopped(strlen("." AFD_NOTEBOOK_EXTENSION));
+ } else if (notebook == PAPER_REPLAY) {
+ return tr("Voice recordings");
+ } else {
+ return notebook;
+ }
+}
+
+QIcon NotebookModel::penIcon(const QString &pen) const
+{
+ if (isPenArchive(pen)) {
+ return QApplication::style()->standardIcon(QStyle::SP_DirOpenIcon);
+ } else {
+ return QIcon::fromTheme("scribiu");
+ }
+}
+
+QIcon NotebookModel::notebookIcon(const QString &pen, const QString &notebook) const
+{
+ if (notebook == PAPER_REPLAY) {
+ return QApplication::style()->standardIcon(QStyle::SP_MediaVolume);
}
- QIcon icon;
QDir dir = notebookDir(pen, notebook);
- if (dir.exists()) {
+
+ QIcon icon = _iconCache.value(dir.path());
+ if (icon.isNull() && dir.exists()) {
+ static const QStringList candidates{"userdata/icon/Notebook.png",
+ "userdata/icon/active_64x64.png",
+ "userdata/icon/active_32x32.png",
+ "userdata/icon/active_16x16.png"};
foreach (const QString &candidate, candidates) {
if (dir.exists(candidate)) {
icon.addFile(dir.filePath(candidate));
}
}
+
+ _iconCache.insert(dir.path(), icon);
}
return icon;
}
+bool NotebookModel::isPenArchive(const QString &pen) const
+{
+ return pen.endsWith("." ARCHIVE_EXTENSION);
+}
+
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;
@@ -393,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;
@@ -402,12 +457,7 @@ bool NotebookModel::isNotebookLocked(const QString &pen, const QString &notebook
bool NotebookModel::isPaperReplayLocked(const QString &pen) const
{
- QDir dir(paperReplayDirectory(pen));
- if (dir.exists(".sync.lck")) {
- return true;
- } else {
- return false;
- }
+ return isNotebookLocked(pen, PAPER_REPLAY);
}
int NotebookModel::estimatePagesOfNotebook(const QString &pen, const QString &notebook) const
@@ -422,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 1d69e14..3fda090 100644
--- a/notebookmodel.h
+++ b/notebookmodel.h
@@ -22,6 +22,15 @@
#include <QtCore/QAbstractItemModel>
#include <QtCore/QDir>
#include <QtCore/QFileSystemWatcher>
+#include <QtGui/QIcon>
+
+#define PEN_EXTENSION "pen"
+#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
{
@@ -30,10 +39,17 @@ class NotebookModel : public QAbstractItemModel
public:
explicit NotebookModel(QObject *parent = 0);
+ typedef quint64 PenTime;
+
+ enum Roles {
+ FileNameRole = Qt::UserRole
+ };
+
static QString defaultDataDirectory();
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;
@@ -57,7 +73,11 @@ private:
int indexOfPen(const QString &name);
QDir penDir(const QString &pen) const;
QDir notebookDir(const QString &pen, const QString &notebook) const;
- QIcon getNotebookIcon(const QString &pen, const QString &notebook) const;
+ QString penDisplayName(const QString &pen) const;
+ QString notebookDisplayName(const QString &pen, const QString &notebook) const;
+ QIcon penIcon(const QString &pen) const;
+ QIcon notebookIcon(const QString &pen, const QString &notebook) const;
+ bool isPenArchive(const QString &pen) const;
bool isPenLocked(const QString &pen) const;
bool isNotebookLocked(const QString &pen, const QString &notebook) const;
bool isPaperReplayLocked(const QString &pen) const;
@@ -72,6 +92,8 @@ private:
QFileSystemWatcher _watcher;
QStringList _pens;
QHash<QString, QStringList> _notebooks;
+
+ mutable QHash<QString, QIcon> _iconCache;
};
#endif // NOTEBOOKMODEL_H
diff --git a/notebookview.cc b/notebookview.cc
index 1f97ead..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
@@ -25,7 +26,7 @@
NotebookView::NotebookView(QWidget *parent) :
QGraphicsView(parent), _nb(new AfdNotebook(this)), _replay(new PaperReplay(this)),
- _zoom(100), _curPage(0)
+ _numColumns(1), _zoom(100), _curPage(0)
{
setScene(new QGraphicsScene(this));
setTransformationAnchor(AnchorUnderMouse);
@@ -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();
@@ -102,7 +113,12 @@ void NotebookView::setZoom(int zoom)
{
if (zoom != _zoom) {
_zoom = zoom;
+ int oldNumColumns = _numColumns;
calculateScale();
+ if (_numColumns != oldNumColumns) {
+ layoutPages();
+ }
+ emit zoomChanged(_zoom);
}
}
@@ -116,9 +132,9 @@ QRect NotebookView::getCurPageTrim() const
return _nb->getPageTrim(_curPage);
}
-QImage NotebookView::exportPage(int pageNum) const
+QImage NotebookView::exportPageAsImage(int pageNum) const
{
- const QRect pageTrim = getCurPageTrim();
+ const QRect pageTrim = _nb->getPageTrim(pageNum);
QImage image(pageTrim.width() / 4, pageTrim.height() / 4, QImage::Format_RGB32);
QPainter painter(&image);
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
@@ -134,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);
@@ -162,6 +190,16 @@ void NotebookView::nextPage()
}
}
+void NotebookView::focusOnPage(int pageNum)
+{
+ if (_pages.contains(pageNum)) {
+ if (_zoom < 100) {
+ setZoom(100);
+ }
+ setCurPage(pageNum);
+ }
+}
+
void NotebookView::resizeEvent(QResizeEvent *event)
{
QGraphicsView::resizeEvent(event);
@@ -209,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());
@@ -226,21 +264,11 @@ bool NotebookView::createPages()
_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);
+ calculateScale();
+ layoutPages();
return true;
}
@@ -253,12 +281,41 @@ void NotebookView::calculateScale()
qreal baseScale = qMin(viewRect.width() / _maxPageSize.width(),
viewRect.height() / _maxPageSize.height());
resetTransform();
+ _numColumns = 1;
+
scale(baseScale, baseScale);
+
if (_zoom < 100) {
qreal s = 0.25 + ((_zoom / 100.0) * 0.75);
+ int potentialColumns = viewRect.width() / (_maxPageSize.width() * baseScale * s);
+ if (potentialColumns >= 2) {
+ _numColumns = std::min(potentialColumns, _pages.size());
+ }
scale(s, s);
} else if (_zoom > 100) {
qreal s = 1.0 + (_zoom - 100) * 0.015;
scale(s, s);
}
}
+
+void NotebookView::layoutPages()
+{
+ const int numRows = _pages.size() / _numColumns
+ + (_pages.size() % _numColumns > 0 ? 1 : 0);
+ const QSizeF pageSpace( _maxPageSize.width() + PAGE_SEPARATION,
+ _maxPageSize.height() + PAGE_SEPARATION);
+
+ int pageIndex = 0;
+ foreach (PageItem *page, _pages) {
+ QRectF box = page->boundingRect();
+ qreal curX = (pageIndex % _numColumns) * pageSpace.width();
+ qreal curY = (pageIndex / _numColumns) * pageSpace.height();
+ page->setPos(curX + (_maxPageSize.width() - box.width()) / 2.0,
+ curY + (_maxPageSize.height() - box.height()) / 2.0);
+ pageIndex++;
+ }
+
+ scene()->setSceneRect(0, 0,
+ pageSpace.width() * _numColumns,
+ pageSpace.height() * numRows);
+}
diff --git a/notebookview.h b/notebookview.h
index c9f5686..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:
@@ -69,6 +75,7 @@ public slots:
void setZoom(int zoom);
void prevPage();
void nextPage();
+ void focusOnPage(int pageNum);
protected:
void resizeEvent(QResizeEvent *event);
@@ -78,14 +85,17 @@ private:
void removePages();
bool createPages();
void calculateScale();
+ void layoutPages();
private:
AfdNotebook *_nb;
PaperReplay *_replay;
QString _nbPath;
QString _replayPath;
+ quint64 _penUserTime;
QMap<int, PageItem*> _pages;
QSizeF _maxPageSize;
+ int _numColumns;
int _zoom;
int _curPage;
};
diff --git a/pageitem.cc b/pageitem.cc
index b5be4cf..9260ecd 100644
--- a/pageitem.cc
+++ b/pageitem.cc
@@ -18,6 +18,8 @@
#include <QtCore/QDebug>
#include <QtGui/QPen>
+#include <QtWidgets/QGraphicsSceneMouseEvent>
+#include "notebookview.h"
#include "pageitem.h"
#include "stfgraphicsitem.h"
@@ -65,6 +67,17 @@ int PageItem::pageNum() const
return _pageNum;
}
+void PageItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
+{
+ QGraphicsView *view = scene()->views().first();
+ if (NotebookView *nbview = qobject_cast<NotebookView*>(view)) {
+ nbview->focusOnPage(_pageNum);
+ event->accept();
+ return;
+ }
+ QGraphicsItem::mouseDoubleClickEvent(event);
+}
+
void PageItem::createStrokes()
{
QStringList pens = _nb->penSerials();
diff --git a/pageitem.h b/pageitem.h
index 831ca61..0963276 100644
--- a/pageitem.h
+++ b/pageitem.h
@@ -37,6 +37,9 @@ public:
int pageNum() const;
+protected:
+ void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
+
private:
void createStrokes();
diff --git a/paperreplay.cc b/paperreplay.cc
index cae62f7..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;
@@ -176,6 +184,7 @@ bool PaperReplay::open(const QString &path, quint64 notebookGuid)
_byPageTime[page].insert(session.d->start, session);
}
}
+
return true;
}
@@ -184,6 +193,7 @@ void PaperReplay::close()
_byPageTime.clear();
_sessions.clear();
_dir.setPath(QString());
+ _userTime = 0;
}
QList<PaperReplay::Session> PaperReplay::sessions() const
@@ -191,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)) {
@@ -206,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 ||
@@ -230,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;
@@ -247,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;
@@ -261,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;
@@ -279,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)) {
@@ -295,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 ||
@@ -319,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;
@@ -329,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 328939c..5427dee 100644
--- a/paperreplay.h
+++ b/paperreplay.h
@@ -24,17 +24,26 @@
#include <QtCore/QMap>
#include <QtCore/QVector>
+/** 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;
};
@@ -48,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;
@@ -73,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 0623205..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 quazip
+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 \
@@ -46,9 +56,13 @@ isEmpty(PREFIX) {
PREFIX = /usr
}
+isEmpty(UDEVDIR) {
+ UDEVDIR = $$system(pkg-config --variable=udevdir udev)
+}
+
target.path = $$PREFIX/bin
-udev.path = /lib/udev/rules.d
+udev.path = $$UDEVDIR/rules.d
udev.files = 60-livescribe.rules
icon_scalable.path = $$PREFIX/share/icons/hicolor/scalable/apps
diff --git a/smartpen.cc b/smartpen.cc
index 1282e33..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(int(parameter), 4);
+ 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;
+}
+
+QString Smartpen::getPenSerial()
+{
+ return toPenSerial(getPenId());
+}
+
+QString Smartpen::getPenName()
+{
+ return QString::fromUtf8(getParameter(Parameter::Name));
+}
- QByteArray hex = QByteArray::fromHex(name.mid(2).toLatin1());
- return QString::fromUtf8(hex);
+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()
@@ -120,6 +154,8 @@ QVariantMap Smartpen::getPenInfo()
QByteArray data = getObject("peninfo");
QXmlStreamReader r(data);
+ qDebug() << "PenInfo: " << QString::fromLatin1(data);
+
advanceToFirstChildElement(r, "xml");
advanceToFirstChildElement(r, "peninfo");
@@ -214,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)
+QDateTime Smartpen::fromPenTime(PenTime userTime, PenTime penTime)
{
- 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();
- }
+ 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 30c1f74..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 {
- PenName = 8011
+ 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 8047f4e..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;
@@ -262,16 +280,16 @@ bool SmartpenSyncer::syncPen()
bool SmartpenSyncer::syncNotebook(Smartpen::PenTime lastSync, const Smartpen::ChangeReport &change)
{
- QDir notebookDir(_penDataDir.filePath(change.title + ".afd"));
+ QDir notebookDir(_penDataDir.filePath(change.title + "." AFD_NOTEBOOK_EXTENSION));
if (!notebookDir.exists()) {
if (!notebookDir.mkpath(".")) {
qWarning() << "Cannot create notebook data directory:" << notebookDir.absolutePath();
}
}
- 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 dbc7ca0..a9ca693 100644
--- a/stfstrokeitem.cc
+++ b/stfstrokeitem.cc
@@ -29,7 +29,7 @@ StfStrokeItem::StfStrokeItem(const QPainterPath &stroke, const PaperReplay::Sess
_startTime(startTime), _endTime(endTime)
{
QPen pen(Qt::black, 8.0f);
- if (_session.isValid()) {
+ if (hasPaperReplay()) {
pen.setColor(Qt::darkGreen);
setCursor(Qt::PointingHandCursor);
}
@@ -41,16 +41,25 @@ int StfStrokeItem::type() const
return Type;
}
+bool StfStrokeItem::hasPaperReplay() const
+{
+ return _session.isValid() && !_session.fileName().isEmpty();
+}
+
void StfStrokeItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
- if (_session.isValid() && !_session.fileName().isEmpty()) {
+ if (event->button() == Qt::LeftButton && hasPaperReplay()) {
QGraphicsView *view = scene()->views().first();
if (NotebookView *nbview = qobject_cast<NotebookView*>(view)) {
- event->accept();
- 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;
}
}
+ QGraphicsItem::mousePressEvent(event);
}
diff --git a/stfstrokeitem.h b/stfstrokeitem.h
index 5e7a253..eaf8d8a 100644
--- a/stfstrokeitem.h
+++ b/stfstrokeitem.h
@@ -29,10 +29,12 @@ public:
enum { Type = UserType + 't' };
- int type() const;
+ int type() const override;
+
+ bool hasPaperReplay() const;
protected:
- void mousePressEvent(QGraphicsSceneMouseEvent *event);
+ void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
private:
PaperReplay::Session _session;