aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md23
-rw-r--r--main.cc2
-rw-r--r--mainwindow.cc122
-rw-r--r--mainwindow.h18
-rw-r--r--mainwindow.ui70
-rw-r--r--notebookmodel.cc2
-rw-r--r--notebookview.cc13
-rw-r--r--notebookview.h1
-rw-r--r--paperreplay.cc7
-rw-r--r--paperreplay.h3
-rw-r--r--paperreplaymodel.cc4
-rw-r--r--scribiu.pro9
-rw-r--r--smartpen.cc26
-rw-r--r--smartpen.h15
-rw-r--r--smartpenmanager.cc23
-rw-r--r--smartpenmanager.h1
-rw-r--r--smartpensyncer.cc10
-rw-r--r--stfexporter.cc178
-rw-r--r--stfexporter.h (renamed from stftxtexport.h)14
-rw-r--r--stfreader.cc18
-rw-r--r--stfstrokeitem.cc2
-rw-r--r--stftxtexport.cc90
22 files changed, 431 insertions, 220 deletions
diff --git a/README.md b/README.md
index e8b0fac..9510f99 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Screenshots
-![Screenshot](http://depot.javispedro.com/livescribe/scribiu1.png)
+![Screenshot](http://depot.javispedro.com/livescribe/scribiu-tutorial.png)
# About
@@ -10,16 +10,17 @@ It also allows you to export individual pages as PNG files or voice memos as AAC
# Requirements
-Scribiu requires Qt 5, including the core, gui, widgets, and svg modules.
-It also requires phonon, libudev, openobex (>=1.7), libusb (>=1.0) and QuaZip (1.0).
-Most of these should be packaged by your distribution.
+Scribiu requires Qt 5, including the core, gui, widgets, svg and multimedia modules.
+It also requires libudev, openobex (>=1.7.2), libusb (>=1.0) and QuaZip (1.0).
+Most of these should be packaged by your GNU/Linux distribution.
For example, on Ubuntu, these correspond with packages:
`
qtbase5-dev
libqt5svg5-dev
-libphonon4qt5-dev
+qtmultimedia5-dev
+libqt5multimedia5-plugins
libudev-dev
libopenobex2-dev
libusb-1.0-0-dev
@@ -27,7 +28,7 @@ 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
@@ -37,7 +38,7 @@ It may work with the Livescribe Sky if you get it to work with Echo Desktop, but
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.
@@ -50,9 +51,9 @@ These directories contain the raw notebook, stroke, and voice data as received f
Therefore, even if Scribiu fails to display a certain notebook, you may be able to use other Livescribe programs in order to view the synchronized data.
-We thank the authors of [libsmartpen](https://github.com/srwalter/libsmartpen)
-and [LibreScribe](https://github.com/dylanmtaylor/LibreScribe) because of their
-protocol reverse engineering efforts, specially regarding the STF data format,
+Many thanks to the authors of [libsmartpen](https://github.com/srwalter/libsmartpen)
+and [LibreScribe](https://github.com/dylanmtaylor/LibreScribe) for their efforts
+reverse engineering the protocols, specially regarding the STF data format,
which has saved me a lot of time.
# Tasks
@@ -62,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/main.cc b/main.cc
index 2aaa8a7..f3d6371 100644
--- a/main.cc
+++ b/main.cc
@@ -29,7 +29,7 @@ int main(int argc, char *argv[])
app.setOrganizationName("scribiu");
app.setOrganizationDomain("com.javispedro.scribiu");
app.setApplicationName("scribiu");
- app.setApplicationVersion("1.4");
+ app.setApplicationVersion("1.5");
QPixmapCache::setCacheLimit(100 * 1024);
diff --git a/mainwindow.cc b/mainwindow.cc
index e7a73fa..cf1ef1a 100644
--- a/mainwindow.cc
+++ b/mainwindow.cc
@@ -25,13 +25,14 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
+#define PAPER_REPLAY_SLIDER_SCALE 100LL /* in msec */
+
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
_notebooks(new NotebookModel(this)),
_manager(new SmartpenManager(this)),
- _media(new Phonon::MediaObject(this)),
- _mediaOutput(new Phonon::AudioOutput(this)),
+ _player(new QMediaPlayer(this)),
_replay(new PaperReplay(this)),
_replayModel(new PaperReplayModel(_replay, this)),
_statusLabel(new QLabel)
@@ -42,14 +43,15 @@ MainWindow::MainWindow(QWidget *parent) :
ui->notebookTree->header()->setSectionResizeMode(1, QHeaderView::Fixed);
ui->notebookTree->header()->setSectionResizeMode(2, QHeaderView::Fixed);
ui->notebookTree->expandAll();
+ ui->replaySlider->setSingleStep(5000 / PAPER_REPLAY_SLIDER_SCALE);
+ ui->replaySlider->setPageStep(30000 / PAPER_REPLAY_SLIDER_SCALE);
ui->paperReplayView->setModel(_replayModel);
ui->paperReplayView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
ui->paperReplayView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed);
- Phonon::createPath(_media, _mediaOutput);
- _media->setTickInterval(500);
- ui->replaySlider->setMediaObject(_media);
ui->pauseButton->setVisible(false);
ui->statusBar->addWidget(_statusLabel, 1);
+ _player->setAudioRole(QAudio::VideoRole);
+
connect(_notebooks, SIGNAL(rowsInserted(QModelIndex,int,int)),
this, SLOT(handleNotebookRowsInserted(QModelIndex,int,int)));
connect(_manager, SIGNAL(pensBeingSynchronizedChanged()),
@@ -58,12 +60,14 @@ MainWindow::MainWindow(QWidget *parent) :
this, SLOT(handlePenSyncComplete(QString)));
connect(_manager, SIGNAL(syncFailed(QString)),
this, SLOT(handlePenSyncFailed(QString)));
- connect(_media, SIGNAL(stateChanged(Phonon::State,Phonon::State)),
- this, SLOT(handleMediaStateChange(Phonon::State)));
- connect(_media, SIGNAL(totalTimeChanged(qint64)),
- this, SLOT(handleMediaTotalTimeChanged(qint64)));
- connect(_media, SIGNAL(tick(qint64)),
- this, SLOT(handleMediaTick(qint64)));
+ connect(_player, SIGNAL(stateChanged(QMediaPlayer::State)),
+ this, SLOT(handlePlayerStateChanged(QMediaPlayer::State)));
+ connect(_player, SIGNAL(durationChanged(qint64)),
+ this, SLOT(handlePlayerDurationChanged(qint64)));
+ connect(_player, SIGNAL(positionChanged(qint64)),
+ this, SLOT(handlePlayerPositionChanged(qint64)));
+ connect(_player, SIGNAL(seekableChanged(bool)),
+ this, SLOT(handlePlayerSeekableChanged(bool)));
QSettings settings;
settings.beginGroup("mainwindow");
@@ -154,10 +158,22 @@ void MainWindow::exportCurrentPageAsTXYP(const QString &file, bool 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;
@@ -217,75 +233,80 @@ void MainWindow::handlePaperReplayRequested(const QString &file, qint64 time)
QString filePath = finfo.canonicalFilePath();
- if (_media->currentSource().fileName() != filePath) {
+ if (currentPlayerMediaPath() != filePath) {
qDebug() << "requesting media " << filePath;
- _media->setCurrentSource(QUrl::fromLocalFile(filePath));
+ _player->setMedia(QUrl::fromLocalFile(filePath));
}
- qDebug() << "requesting media seek to" << time << "/" << _media->totalTime();
+ qDebug() << "requesting media seek to" << time << "/" << _player->duration();
- switch (_media->state()) {
- case Phonon::PlayingState:
- case Phonon::BufferingState:
- case Phonon::PausedState:
+ 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::handleMediaStateChange(Phonon::State state)
+void MainWindow::handlePaperReplaySliderChanged(int value)
+{
+ _player->setPosition(value * PAPER_REPLAY_SLIDER_SCALE);
+}
+
+void MainWindow::handlePlayerStateChanged(QMediaPlayer::State state)
{
switch (state) {
- case Phonon::PlayingState:
+ case QMediaPlayer::PlayingState:
ui->playButton->setVisible(false);
ui->pauseButton->setVisible(true);
- if (_pendingSeek) {
- qDebug() << "requesting (pending) media seek to" << _pendingSeek << "/" << _media->totalTime();
- _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()
@@ -349,6 +370,7 @@ void MainWindow::handleExport()
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)");
int filterIndex = settings.value("filetype").toInt();
QString filter = filters.value(filterIndex);
@@ -378,6 +400,12 @@ void MainWindow::handleExport()
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");
}
@@ -412,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;
@@ -427,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 dc06243..03479a2 100644
--- a/mainwindow.h
+++ b/mainwindow.h
@@ -21,8 +21,7 @@
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QLabel>
-#include <phonon/MediaObject>
-#include <phonon/AudioOutput>
+#include <QtMultimedia/QMediaPlayer>
#include "notebookmodel.h"
#include "paperreplaymodel.h"
#include "smartpenmanager.h"
@@ -46,6 +45,7 @@ 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:
@@ -56,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);
@@ -69,15 +71,15 @@ protected:
void closeEvent(QCloseEvent *event);
private:
- QString formatDuration(qint64 time);
+ QString formatDuration(qint64 time) const;
+ QString currentPlayerMediaPath() const;
private:
Ui::MainWindow *ui;
NotebookModel *_notebooks;
SmartpenManager *_manager;
- Phonon::MediaObject *_media;
- Phonon::AudioOutput *_mediaOutput;
+ QMediaPlayer *_player;
qint64 _pendingSeek;
QString _curPenName;
diff --git a/mainwindow.ui b/mainwindow.ui
index a56ffba..52a8891 100644
--- a/mainwindow.ui
+++ b/mainwindow.ui
@@ -56,7 +56,7 @@
<bool>false</bool>
</attribute>
<attribute name="headerDefaultSectionSize">
- <number>29</number>
+ <number>41</number>
</attribute>
<attribute name="headerStretchLastSection">
<bool>false</bool>
@@ -147,7 +147,7 @@
</property>
</spacer>
</item>
- <item>
+ <item alignment="Qt::AlignVCenter">
<widget class="QLabel" name="zoomLabel">
<property name="text">
<string>Zoom:</string>
@@ -157,7 +157,7 @@
</property>
</widget>
</item>
- <item>
+ <item alignment="Qt::AlignVCenter">
<widget class="QSlider" name="zoomSlider">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@@ -259,10 +259,27 @@
</widget>
</item>
<item>
- <widget class="Phonon::SeekSlider" name="replaySlider"/>
+ <widget class="QSlider" name="replaySlider">
+ <property name="maximum">
+ <number>0</number>
+ </property>
+ <property name="tracking">
+ <bool>false</bool>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="tickInterval">
+ <number>1</number>
+ </property>
+ </widget>
</item>
<item>
- <widget class="QLabel" name="mediaPosLabel"/>
+ <widget class="QLabel" name="mediaPosLabel">
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
</item>
<item>
<widget class="QLabel" name="mediaLenLabel">
@@ -285,7 +302,7 @@
<x>0</x>
<y>0</y>
<width>718</width>
- <height>27</height>
+ <height>23</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@@ -346,12 +363,6 @@
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
- <class>Phonon::SeekSlider</class>
- <extends>QWidget</extends>
- <header location="global">phonon/seekslider.h</header>
- <container>1</container>
- </customwidget>
- <customwidget>
<class>NotebookView</class>
<extends>QGraphicsView</extends>
<header>notebookview.h</header>
@@ -406,7 +417,7 @@
<slot>handlePaperReplayPlay()</slot>
<hints>
<hint type="sourcelabel">
- <x>357</x>
+ <x>331</x>
<y>356</y>
</hint>
<hint type="destinationlabel">
@@ -422,7 +433,7 @@
<slot>handlePaperReplayPause()</slot>
<hints>
<hint type="sourcelabel">
- <x>391</x>
+ <x>365</x>
<y>356</y>
</hint>
<hint type="destinationlabel">
@@ -502,8 +513,8 @@
<slot>prevPage()</slot>
<hints>
<hint type="sourcelabel">
- <x>347</x>
- <y>46</y>
+ <x>331</x>
+ <y>61</y>
</hint>
<hint type="destinationlabel">
<x>441</x>
@@ -518,8 +529,8 @@
<slot>nextPage()</slot>
<hints>
<hint type="sourcelabel">
- <x>436</x>
- <y>38</y>
+ <x>421</x>
+ <y>61</y>
</hint>
<hint type="destinationlabel">
<x>473</x>
@@ -566,8 +577,8 @@
<slot>handlePaperReplaySelected(QModelIndex)</slot>
<hints>
<hint type="sourcelabel">
- <x>367</x>
- <y>117</y>
+ <x>402</x>
+ <y>95</y>
</hint>
<hint type="destinationlabel">
<x>778</x>
@@ -586,7 +597,7 @@
<y>116</y>
</hint>
<hint type="destinationlabel">
- <x>613</x>
+ <x>715</x>
<y>62</y>
</hint>
</hints>
@@ -607,6 +618,22 @@
</hint>
</hints>
</connection>
+ <connection>
+ <sender>replaySlider</sender>
+ <signal>valueChanged(int)</signal>
+ <receiver>MainWindow</receiver>
+ <slot>handlePaperReplaySliderChanged(int)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>416</x>
+ <y>339</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>681</x>
+ <y>300</y>
+ </hint>
+ </hints>
+ </connection>
</connections>
<slots>
<slot>handleNotebookSelected(QModelIndex)</slot>
@@ -616,6 +643,7 @@
<slot>handlePaperReplayPlay()</slot>
<slot>handlePaperReplayPause()</slot>
<slot>handlePaperReplaySelected(QModelIndex)</slot>
+ <slot>handlePaperReplaySliderChanged(int)</slot>
<slot>handleExport()</slot>
<slot>handleAbout()</slot>
</slots>
diff --git a/notebookmodel.cc b/notebookmodel.cc
index dc066de..9a51bf9 100644
--- a/notebookmodel.cc
+++ b/notebookmodel.cc
@@ -172,7 +172,7 @@ QVariant NotebookModel::data(const QModelIndex &index, int role) const
case Qt::TextAlignmentRole:
switch (index.column()) {
case 0:
- return Qt::AlignLeft;
+ return QVariant::fromValue<int>(Qt::AlignLeft | Qt::AlignVCenter);
case 1:
case 2:
return Qt::AlignCenter;
diff --git a/notebookview.cc b/notebookview.cc
index 06d035b..f8394c0 100644
--- a/notebookview.cc
+++ b/notebookview.cc
@@ -18,7 +18,7 @@
#include <QtCore/QDebug>
#include <QtGui/QResizeEvent>
-#include "stftxtexport.h"
+#include "stfexporter.h"
#include "notebookview.h"
#define VIEW_MARGIN 2
@@ -152,10 +152,16 @@ void NotebookView::renderPage(QPainter *painter, int pageNum, const QRectF &targ
void NotebookView::exportPageAsTXYP(QIODevice *device, int pageNum, bool relativeTime)
{
- StfTxtExport writer(_nb);
+ 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);
@@ -294,7 +300,8 @@ void NotebookView::calculateScale()
void NotebookView::layoutPages()
{
- const int numRows = (_pages.size() + 1) / _numColumns;
+ const int numRows = _pages.size() / _numColumns
+ + (_pages.size() % _numColumns > 0 ? 1 : 0);
const QSizeF pageSpace( _maxPageSize.width() + PAGE_SEPARATION,
_maxPageSize.height() + PAGE_SEPARATION);
diff --git a/notebookview.h b/notebookview.h
index cced421..7d62fab 100644
--- a/notebookview.h
+++ b/notebookview.h
@@ -60,6 +60,7 @@ public:
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);
diff --git a/paperreplay.cc b/paperreplay.cc
index c7a7f49..9f0a7f6 100644
--- a/paperreplay.cc
+++ b/paperreplay.cc
@@ -97,7 +97,12 @@ 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;
}
diff --git a/paperreplay.h b/paperreplay.h
index 62a75a6..5427dee 100644
--- a/paperreplay.h
+++ b/paperreplay.h
@@ -63,12 +63,13 @@ public:
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(SessionId id);
diff --git a/paperreplaymodel.cc b/paperreplaymodel.cc
index 4c40f76..dcab5cf 100644
--- a/paperreplaymodel.cc
+++ b/paperreplaymodel.cc
@@ -98,7 +98,7 @@ void PaperReplayModel::refresh()
{
beginResetModel();
_sessions = _replay->sessions();
- std::sort(_sessions.begin(), _sessions.end(), PaperReplay::Session::startTimeLess);
+ std::sort(_sessions.begin(), _sessions.end(), PaperReplay::Session::compareByStartTime);
endResetModel();
}
@@ -116,7 +116,7 @@ QString PaperReplayModel::getSessionName(const PaperReplay::Session &session) co
QString PaperReplayModel::getSessionLength(const PaperReplay::Session &session) const
{
- qint64 msecs = session.endTime() - session.startTime();
+ quint64 msecs = session.duration();
uint secs = msecs / 1000;
uint mins = secs / 60;
secs %= 60;
diff --git a/scribiu.pro b/scribiu.pro
index c47865e..0c68fa4 100644
--- a/scribiu.pro
+++ b/scribiu.pro
@@ -1,9 +1,8 @@
TARGET = scribiu
-VERSION = 1.4
+VERSION = 1.5.3
TEMPLATE = app
-QT += core gui widgets svg
-QT += phonon4qt5
+QT += core gui widgets svg multimedia
CONFIG += c++11
@@ -24,12 +23,12 @@ SOURCES += main.cc \
notebookmodel.cc \
smartpensyncer.cc \
smartpen.cc bitreader.cc stfreader.cc \
+ stfexporter.cc \
xmlutils.cc \
notebookview.cc \
afdnotebook.cc \
pageitem.cc \
stfgraphicsitem.cc \
- stftxtexport.cc \
paperreplay.cc \
afdpageaddress.cc \
stfstrokeitem.cc \
@@ -40,12 +39,12 @@ HEADERS += mainwindow.h \
notebookmodel.h \
smartpensyncer.h \
smartpen.h bitreader.h stfreader.h \
+ stfexporter.h \
xmlutils.h \
notebookview.h \
afdnotebook.h \
pageitem.h \
stfgraphicsitem.h \
- stftxtexport.h \
paperreplay.h \
afdpageaddress.h \
stfstrokeitem.h \
diff --git a/smartpen.cc b/smartpen.cc
index 686170d..12e2ac2 100644
--- a/smartpen.cc
+++ b/smartpen.cc
@@ -87,7 +87,7 @@ QByteArray Smartpen::getObject(const QString &name)
QByteArray Smartpen::getParameter(Parameter parameter)
{
- QString objectName = QString("ppdata?key=pp%1").arg(uint(parameter), 4, 16);
+ QString objectName = QString("ppdata?key=pp%1").arg(uint(parameter), 4, 16, QChar('0'));
QByteArray data = getObject(objectName);
QXmlStreamReader r(data);
@@ -109,14 +109,34 @@ QByteArray Smartpen::getParameter(Parameter parameter)
return QByteArray();
}
+Smartpen::PenId Smartpen::getPenId()
+{
+ PenId id;
+ QByteArray value = getParameter(Parameter::Id);
+ if (value.isEmpty() || value.size() != 1 + sizeof(Smartpen::PenId)) {
+ qWarning() << "got invalid value for pen id: " << value.toHex();
+ return 0;
+ }
+ QDataStream ds(value);
+ ds.setByteOrder(QDataStream::BigEndian);
+ ds.skipRawData(1); // Unclear what first byte is
+ ds >> id;
+ return id;
+}
+
+QString Smartpen::getPenSerial()
+{
+ return toPenSerial(getPenId());
+}
+
QString Smartpen::getPenName()
{
- return QString::fromUtf8(getParameter(PenName));
+ return QString::fromUtf8(getParameter(Parameter::Name));
}
Smartpen::PenTime Smartpen::getPenTime(Parameter parameter)
{
- Smartpen::PenTime time;
+ PenTime time;
QByteArray value = getParameter(parameter);
if (value.isEmpty() || value.size() != sizeof(Smartpen::PenTime)) {
qWarning() << "got invalid value for pen time: " << value.toHex();
diff --git a/smartpen.h b/smartpen.h
index 2ff64e3..e3dadad 100644
--- a/smartpen.h
+++ b/smartpen.h
@@ -45,18 +45,21 @@ public:
bool isConnected() const;
- enum Parameter : quint16 {
- /// The offset between the PenTime (see below) and the user's configured time. This value is static.
- PenUserTime = 0x8003,
+ 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
- PenRtcTime = 0x8004,
- PenType = 0x8006,
- PenName = 0x8011
+ RtcTime = 0x8004,
+ Type = 0x8006,
+ Name = 0x8011
};
QByteArray getObject(const QString& name);
QByteArray getParameter(Parameter parameter);
+ PenId getPenId();
+ QString getPenSerial();
QString getPenName();
PenTime getPenTime(Parameter parameter);
diff --git a/smartpenmanager.cc b/smartpenmanager.cc
index 1eeb685..d375fea 100644
--- a/smartpenmanager.cc
+++ b/smartpenmanager.cc
@@ -197,9 +197,8 @@ void SmartpenManager::handleTimerNextTry()
void SmartpenManager::processDeviceAdded(udev_device *dev)
{
- uint busnum = atol(udev_device_get_sysattr_value(dev, "busnum"));
- uint devnum = atol(udev_device_get_sysattr_value(dev, "devnum"));
- Smartpen::Address addr(busnum, devnum);
+ Smartpen::Address addr = getDeviceAddress(dev);
+ if (!addr.first && !addr.second) return;
QString name = parseUdevEscapedString(udev_device_get_property_value(dev, "ID_MODEL_ENC"));
@@ -223,9 +222,8 @@ void SmartpenManager::processDeviceAdded(udev_device *dev)
void SmartpenManager::processDeviceRemoved(udev_device *dev)
{
- uint busnum = atol(udev_device_get_sysattr_value(dev, "busnum"));
- uint devnum = atol(udev_device_get_sysattr_value(dev, "devnum"));
- Smartpen::Address addr(busnum, devnum);
+ Smartpen::Address addr = getDeviceAddress(dev);
+ if (!addr.first && !addr.second) return;
qDebug() << "Device removed with address:" << addr;
@@ -325,3 +323,16 @@ QString SmartpenManager::parseUdevEscapedString(const char *s)
}
return r;
}
+
+Smartpen::Address SmartpenManager::getDeviceAddress(udev_device *dev)
+{
+ const char *busnumS = udev_device_get_property_value(dev, "BUSNUM");
+ const char *devnumS = udev_device_get_property_value(dev, "DEVNUM");
+ if (!busnumS || !devnumS) {
+ qWarning() << "Cannot find busnum/devnum env var for udev device";
+ return Smartpen::Address();
+ }
+ uint busnum = atol(busnumS);
+ uint devnum = atol(devnumS);
+ return Smartpen::Address(busnum, devnum);
+}
diff --git a/smartpenmanager.h b/smartpenmanager.h
index a823709..6a5e39f 100644
--- a/smartpenmanager.h
+++ b/smartpenmanager.h
@@ -61,6 +61,7 @@ private:
void scheduleNextTry();
static QString parseUdevEscapedString(const char *s);
+ static Smartpen::Address getDeviceAddress(udev_device *dev);
private:
udev *_udev;
diff --git a/smartpensyncer.cc b/smartpensyncer.cc
index 794b7c5..3556f77 100644
--- a/smartpensyncer.cc
+++ b/smartpensyncer.cc
@@ -181,6 +181,12 @@ void SmartpenSyncer::run()
_penName = _pen->getPenName();
qDebug() << "got pen name:" << _penName;
+
+ if (_penName.isEmpty()) {
+ _penName = _pen->getPenSerial();
+ qDebug() << "pen with no name, using pen serial instead:" << _penName;
+ }
+
emit gotPenName(_penName);
QVariantMap penInfo = _pen->getPenInfo();
@@ -222,11 +228,11 @@ bool SmartpenSyncer::syncPen()
// 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::PenUserTime);
+ 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::PenRtcTime);
+ 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)
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/stftxtexport.h b/stfexporter.h
index b9ca821..c67a75c 100644
--- a/stftxtexport.h
+++ b/stfexporter.h
@@ -16,20 +16,24 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#ifndef STFTXTEXPORT_H
-#define STFTXTEXPORT_H
+#ifndef STFEXPORTER_H
+#define STFEXPORTER_H
#include "afdnotebook.h"
-class StfTxtExport
+class StfExporter
{
public:
- explicit StfTxtExport(AfdNotebook *nb);
+ 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 // STFTXTEXPORT_H
+#endif // STFEXPORTER_H
diff --git a/stfreader.cc b/stfreader.cc
index ad15b59..efe7412 100644
--- a/stfreader.cc
+++ b/stfreader.cc
@@ -45,7 +45,7 @@ StfReader::StrokeHandler::~StrokeHandler()
bool StfReader::parseV1(BitReader& br)
{
- quint64 cur_time = 0;
+ quint64 stroke_time = 0;
while (!br.atEnd()) {
syncV1(br);
@@ -76,18 +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);
- quint64 stroke_time = cur_time;
-
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) {
@@ -112,7 +112,7 @@ bool StfReader::parseV1(BitReader& br)
if (time == 0) {
if (handler) {
- bool res = handler->endStroke(stroke_time);
+ bool res = handler->endStroke(stroke_time + ((point_time * 1000ULL) / speed));
if (!res) return false;
}
break;
@@ -144,17 +144,17 @@ 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;
- stroke_time += time;
+ point_time += time;
if (handler) {
- bool res = handler->strokePoint(p0, f0, stroke_time);
+ bool res = handler->strokePoint(p0, f0, stroke_time + ((point_time * 1000ULL) / speed));
if (!res) return false;
}
}
diff --git a/stfstrokeitem.cc b/stfstrokeitem.cc
index 8fce6b7..a9ca693 100644
--- a/stfstrokeitem.cc
+++ b/stfstrokeitem.cc
@@ -54,7 +54,7 @@ void StfStrokeItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
PaperReplay::PenTime time = _startTime - _session.startTime();
if (time < 10) time = 0;
- qDebug() << "requesting paper replay at time" << time << "/" << (_session.endTime() - _session.startTime());
+ qDebug() << "requesting paper replay at time" << time << "/" << _session.duration();
nbview->requestPaperReplay(_session.fileName(), time);
event->accept();
diff --git a/stftxtexport.cc b/stftxtexport.cc
deleted file mode 100644
index e6793b2..0000000
--- a/stftxtexport.cc
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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 "stftxtexport.h"
-
-class StfToTXYP : public StfReader::StrokeHandler {
- QTextStream _out;
- QPoint _lastP;
- int _lastForce;
- qint64 _startTime;
- bool _relativeTime;
-
-public:
- StfToTXYP(QIODevice *out, bool relativeTime)
- : _out(out), _lastP(), _lastForce(0), _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';
- _lastP = p;
- _lastForce = force;
- return true;
- }
-
- bool strokePoint(const QPoint& p, int force, qint64 time) {
- _out << (time - _startTime) << '\t' << p.x() << '\t' << p.y() << '\t' << force << '\n';
- _lastP = p;
- _lastForce = force;
- return true;
- }
-
- bool endStroke(qint64 time) {
- // Ensure there is a entry with force=0, in case the pen didn't provide it
- if (_lastForce != 0) {
- _out << (time - _startTime) << '\t' << _lastP.x() << '\t' << _lastP.y() << '\t' << 0 << '\n';
- _lastForce = 0;
- }
- return true;
- }
-};
-
-StfTxtExport::StfTxtExport(AfdNotebook *nb)
- : _nb(nb)
-{
-}
-
-void StfTxtExport::exportToTXYP(QIODevice *out, int pageNum, bool relativeTime)
-{
- QStringList pens = _nb->penSerials();
- if (pens.isEmpty()) return;
-
- StfToTXYP h(out, relativeTime);
- StfReader r;
- r.setStrokeHandler(&h);
-
- QStringList strokeFiles = _nb->strokeFiles(pens.first(), 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;
- }
- }
-}