aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--60-livescribe.rules2
-rw-r--r--README.md39
-rw-r--r--afdnotebook.cc66
-rw-r--r--afdnotebook.h13
-rw-r--r--main.cc10
-rw-r--r--mainwindow.cc228
-rw-r--r--mainwindow.h23
-rw-r--r--mainwindow.ui383
-rw-r--r--notebookmodel.cc148
-rw-r--r--notebookmodel.h24
-rw-r--r--notebookview.cc89
-rw-r--r--notebookview.h16
-rw-r--r--pageitem.cc15
-rw-r--r--pageitem.h5
-rw-r--r--paperreplay.cc69
-rw-r--r--paperreplay.h55
-rw-r--r--paperreplaymodel.cc21
-rw-r--r--paperreplaymodel.h5
-rw-r--r--scribiu.pro32
-rw-r--r--smartpen.cc344
-rw-r--r--smartpen.h37
-rw-r--r--smartpenmanager.cc278
-rw-r--r--smartpenmanager.h32
-rw-r--r--smartpensyncer.cc88
-rw-r--r--smartpensyncer.h20
-rw-r--r--stfexporter.cc178
-rw-r--r--stfexporter.h39
-rw-r--r--stfgraphicsitem.cc11
-rw-r--r--stfgraphicsitem.h2
-rw-r--r--stfreader.cc19
-rw-r--r--stfreader.h2
-rw-r--r--stfstrokeitem.cc25
-rw-r--r--stfstrokeitem.h8
34 files changed, 1636 insertions, 695 deletions
diff --git a/.gitignore b/.gitignore
index 75c107b..44a5db4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,6 @@
*.pro.user
+*.pro.user.*
+.qmake.stash
+Makefile
+/build
+/build-*
diff --git a/60-livescribe.rules b/60-livescribe.rules
index 340ac5a..a10d9c3 100644
--- a/60-livescribe.rules
+++ b/60-livescribe.rules
@@ -1 +1 @@
-SUBSYSTEM=="usb", ENV{ID_VENDOR}=="Livescribe", TAG+="udev-acl", TAG+="livescribe-pen"
+SUBSYSTEM=="usb", ENV{ID_VENDOR}=="Livescribe", TAG+="uaccess", TAG+="livescribe-pen"
diff --git a/README.md b/README.md
index aecd148..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,37 +10,50 @@ It also allows you to export individual pages as PNG files or voice memos as AAC
# Requirements
-Scribiu requires Qt 4.8, including the core, gui, svg and phonon modules.
-It also requires libudev, openobex, 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.
# Design
-By default Scribiu stores its information inside `$XDG_DATA_HOME/data/scribiu`.
+By default Scribiu stores its information inside `$XDG_DATA_HOME/scribiu`.
Inside there you will find a directory for every synchronized pen, and inside the pen directory, you will find a subdirectory for each one of your LiveScribe notebooks.
These directories contain the raw notebook, stroke, and voice data as received from the pen -- no processing is done.
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 9ddf7c4..f3d6371 100644
--- a/main.cc
+++ b/main.cc
@@ -17,15 +17,21 @@
*/
#include "mainwindow.h"
-#include <QApplication>
+#include <QtWidgets/QApplication>
+#include <QtGui/QPixmapCache>
int main(int argc, char *argv[])
{
+ QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
+
QApplication app(argc, argv);
app.setOrganizationName("scribiu");
app.setOrganizationDomain("com.javispedro.scribiu");
app.setApplicationName("scribiu");
- app.setApplicationVersion("1.0");
+ app.setApplicationVersion("1.5");
+
+ QPixmapCache::setCacheLimit(100 * 1024);
MainWindow w;
w.show();
diff --git a/mainwindow.cc b/mainwindow.cc
index 5d513b1..cf1ef1a 100644
--- a/mainwindow.cc
+++ b/mainwindow.cc
@@ -19,54 +19,39 @@
#include <QtCore/QDebug>
#include <QtCore/QTimer>
#include <QtCore/QSettings>
-#include <QtGui/QFileDialog>
-#include <QtGui/QMessageBox>
+#include <QtWidgets/QFileDialog>
+#include <QtWidgets/QMessageBox>
#include <QtSvg/QSvgGenerator>
#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)
{
ui->setupUi(this);
-#if QT_VERSION < QT_VERSION_CHECK(5, 1, 0)
- // Some tricks for DPI support
- const qreal scale = logicalDpiX() / 96.0;
- if (scale > 1.1) {
- ui->notebookTree->header()->setDefaultSectionSize(ui->notebookTree->header()->defaultSectionSize() * scale);
- ui->notebookTree->setIconSize(QSize(16, 16) * scale);
- ui->prevButton->setMaximumSize(ui->prevButton->maximumSize() * scale);
- ui->nextButton->setMaximumSize(ui->nextButton->maximumSize() * scale);
- ui->exportButton->setMaximumSize(ui->exportButton->maximumSize() * scale);
- ui->playButton->setMaximumSize(ui->playButton->maximumSize() * scale);
- ui->pauseButton->setMaximumSize(ui->pauseButton->maximumSize() * scale);
- ui->playButton->setMaximumSize(ui->prevButton->maximumSize() * scale);
- ui->pageEdit->setMaximumWidth(ui->pageEdit->maximumWidth() * scale);
- }
-#endif
ui->notebookTree->setModel(_notebooks);
- ui->notebookTree->header()->setResizeMode(0, QHeaderView::Stretch);
- ui->notebookTree->header()->setResizeMode(1, QHeaderView::Fixed);
- ui->notebookTree->header()->setResizeMode(2, QHeaderView::Fixed);
+ ui->notebookTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ 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()->setResizeMode(0, QHeaderView::Stretch);
- ui->paperReplayView->horizontalHeader()->setResizeMode(1, QHeaderView::Fixed);
- ui->paperReplayView->setVisible(false);
- Phonon::createPath(_media, _mediaOutput);
- _media->setTickInterval(500);
- ui->replaySlider->setMediaObject(_media);
+ ui->paperReplayView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
+ ui->paperReplayView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed);
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()),
@@ -75,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");
@@ -100,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)
@@ -114,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));
@@ -157,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;
@@ -185,10 +198,11 @@ void MainWindow::handleNotebookSelected(const QModelIndex &index)
return;
}
- QModelIndex child = parent.child(index.row(), 0);
+ // 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(child, 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)
@@ -219,71 +233,80 @@ void MainWindow::handlePaperReplayRequested(const QString &file, qint64 time)
QString filePath = finfo.canonicalFilePath();
- if (_media->currentSource().fileName() != filePath) {
- _media->setCurrentSource(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()
@@ -308,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:
@@ -323,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)) {
@@ -348,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"));
@@ -376,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;
@@ -391,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 6689795..03479a2 100644
--- a/mainwindow.h
+++ b/mainwindow.h
@@ -19,10 +19,9 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
-#include <QtGui/QMainWindow>
-#include <QtGui/QLabel>
-#include <phonon/MediaObject>
-#include <phonon/AudioOutput>
+#include <QtWidgets/QMainWindow>
+#include <QtWidgets/QLabel>
+#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 1cfff9a..52a8891 100644
--- a/mainwindow.ui
+++ b/mainwindow.ui
@@ -15,8 +15,7 @@
</property>
<property name="windowIcon">
<iconset theme="scribiu">
- <normaloff/>
- </iconset>
+ <normaloff>.</normaloff>.</iconset>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="verticalLayout">
@@ -57,7 +56,7 @@
<bool>false</bool>
</attribute>
<attribute name="headerDefaultSectionSize">
- <number>27</number>
+ <number>41</number>
</attribute>
<attribute name="headerStretchLastSection">
<bool>false</bool>
@@ -72,19 +71,12 @@
<layout class="QHBoxLayout" name="viewpaneTools">
<item>
<widget class="QToolButton" name="prevButton">
- <property name="maximumSize">
- <size>
- <width>30</width>
- <height>30</height>
- </size>
- </property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="go-previous">
- <normaloff/>
- </iconset>
+ <normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
@@ -109,16 +101,9 @@
</item>
<item>
<widget class="QToolButton" name="nextButton">
- <property name="maximumSize">
- <size>
- <width>30</width>
- <height>30</height>
- </size>
- </property>
<property name="icon">
<iconset theme="go-next">
- <normaloff/>
- </iconset>
+ <normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
@@ -140,19 +125,12 @@
</item>
<item>
<widget class="QToolButton" name="exportButton">
- <property name="maximumSize">
- <size>
- <width>30</width>
- <height>30</height>
- </size>
- </property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
- <iconset theme="document-save-as">
- <normaloff/>
- </iconset>
+ <iconset theme="document-send">
+ <normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
@@ -169,7 +147,7 @@
</property>
</spacer>
</item>
- <item>
+ <item alignment="Qt::AlignVCenter">
<widget class="QLabel" name="zoomLabel">
<property name="text">
<string>Zoom:</string>
@@ -179,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>
@@ -222,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">
@@ -277,45 +238,48 @@
</property>
<item>
<widget class="QToolButton" name="playButton">
- <property name="maximumSize">
- <size>
- <width>30</width>
- <height>30</height>
- </size>
- </property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset theme="media-playback-start">
- <normaloff/>
- </iconset>
+ <normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="pauseButton">
- <property name="maximumSize">
- <size>
- <width>30</width>
- <height>30</height>
- </size>
- </property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset theme="media-playback-pause">
- <normaloff/>
- </iconset>
+ <normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
- <widget class="Phonon::SeekSlider" name="replaySlider" native="true"/>
+ <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">
@@ -362,8 +326,7 @@
<action name="actionQuit">
<property name="icon">
<iconset theme="application-exit">
- <normaloff/>
- </iconset>
+ <normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>&amp;Quit</string>
@@ -377,19 +340,20 @@
</action>
<action name="actionExport">
<property name="icon">
- <iconset theme="document-save-as">
- <normaloff/>
- </iconset>
+ <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">
<iconset theme="help-about">
- <normaloff/>
- </iconset>
+ <normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>About...</string>
@@ -411,12 +375,6 @@
<slot>nextPage()</slot>
</slots>
</customwidget>
- <customwidget>
- <class>Phonon::SeekSlider</class>
- <extends>QWidget</extends>
- <header location="global">Phonon/SeekSlider</header>
- <container>1</container>
- </customwidget>
</customwidgets>
<resources/>
<connections>
@@ -427,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>
@@ -453,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>
@@ -533,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>
@@ -661,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>
@@ -685,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 e52d3f4..9a51bf9 100644
--- a/notebookmodel.cc
+++ b/notebookmodel.cc
@@ -18,10 +18,10 @@
#include <QtCore/QDebug>
#include <QtCore/QSettings>
-#include <QtGui/QApplication>
#include <QtGui/QIcon>
#include <QtGui/QDesktopServices>
-#include <QtGui/QStyle>
+#include <QtWidgets/QApplication>
+#include <QtWidgets/QStyle>
#include "paperreplay.h"
#include "afdnotebook.h"
#include "notebookmodel.h"
@@ -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()),
@@ -46,7 +49,7 @@ NotebookModel::NotebookModel(QObject *parent) :
QString NotebookModel::defaultDataDirectory()
{
- QString path = QDesktopServices::storageLocation(QDesktopServices::DataLocation);
+ QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (path.isEmpty()) {
path = QDir::home().absoluteFilePath(".scribiu");
}
@@ -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,29 +159,27 @@ 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;
}
+ break;
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();
@@ -237,11 +259,9 @@ 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);
- }
+ QStringList pens = _dataDir.entryList(QStringList{"*." PEN_EXTENSION, "*." ARCHIVE_EXTENSION}, QDir::Dirs, QDir::Name);
+ // Insert/remove new/deleted pens
int i = 0, j = 0;
while (i < _pens.size() && j < pens.size()) {
int comp = QString::compare(_pens[i], pens[j], Qt::CaseInsensitive);
@@ -288,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());
@@ -336,7 +354,7 @@ void NotebookModel::refreshPen(const QString &name)
int NotebookModel::indexOfPen(const QString &name)
{
- QStringList::const_iterator it = qBinaryFind(_pens, name);
+ auto it = std::lower_bound(_pens.begin(), _pens.end(), name);
if (it == _pens.end()) {
return -1;
} else {
@@ -354,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;
@@ -390,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;
@@ -399,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
@@ -419,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 605db5c..7d62fab 100644
--- a/notebookview.h
+++ b/notebookview.h
@@ -19,8 +19,8 @@
#ifndef NOTEBOOKVIEW_H
#define NOTEBOOKVIEW_H
-#include <QtGui/QGraphicsView>
-#include <QtGui/QGraphicsItem>
+#include <QtWidgets/QGraphicsView>
+#include <QtWidgets/QGraphicsItem>
#include "afdnotebook.h"
#include "paperreplay.h"
#include "pageitem.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 09c84ae..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"
@@ -35,7 +37,7 @@ PageItem::PageItem(AfdNotebook *nb, PaperReplay *replay, int pageNum, QGraphicsI
bg->setShapeMode(QGraphicsPixmapItem::BoundingRectShape);
bg->setTransformationMode(Qt::SmoothTransformation);
QRectF bgRect = bg->boundingRect();
- bg->scale(_pageTrim.width() / bgRect.width(), _pageTrim.height() / bgRect.height());
+ bg->setScale(std::min(_pageTrim.width() / bgRect.width(), _pageTrim.height() / bgRect.height()));
bg->setPos(_pageTrim.topLeft());
}
@@ -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 446b7b5..0963276 100644
--- a/pageitem.h
+++ b/pageitem.h
@@ -19,7 +19,7 @@
#ifndef PAGEITEM_H
#define PAGEITEM_H
-#include <QtGui/QGraphicsItem>
+#include <QtWidgets/QGraphicsItem>
#include "afdnotebook.h"
#include "paperreplay.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 ae17367..dcab5cf 100644
--- a/paperreplaymodel.cc
+++ b/paperreplaymodel.cc
@@ -98,34 +98,29 @@ void PaperReplayModel::refresh()
{
beginResetModel();
_sessions = _replay->sessions();
- qSort(_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 a9b12d0..0c68fa4 100644
--- a/scribiu.pro
+++ b/scribiu.pro
@@ -1,20 +1,29 @@
-QT += core gui svg phonon
-
-greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
-
TARGET = scribiu
+VERSION = 1.5.3
TEMPLATE = app
+QT += core gui widgets svg multimedia
+
+CONFIG += c++11
+
CONFIG += link_pkgconfig
-PKGCONFIG += libudev libusb openobex
-LIBS += -lquazip
+PKGCONFIG += libudev libusb-1.0 openobex
-SOURCES += main.cc\
- mainwindow.cc \
+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 \
smartpenmanager.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 79e9a01..12e2ac2 100644
--- a/smartpen.cc
+++ b/smartpen.cc
@@ -16,31 +16,26 @@
* 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>
#include <QtCore/QStringList>
#include <QtCore/QtEndian>
-#include <usb.h>
+#include <libusb-1.0/libusb.h>
#include "xmlutils.h"
#include "smartpen.h"
-#define PEN_EPOCH (1289335960000LL) // This is probably not correct
-#define PEN_MTU 900
-#define PEN_TIMEOUT_SECONDS 10
+#define PEN_MTU OBEX_MAXIMUM_MTU
+#define PEN_TIMEOUT_SECONDS 5
#define INVALID_CID 0xFFFFFFFFU
static const char pen_serial_chars[] = "ABCDEFGHJKMNPQRSTUWXYZ23456789";
static const unsigned int pen_serial_num_chars = sizeof(pen_serial_chars) - 1;
-/* Terrible hack comes now: */
-struct obex_usb_intf_transport_t {
- struct obex_usb_intf_transport_t *prev, *next; /* Next and previous interfaces in the list */
- struct usb_device *device; /* USB device that has the interface */
-};
-
Smartpen::Smartpen(QObject *parent) :
- QObject(parent), _obex(0), _connId(INVALID_CID)
+ QObject(parent), _obex(0), _connId(INVALID_CID), _reqComplete(false), _continueReceived(0)
{
}
@@ -58,6 +53,10 @@ bool Smartpen::isConnected() const
QByteArray Smartpen::getObject(const QString &name)
{
+ qDebug() << "Getting object" << name;
+
+ prepareRequest();
+
obex_object_t *obj = OBEX_ObjectNew(_obex, OBEX_CMD_GET);
Q_ASSERT(obj);
@@ -72,23 +71,13 @@ QByteArray Smartpen::getObject(const QString &name)
return QByteArray();
}
- qDebug() << "Getting object" << name;
-
if (OBEX_Request(_obex, obj) < 0) {
qWarning() << "Get object request failed";
+ OBEX_ObjectDelete(_obex, obj);
return QByteArray();
}
- QDateTime start = QDateTime::currentDateTimeUtc();
- QDateTime now;
- do {
- OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS);
- now = QDateTime::currentDateTimeUtc();
- } while (_inBuf.isEmpty() && start.secsTo(now) < PEN_TIMEOUT_SECONDS);
-
- if (_inBuf.isEmpty()) {
- qWarning() << "Did not receive any data in" << start.secsTo(now) << "seconds";
- }
+ waitForRequestComplete(PEN_TIMEOUT_SECONDS);
QByteArray result;
qSwap(_inBuf, result);
@@ -96,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);
@@ -107,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;
+ 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());
+}
- QByteArray hex = QByteArray::fromHex(name.mid(2).toLatin1());
- return QString::fromUtf8(hex);
+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()
@@ -130,6 +154,8 @@ QVariantMap Smartpen::getPenInfo()
QByteArray data = getObject("peninfo");
QXmlStreamReader r(data);
+ qDebug() << "PenInfo: " << QString::fromLatin1(data);
+
advanceToFirstChildElement(r, "xml");
advanceToFirstChildElement(r, "peninfo");
@@ -163,7 +189,7 @@ QList<Smartpen::ChangeReport> Smartpen::getChangeList(PenTime from)
QByteArray data = getObject(QString("changelist?start_time=%1").arg(from));
QXmlStreamReader r(data);
- qDebug() << QString::fromAscii(data);
+ qDebug() << "changelist:" << QString::fromLatin1(data);
advanceToFirstChildElement(r, "xml");
advanceToFirstChildElement(r, "changelist");
@@ -224,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)
@@ -271,6 +286,64 @@ quint64 Smartpen::toPenId(const QString &serial)
return id;
}
+bool Smartpen::reset(const Address &addr)
+{
+ libusb_context *ctx = 0;
+ libusb_device **devlist = 0;
+ libusb_device *dev = 0;
+ libusb_device_handle *handle = 0;
+ ssize_t ndevs;
+ int err = 0;
+
+ err = libusb_init(&ctx);
+ if (err != 0) {
+ qWarning() << "libusb_init failed:" << err;
+ goto err0;
+ }
+
+ ndevs = libusb_get_device_list(ctx, &devlist);
+ if (ndevs < 0) {
+ qWarning() << "libusb_get_device_list failed:" << err;
+ goto err1;
+ }
+
+ for (ssize_t i = 0; i < ndevs; ++i) {
+ if (libusb_get_bus_number(devlist[i]) == addr.first &&
+ libusb_get_device_address(devlist[i]) == addr.second) {
+ dev = devlist[i];
+ }
+ }
+
+ if (!dev) {
+ qWarning() << "could not find device in libusb";
+ err = -ENODEV;
+ goto err2;
+ }
+
+ err = libusb_open(dev, &handle);
+ if (err != 0) {
+ qWarning() << "libusb_open failed:" << err;
+ goto err2;
+ }
+
+ err = libusb_reset_device(handle);
+ if (err != 0) {
+ qWarning() << "libusb_reset_device failed: " << err;
+ goto err3;
+ }
+
+ qDebug() << "USB device resetted";
+
+err3:
+ libusb_close(handle);
+err2:
+ libusb_free_device_list(devlist, 1);
+err1:
+ libusb_exit(ctx);
+err0:
+ return err == 0;
+}
+
bool Smartpen::connectToPen(const Address &addr)
{
if (_obex) {
@@ -278,18 +351,21 @@ bool Smartpen::connectToPen(const Address &addr)
return false;
}
- _obex = OBEX_Init(OBEX_TRANS_USB, obexEventCb, 0);
+ _connId = INVALID_CID;
+
+ _obex = OBEX_Init(OBEX_TRANS_USB, obexEventCb, OBEX_FL_CLOEXEC);
Q_ASSERT(_obex);
OBEX_SetUserData(_obex, this);
OBEX_SetTransportMTU(_obex, PEN_MTU, PEN_MTU);
- obex_interface_t *interfaces, *ls_interface = 0;
- int count = OBEX_FindInterfaces(_obex, &interfaces);
+ obex_interface_t *ls_interface = 0;
+ int count = OBEX_EnumerateInterfaces(_obex);
for (int i = 0; i < count; i++) {
- if (interfaces[i].usb.intf->device->bus->location == addr.first &&
- interfaces[i].usb.intf->device->devnum == addr.second) {
- ls_interface = &interfaces[i];
+ obex_interface_t *intf = OBEX_GetInterfaceByIndex(_obex, i);
+ if (intf->usb.bus_number == addr.first &&
+ intf->usb.device_address == addr.second) {
+ ls_interface = intf;
}
}
@@ -298,22 +374,16 @@ bool Smartpen::connectToPen(const Address &addr)
return false;
}
- usb_dev_handle *handle = usb_open(ls_interface->usb.intf->device);
- if (handle) {
- qDebug() << "resetting usb device";
- usb_reset(handle);
- usb_close(handle);
- } else {
- qWarning() << "could not open usb device for resetting";
- }
+ qDebug() << "Connecting to" << ls_interface->usb.product;
- qDebug() << "connecting to" << ls_interface->usb.product;
-
- if (OBEX_InterfaceConnect(_obex, ls_interface) < 0) {
- qWarning() << "Could not connect to Livescribe interface";
+ int err = OBEX_InterfaceConnect(_obex, ls_interface) ;
+ if (err < 0) {
+ qWarning() << "Could not connect to Livescribe interface" << strerror(-err);
return false;
}
+ prepareRequest();
+
static const char * livescribe_service = "LivescribeService";
obex_object_t *object = OBEX_ObjectNew(_obex, OBEX_CMD_CONNECT);
obex_headerdata_t hd;
@@ -337,7 +407,7 @@ bool Smartpen::connectToPen(const Address &addr)
qDebug() << "Connection in progress";
- OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS);
+ waitForRequestComplete(PEN_TIMEOUT_SECONDS);
return _connId != INVALID_CID;
}
@@ -346,19 +416,27 @@ void Smartpen::disconnectFromPen()
{
if (_connId != INVALID_CID) {
if (_obex) {
+ OBEX_CancelRequest(_obex, 0);
+
+ prepareRequest();
+
obex_object_t *object = OBEX_ObjectNew(_obex, OBEX_CMD_DISCONNECT);
Q_ASSERT(object);
addConnHeader(object);
- OBEX_Request(_obex, object);
- OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS);
+ if (OBEX_Request(_obex, object) == 0) {
+ qDebug() << "Send disconnect";
+ waitForRequestComplete(PEN_TIMEOUT_SECONDS);
+ }
}
_connId = INVALID_CID;
}
if (_obex) {
+ OBEX_TransportDisconnect(_obex);
OBEX_Cleanup(_obex);
_obex = 0;
}
_inBuf.clear();
+ qDebug() << "Disconnected";
}
void Smartpen::obexEventCb(obex_t *handle, obex_object_t *obj,
@@ -372,24 +450,38 @@ void Smartpen::obexEventCb(obex_t *handle, obex_object_t *obj,
void Smartpen::handleObexEvent(obex_object_t *object,
int event, int obex_cmd, int obex_rsp)
{
+ // Special flag used for temporarily ignoring the synthetic events generated during OBEX_CancelRequest
+ static bool aborting_continue = false;
+ if (aborting_continue) return;
switch (event) {
case OBEX_EV_PROGRESS:
- if (obex_cmd == OBEX_CMD_GET) {
- // It seems that the pen wants us to add this header on every continue response
- addConnHeader(object);
- }
break;
case OBEX_EV_REQDONE:
qDebug() << "event reqdone cmd=" << obex_cmd << " rsp=" << OBEX_ResponseToString(obex_rsp);
handleObexRequestDone(object, obex_cmd, obex_rsp);
break;
case OBEX_EV_LINKERR:
- qWarning() << "link error cmd=" << obex_cmd;
- emit error();
+ qWarning() << "link error cmd=" << obex_cmd << " rsp=" << OBEX_ResponseToString(obex_rsp);
+ emit linkError("Link error");
+ break;
+ case OBEX_EV_PARSEERR:
+ qWarning() << "parse error cmd=" << obex_cmd << " rsp=" << OBEX_ResponseToString(obex_rsp);
+ emit linkError("Protocol error");
+ break;
+ case OBEX_EV_CONTINUE:
+ // The standard "Continue" messages sent by OpenObex "forget" to include the ConnectionID, confusing the pen
+ // We're going to therefore implement the "continue" state machine on our own
+ handleObexContinue(object, obex_cmd);
+
+ // Cancel OpenObex's Continue message, avoiding the spurious events that generates
+ Q_ASSERT(!aborting_continue);
+ aborting_continue = true;
+ OBEX_CancelRequest(_obex, 0);
+ aborting_continue = false;
break;
default:
- qDebug() << "event" << event << obex_cmd << obex_rsp;
+ qDebug() << "event unknown=" << event << " cmd=" << obex_cmd << " rsp=" << OBEX_ResponseToString(obex_rsp);
break;
}
}
@@ -400,6 +492,8 @@ void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int ob
obex_headerdata_t hdata;
quint32 hlen;
+ _reqComplete = true;
+
switch (obex_cmd & ~OBEX_FINAL) {
case OBEX_CMD_CONNECT:
switch (obex_rsp) {
@@ -414,7 +508,7 @@ void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int ob
break;
default:
qWarning() << "Failed connection request:" << OBEX_ResponseToString(obex_rsp);
- emit error();
+ emit linkError("OBEX connection error");
break;
}
break;
@@ -437,7 +531,7 @@ void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int ob
qDebug() << "GET request succesful";
while (OBEX_ObjectGetNextHeader(_obex, object, &header_id, &hdata, &hlen)) {
if (header_id == OBEX_HDR_BODY || header_id == OBEX_HDR_BODY_END) {
- _inBuf = QByteArray(reinterpret_cast<const char*>(hdata.bs), hlen);
+ _inBuf.append(reinterpret_cast<const char*>(hdata.bs), hlen);
}
}
break;
@@ -450,6 +544,85 @@ void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int ob
}
}
+void Smartpen::handleObexContinue(obex_object_t *object, int obex_cmd)
+{
+ const uint8_t *data;
+ int len;
+
+ _continueReceived++;
+
+ switch (obex_cmd & ~OBEX_FINAL) {
+ case OBEX_CMD_GET:
+ len = OBEX_ObjectReadStream(_obex, object, &data);
+ if (len > 0) {
+ _inBuf.append(reinterpret_cast<const char*>(data), len);
+ }
+ break;
+ }
+}
+
+void Smartpen::prepareRequest()
+{
+ _reqComplete = false;
+ _continueReceived = 0;
+ _inBuf.clear();
+}
+
+bool Smartpen::waitForRequestComplete(int timeout)
+{
+ QDeadlineTimer timer(timeout * 1000UL);
+ timer.setTimerType(Qt::CoarseTimer);
+
+ int cmd = OBEX_ObjectGetCommand(_obex, NULL);
+
+ do {
+ if (OBEX_HandleInput(_obex, timer.remainingTime() / 1000) < 0) {
+ qWarning() << "OBEX_HandleInput failed";
+ break;
+ }
+ if (_continueReceived) {
+ sendContinue(cmd);
+ _continueReceived--;
+
+ // Reset timeout
+ timer.setRemainingTime(timeout * 1000UL);
+ }
+ } while (!_reqComplete && !timer.hasExpired());
+
+ if (!_reqComplete) {
+ qWarning() << "Did not complete request in" << timeout << "seconds";
+ emit linkError("Timeout");
+ return false;
+ }
+
+ return true;
+}
+
+void Smartpen::addConnHeader(obex_object_t *obj) const
+{
+ obex_headerdata_t hd;
+ hd.bq4 = _connId;
+ if (OBEX_ObjectAddHeader(_obex, obj, OBEX_HDR_CONNECTION, hd, sizeof(hd.bq4), OBEX_FL_FIT_ONE_PACKET) < 0) {
+ qCritical() << "Could not add connection header";
+ }
+}
+
+bool Smartpen::sendContinue(int obex_cmd)
+{
+ obex_object_t *obj = OBEX_ObjectNew(_obex, obex_cmd);
+ Q_ASSERT(obj);
+
+ addConnHeader(obj);
+
+ if (OBEX_Request(_obex, obj) < 0) {
+ qWarning() << "Send continue failed";
+ OBEX_ObjectDelete(_obex, obj);
+ return false;
+ }
+
+ return true;
+}
+
QString Smartpen::toPenSerialSegment(quint32 id, int len)
{
@@ -469,7 +642,7 @@ quint32 Smartpen::fromPenSerialSegment(const QString &s)
quint32 id = 0;
for (int i = 0; i < len; i++) {
- uint val = qFind(&pen_serial_chars[0], &pen_serial_chars[pen_serial_num_chars], s[i]) - &pen_serial_chars[0];
+ uint val = std::find(&pen_serial_chars[0], &pen_serial_chars[pen_serial_num_chars], s[i]) - &pen_serial_chars[0];
if (val >= pen_serial_num_chars) return 0;
id = val + id * pen_serial_num_chars;
}
@@ -488,12 +661,3 @@ QByteArray Smartpen::encodeUtf16(const QString &s)
p[size] = 0;
return data;
}
-
-void Smartpen::addConnHeader(obex_object_t *obj) const
-{
- obex_headerdata_t hd;
- hd.bq4 = _connId;
- if (OBEX_ObjectAddHeader(_obex, obj, OBEX_HDR_CONNECTION, hd, sizeof(hd.bq4), 0) < 0) {
- qCritical() << "Could not add connection header";
- }
-}
diff --git a/smartpen.h b/smartpen.h
index 18ac955..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,18 +77,19 @@ 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);
+ static bool reset(const Address &addr);
+
public slots:
bool connectToPen(const Address &addr);
void disconnectFromPen();
signals:
- void error();
+ void linkError(const QString &msg);
private:
static void obexEventCb(obex_t *handle, obex_object_t *obj,
@@ -86,17 +97,25 @@ private:
void handleObexEvent(obex_object_t *object,
int event, int obex_cmd, int obex_rsp);
void handleObexRequestDone(obex_object_t *object, int obex_cmd, int obex_rsp);
+ void handleObexContinue(obex_object_t *object, int obex_cmd);
+
+ void prepareRequest();
+ bool waitForRequestComplete(int timeout);
+ void addConnHeader(obex_object_t *object) const;
+ bool sendContinue(int obex_cmd);
static QString toPenSerialSegment(quint32 id, int len);
static quint32 fromPenSerialSegment(const QString &s);
static QByteArray encodeUtf16(const QString &s);
- void addConnHeader(obex_object_t *object) const;
+
private:
obex_t * _obex;
quint32 _connId;
QByteArray _inBuf;
+ bool _reqComplete;
+ uint _continueReceived;
};
#endif // SMARTPEN_H
diff --git a/smartpenmanager.cc b/smartpenmanager.cc
index 0ac56e2..d375fea 100644
--- a/smartpenmanager.cc
+++ b/smartpenmanager.cc
@@ -21,17 +21,30 @@
#include "smartpenmanager.h"
+// Sync with a connected pen every 120 seconds
+#define SYNC_INTERVAL 120
+// If the previous sync failed, try in 5 seconds
+#define FAILED_SYNC_INTERVAL 5
+#define MAX_SYNC_RETRIES 3
+
SmartpenManager::SmartpenManager(QObject *parent)
: QObject(parent), _udev(udev_new()), _monitor(udev_monitor_new_from_netlink(_udev, "udev")),
- _notifier(new QSocketNotifier(udev_monitor_get_fd(_monitor), QSocketNotifier::Read))
+ _notifier(new QSocketNotifier(udev_monitor_get_fd(_monitor), QSocketNotifier::Read)),
+ _nextTry(new QTimer(this))
{
- udev_monitor_filter_add_match_tag(_monitor, "livescribe-pen");
-
connect(_notifier, SIGNAL(activated(int)), SLOT(handleMonitorActivity()));
+ connect(_nextTry, SIGNAL(timeout()), SLOT(handleTimerNextTry()));
+ _nextTry->setSingleShot(true);
+ _nextTry->setTimerType(Qt::VeryCoarseTimer);
+
+ // Start udev monitoring
+ udev_monitor_filter_add_match_subsystem_devtype(_monitor, "usb", "usb_device");
+ udev_monitor_filter_add_match_tag(_monitor, "livescribe-pen");
udev_monitor_enable_receiving(_monitor);
udev_enumerate *scan = udev_enumerate_new(_udev);
+ udev_enumerate_add_match_subsystem(scan, "usb");
udev_enumerate_add_match_tag(scan, "livescribe-pen");
if (udev_enumerate_scan_devices(scan) == 0) {
@@ -39,7 +52,7 @@ SmartpenManager::SmartpenManager(QObject *parent)
udev_list_entry_foreach(i, l) {
const char *path = udev_list_entry_get_name(i);
udev_device *dev = udev_device_new_from_syspath(_udev, path);
- processDevice(dev);
+ processDeviceAdded(dev);
udev_device_unref(dev);
}
} else {
@@ -56,18 +69,26 @@ SmartpenManager::~SmartpenManager()
udev_unref(_udev);
}
+QStringList SmartpenManager::pensConnected() const
+{
+ QStringList pens;
+ pens.reserve(_pens.size());
+ foreach (const PenInfo &pen, _pens) {
+ if (pen.connected) {
+ pens.append(pen.name);
+ }
+ }
+ return pens;
+}
+
QStringList SmartpenManager::pensBeingSynchronized() const
{
QStringList pens;
- pens.reserve(_syncers.size());
- for (QMap<Smartpen::Address, SmartpenSyncer*>::const_iterator it = _syncers.begin();
- it != _syncers.end(); ++it) {
- QString name = it.value()->penName();
- if (name.isEmpty()) {
- Smartpen::Address addr = it.value()->penAddress();
- name = QString("%1-%2").arg(addr.first).arg(addr.second);
+ pens.reserve(_pens.size());
+ foreach (const PenInfo &pen, _pens) {
+ if (pen.syncer && pen.syncer->isRunning()) {
+ pens.append(pen.name);
}
- pens.append(name);
}
return pens;
}
@@ -78,8 +99,11 @@ void SmartpenManager::handleMonitorActivity()
udev_device *dev = udev_monitor_receive_device(_monitor);
if (dev) {
const char *action = udev_device_get_action(dev);
- if (action && strcmp(action, "add") == 0) {
- processDevice(dev);
+ Q_ASSERT(action);
+ if (strcmp(action, "add") == 0) {
+ processDeviceAdded(dev);
+ } else if (strcmp(action, "remove") == 0) {
+ processDeviceRemoved(dev);
}
udev_device_unref(dev);
}
@@ -92,31 +116,223 @@ void SmartpenManager::handleSyncerFinished()
qDebug() << "Finished synchronization with pen with address:" << addr;
- _syncers.remove(addr);
- emit pensBeingSynchronizedChanged();
+ Q_ASSERT(_pens.contains(addr));
+
+ PenInfo &pen = _pens[addr];
+
+ bool failed = syncer->hasErrors();
+ bool disconnected = false;
- if (syncer->hasErrors()) {
+ if (failed) {
qWarning() << "Synchronization with address" << addr << "failed";
- emit syncFailed(syncer->penName());
+ emit syncFailed(pen.name);
+ if (pen.connected) {
+ if (pen.numRetries < MAX_SYNC_RETRIES) {
+ // Try resetting USB device first
+ if (Smartpen::reset(addr)) {
+ pen.numRetries++;
+ pen.nextTry = QDateTime::currentDateTimeUtc().addSecs(FAILED_SYNC_INTERVAL);
+ } else {
+ qWarning() << "Failed to reset; assuming disconnected";
+ pen.connected = false;
+ disconnected = true;
+ }
+ } else {
+ qWarning() << "Too many failures; assuming disconnected";
+ pen.connected = false;
+ disconnected = true;
+ }
+ }
} else {
- emit syncComplete(syncer->penName());
+ emit syncComplete(pen.name);
+ if (pen.connected) {
+ pen.numRetries = 0;
+ pen.nextTry = QDateTime::currentDateTimeUtc().addSecs(SYNC_INTERVAL);
+ }
}
- syncer->deleteLater();
+ pen.syncer->deleteLater();
+ pen.syncer = 0;
+
+ emit pensBeingSynchronizedChanged();
+ if (disconnected)
+ emit pensConnectedChanged();
+
+ scheduleNextTry();
}
-void SmartpenManager::processDevice(udev_device *dev)
+void SmartpenManager::handleGotPenName(const QString &name)
{
- uint busnum = atol(udev_device_get_sysattr_value(dev, "busnum"));
- uint devnum = atol(udev_device_get_sysattr_value(dev, "devnum"));
-
- Smartpen::Address addr(busnum, devnum);
- if (!_syncers.contains(addr)) {
- SmartpenSyncer *syncer = new SmartpenSyncer(addr, this);
- _syncers.insert(addr, syncer);
- connect(syncer, SIGNAL(finished()), SLOT(handleSyncerFinished()));
- connect(syncer, SIGNAL(penNameChanged()), SIGNAL(pensBeingSynchronizedChanged()));
- syncer->start();
+ SmartpenSyncer *syncer = static_cast<SmartpenSyncer*>(sender());
+ Smartpen::Address addr = syncer->penAddress();
+
+ Q_ASSERT(_pens.contains(addr));
+
+ PenInfo &pen = _pens[addr];
+ pen.name = name;
+
+ if (pen.connected) {
+ emit pensConnectedChanged();
+ }
+ if (pen.syncer) {
emit pensBeingSynchronizedChanged();
}
}
+
+void SmartpenManager::handleTimerNextTry()
+{
+ QDateTime now = QDateTime::currentDateTimeUtc().addSecs(1);
+
+ foreach (const PenInfo &pen, _pens) {
+ // Not connected or already syncing: ignore
+ if (!pen.connected || pen.syncer) continue;
+
+ if (now >= pen.nextTry) {
+ trySync(pen.addr);
+ }
+ }
+
+ scheduleNextTry();
+}
+
+void SmartpenManager::processDeviceAdded(udev_device *dev)
+{
+ Smartpen::Address addr = getDeviceAddress(dev);
+ if (!addr.first && !addr.second) return;
+
+ QString name = parseUdevEscapedString(udev_device_get_property_value(dev, "ID_MODEL_ENC"));
+
+ qDebug() << "Found smartpen with address:" << addr << name;
+
+ PenInfo &pen = _pens[addr];
+ pen.addr = addr;
+ if (pen.name.isEmpty()) pen.name = name;
+ pen.connected = true;
+
+ // Schedule an attempt to sync in 1 sec
+ if (!pen.syncer) {
+ pen.numRetries = 0;
+ pen.nextTry = QDateTime::currentDateTimeUtc().addSecs(1);
+
+ scheduleNextTry();
+ }
+
+ emit pensConnectedChanged();
+}
+
+void SmartpenManager::processDeviceRemoved(udev_device *dev)
+{
+ Smartpen::Address addr = getDeviceAddress(dev);
+ if (!addr.first && !addr.second) return;
+
+ qDebug() << "Device removed with address:" << addr;
+
+ if (_pens.contains(addr)) {
+ PenInfo &pen = _pens[addr];
+ if (pen.syncer) {
+ pen.syncer->abort();
+ }
+ pen.connected = false;
+ emit pensConnectedChanged();
+ }
+}
+
+void SmartpenManager::trySync(const Smartpen::Address &addr)
+{
+ Q_ASSERT(_pens.contains(addr));
+ PenInfo &pen = _pens[addr];
+
+ qDebug() << "Starting sync for" << pen.name;
+
+ Q_ASSERT(pen.addr == addr);
+ Q_ASSERT(pen.connected);
+ Q_ASSERT(!pen.syncer);
+
+ pen.syncer = new SmartpenSyncer(addr, this);
+ connect(pen.syncer, SIGNAL(finished()), SLOT(handleSyncerFinished()));
+ connect(pen.syncer, SIGNAL(gotPenName(QString)), SLOT(handleGotPenName(QString)));
+ pen.syncer->start();
+
+ emit pensBeingSynchronizedChanged();
+}
+
+void SmartpenManager::scheduleNextTry()
+{
+ QDateTime nearest;
+ foreach (const PenInfo &pen, _pens) {
+ // Not connected or already syncing: ignore
+ if (!pen.connected || pen.syncer) continue;
+
+ if (nearest.isNull() || pen.nextTry < nearest) {
+ nearest = pen.nextTry;
+ }
+ }
+ if (nearest.isValid()) {
+ qint64 msecs = QDateTime::currentDateTimeUtc().msecsTo(nearest);
+ qDebug() << "Sleeping for" << msecs << "ms";
+ _nextTry->start(msecs);
+ } else {
+ qDebug() << "Nothing else to do";
+ _nextTry->stop();
+ }
+}
+
+QString SmartpenManager::parseUdevEscapedString(const char *s)
+{
+ // This just tries to parse \xddd C-style escape sequences
+ // Likely broken, see if there's a generic way to do this
+ const int l = strlen(s);
+
+ QString r;
+ r.reserve(l);
+
+ int prev = 0;
+ const char *bs;
+ while (prev < l && (bs = strchr(s + prev, '\\')) != NULL) {
+ int pos = bs - s;
+ r.append(QString::fromLatin1(s + prev, pos - prev));
+ if (pos + 1 >= l || s[pos + 1] == '\\') {
+ r.append('\\');
+ pos += 2;
+ } else {
+ char c = s[pos + 1];
+ if (c == 'x') {
+ int start = pos + 2;
+ int i = start;
+ for ( ; i < l; ++i) {
+ c = s[i];
+ if (s[i] >= '0' && s[i] <= '9') continue;
+ if (s[i] >= 'A' && s[i] <= 'F') continue;
+ if (s[i] >= 'a' && s[i] <= 'f') continue;
+ break;
+ }
+ bool ok = false;
+ uint charNum = QString::fromLatin1(s + start, i - start).toUInt(&ok, 16);
+ if (ok) {
+ r.append(QChar::fromLatin1(charNum));
+ } else {
+ r.append(QString::fromLatin1(s + pos, i - pos));
+ }
+ pos = i;
+ }
+ }
+ prev = pos;
+ }
+ if (prev < l) {
+ r.append(QString::fromLatin1(s + prev));
+ }
+ 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 42bdc06..6a5e39f 100644
--- a/smartpenmanager.h
+++ b/smartpenmanager.h
@@ -19,10 +19,11 @@
#ifndef SMARTPENMANAGER_H
#define SMARTPENMANAGER_H
-#include <QtCore/QObject>
-#include <QtCore/QSocketNotifier>
#include <QtCore/QMap>
+#include <QtCore/QObject>
#include <QtCore/QPair>
+#include <QtCore/QSocketNotifier>
+#include <QtCore/QTimer>
#include "smartpensyncer.h"
struct udev;
@@ -37,28 +38,47 @@ public:
explicit SmartpenManager(QObject *parent = 0);
~SmartpenManager();
+ QStringList pensConnected() const;
QStringList pensBeingSynchronized() const;
signals:
void syncComplete(const QString &penName);
void syncFailed(const QString &penName);
+ void pensConnectedChanged();
void pensBeingSynchronizedChanged();
-public slots:
-
private slots:
void handleMonitorActivity();
void handleSyncerFinished();
+ void handleGotPenName(const QString &name);
+ void handleTimerNextTry();
private:
- void processDevice(udev_device *dev);
+ void processDeviceAdded(udev_device *dev);
+ void processDeviceRemoved(udev_device *dev);
+
+ void trySync(const Smartpen::Address &addr);
+ void scheduleNextTry();
+
+ static QString parseUdevEscapedString(const char *s);
+ static Smartpen::Address getDeviceAddress(udev_device *dev);
private:
udev *_udev;
udev_monitor *_monitor;
QSocketNotifier *_notifier;
- QMap<Smartpen::Address, SmartpenSyncer*> _syncers;
+ struct PenInfo {
+ Smartpen::Address addr;
+ bool connected = false;
+ QString name;
+ SmartpenSyncer *syncer = 0;
+ QDateTime nextTry;
+ uint numRetries = 0;
+ };
+
+ QMap<Smartpen::Address, PenInfo> _pens;
+ QTimer *_nextTry;
};
#endif // SMARTPENMANAGER_H
diff --git a/smartpensyncer.cc b/smartpensyncer.cc
index f076ff9..3556f77 100644
--- a/smartpensyncer.cc
+++ b/smartpensyncer.cc
@@ -20,13 +20,13 @@
#include <QtCore/QScopedArrayPointer>
#include <QtCore/QThread>
#include <QtCore/QDebug>
-#include <quazip/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
@@ -79,7 +79,7 @@ bool LockFile::lock()
QFileInfo info(_path);
if (info.exists()) {
- if (info.created().secsTo(QDateTime::currentDateTime()) > 10 * 60) {
+ if (info.lastModified().secsTo(QDateTime::currentDateTime()) > 10 * 60) {
if (QFile::remove(info.filePath())) {
qDebug() << "Removing stale lock file:" << info.absoluteFilePath();
}
@@ -106,9 +106,9 @@ Smartpen::PenTime TimestampFile::get()
{
if (_f.exists()) {
if (_f.open(QIODevice::ReadOnly | QIODevice::Text)) {
- QString data = QString::fromAscii(_f.readLine(24));
+ QString data = QString::fromUtf8(_f.readLine(32));
_f.close();
- return data.toLongLong();
+ return data.toULongLong();
} else {
qWarning() << "Could not read timestamp file:" << _f.fileName();
}
@@ -120,18 +120,21 @@ Smartpen::PenTime TimestampFile::get()
void TimestampFile::set(Smartpen::PenTime time)
{
if (_f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
- _f.write(QString::number(time).toAscii());
+ _f.write(QString::number(time).toUtf8());
_f.close();
} else {
qWarning() << "Could not set timestamp file:" << _f.fileName();
return;
}
}
-}
+} /* anonymous namespace */
SmartpenSyncer::SmartpenSyncer(const Smartpen::Address &addr, QObject *parent) :
- QThread(parent), _addr(addr), _pen(new Smartpen(this)), _errored(false), _aborted(false)
+ QThread(parent),
+ _addr(addr), _errored(false), _aborted(false),
+ _pen(new Smartpen(this))
{
+ connect(_pen, SIGNAL(linkError(QString)), SLOT(handleLinkError(QString)));
}
SmartpenSyncer::~SmartpenSyncer()
@@ -147,11 +150,6 @@ Smartpen::Address SmartpenSyncer::penAddress() const
return _addr;
}
-QString SmartpenSyncer::penName() const
-{
- return _penName;
-}
-
bool SmartpenSyncer::hasErrors() const
{
return _errored;
@@ -162,6 +160,12 @@ void SmartpenSyncer::abort()
_aborted = true;
}
+void SmartpenSyncer::reset()
+{
+ _aborted = false;
+ _errored = false;
+}
+
void SmartpenSyncer::run()
{
if (!_pen->connectToPen(_addr)) {
@@ -170,19 +174,29 @@ void SmartpenSyncer::run()
return;
}
+ if (_aborted) {
+ _pen->disconnectFromPen();
+ return;
+ }
+
_penName = _pen->getPenName();
qDebug() << "got pen name:" << _penName;
- emit penNameChanged();
+
+ if (_penName.isEmpty()) {
+ _penName = _pen->getPenSerial();
+ qDebug() << "pen with no name, using pen serial instead:" << _penName;
+ }
+
+ emit gotPenName(_penName);
QVariantMap penInfo = _pen->getPenInfo();
if (penInfo.isEmpty()) {
qWarning() << "Could not get pen info";
_errored = true;
+ _pen->disconnectFromPen();
return;
}
- _penSerial = penInfo["penserial"].toString();
-
_penDataDir.setPath(NotebookModel::userDataDirectory() + "/" + _penName + ".pen");
if (!_penDataDir.exists()) {
if (!_penDataDir.mkpath(".")) {
@@ -194,6 +208,12 @@ void SmartpenSyncer::run()
_errored = true;
}
+ if (_errored) {
+ qDebug() << "Sync finished with errors";
+ } else {
+ qDebug() << "Sync finished";
+ }
+
_pen->disconnectFromPen();
}
@@ -205,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;
@@ -248,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;
}
@@ -279,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;
}
@@ -305,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();
@@ -336,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();
@@ -354,8 +386,6 @@ bool SmartpenSyncer::extractZip(QByteArray &zipData, QDir &dir)
zipFile.close();
}
- buffer.reset();
-
if (zip.getZipError() == UNZ_OK) {
return true;
} else {
@@ -363,3 +393,9 @@ bool SmartpenSyncer::extractZip(QByteArray &zipData, QDir &dir)
return false;
}
}
+
+void SmartpenSyncer::handleLinkError(const QString &msg)
+{
+ qWarning() << "link error:" << msg;
+ _errored = true;
+}
diff --git a/smartpensyncer.h b/smartpensyncer.h
index ea31917..9dae5c0 100644
--- a/smartpensyncer.h
+++ b/smartpensyncer.h
@@ -19,6 +19,7 @@
#ifndef SMARTPENSYNCER_H
#define SMARTPENSYNCER_H
+#include <atomic>
#include <QtCore/QThread>
#include <QtCore/QDir>
#include "smartpen.h"
@@ -26,20 +27,20 @@
class SmartpenSyncer : public QThread
{
Q_OBJECT
+
public:
explicit SmartpenSyncer(const Smartpen::Address &addr, QObject *parent = 0);
~SmartpenSyncer();
Smartpen::Address penAddress() const;
- QString penName() const;
-
bool hasErrors() const;
signals:
- void penNameChanged();
+ void gotPenName(const QString &name);
public slots:
void abort();
+ void reset();
private:
void run();
@@ -48,13 +49,16 @@ private:
bool syncPaperReplay(Smartpen::PenTime lastSync, const Smartpen::ChangeReport &change);
bool extractZip(QByteArray &zipData, QDir &dir);
+private slots:
+ void handleLinkError(const QString &msg);
+
private:
- Smartpen::Address _addr;
- Smartpen *_pen;
- bool _errored;
- bool _aborted;
+ const Smartpen::Address _addr;
+ std::atomic<bool> _errored;
+ std::atomic<bool> _aborted;
- QString _penSerial;
+ // To be used only from this thread
+ Smartpen *_pen;
QString _penName;
QDir _penDataDir;
};
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/stfgraphicsitem.h b/stfgraphicsitem.h
index 6f7038a..84c4d71 100644
--- a/stfgraphicsitem.h
+++ b/stfgraphicsitem.h
@@ -19,7 +19,7 @@
#ifndef STFGRAPHICSITEM_H
#define STFGRAPHICSITEM_H
-#include <QtGui/QGraphicsItem>
+#include <QtWidgets/QGraphicsItem>
#include "paperreplay.h"
class StfGraphicsItem : public QGraphicsItem
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 e2a7714..a9ca693 100644
--- a/stfstrokeitem.cc
+++ b/stfstrokeitem.cc
@@ -19,8 +19,8 @@
#include <QtCore/QDebug>
#include <QtGui/QPen>
#include <QtGui/QCursor>
-#include <QtGui/QGraphicsScene>
-#include <QtGui/QGraphicsSceneMouseEvent>
+#include <QtWidgets/QGraphicsScene>
+#include <QtWidgets/QGraphicsSceneMouseEvent>
#include "notebookview.h"
#include "stfstrokeitem.h"
@@ -28,10 +28,12 @@ StfStrokeItem::StfStrokeItem(const QPainterPath &stroke, const PaperReplay::Sess
: QGraphicsPathItem(stroke, parent), _session(session),
_startTime(startTime), _endTime(endTime)
{
- if (_session.isValid()) {
- setPen(QPen(Qt::darkGreen));
+ QPen pen(Qt::black, 8.0f);
+ if (hasPaperReplay()) {
+ pen.setColor(Qt::darkGreen);
setCursor(Qt::PointingHandCursor);
}
+ setPen(pen);
}
int StfStrokeItem::type() const
@@ -39,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 5825c10..eaf8d8a 100644
--- a/stfstrokeitem.h
+++ b/stfstrokeitem.h
@@ -19,7 +19,7 @@
#ifndef STFSTROKEITEM_H
#define STFSTROKEITEM_H
-#include <QtGui/QGraphicsPathItem>
+#include <QtWidgets/QGraphicsPathItem>
#include "paperreplay.h"
class StfStrokeItem : public QGraphicsPathItem
@@ -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;