/* * scribiu -- read notebooks and voice memos from Livescribe pens * Copyright (C) 2015 Javier S. Pedro * * 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 . */ #include #include #include #include #include #include #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)), _player(new QMediaPlayer(this)), _replay(new PaperReplay(this)), _replayModel(new PaperReplayModel(_replay, this)), _statusLabel(new QLabel) { ui->setupUi(this); ui->notebookTree->setModel(_notebooks); 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->replaySlider->setSingleStep(5000 / PAPER_REPLAY_SLIDER_SCALE); ui->replaySlider->setPageStep(30000 / PAPER_REPLAY_SLIDER_SCALE); ui->paperReplayView->setModel(_replayModel); ui->paperReplayView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); ui->paperReplayView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed); ui->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()), this, SLOT(handlePensBeingSynchronizedChanged())); connect(_manager, SIGNAL(syncComplete(QString)), this, SLOT(handlePenSyncComplete(QString))); connect(_manager, SIGNAL(syncFailed(QString)), this, SLOT(handlePenSyncFailed(QString))); 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"); restoreGeometry(settings.value("geometry").toByteArray()); restoreState(settings.value("state").toByteArray()); ui->splitter->restoreState(settings.value("splitter").toByteArray()); settings.endGroup(); } MainWindow::~MainWindow() { delete ui; } void MainWindow::closeNotebook() { _curPenName.clear(); _curNotebookName.clear(); _replay->close(); ui->pane2Stack->setCurrentWidget(ui->notebookView); ui->notebookView->setNotebook(QString()); } void MainWindow::openNotebook(const QString &pen, const QString ¬ebook) { if (_curPenName == pen && _curNotebookName == notebook) return; closeNotebook(); _curPenName = pen; _curNotebookName = notebook; Smartpen::PenTime userTime = _notebooks->penUserTime(pen); if (_curNotebookName == PAPER_REPLAY) { QString replayDir = _notebooks->paperReplayDirectory(_curPenName); _replay->open(replayDir, PAPER_REPLAY_GUID, userTime); _replayModel->refresh(); 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->pane2Stack->setCurrentWidget(ui->notebookView); } } void MainWindow::exportCurrentPageAsPng(const QString &file) { qDebug() << "Exporting current page" << ui->notebookView->curPage() << "to" << file; 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)); } } void MainWindow::exportCurrentPageAsSvg(const QString &file) { QSvgGenerator svg; svg.setFileName(file); svg.setSize(ui->notebookView->getCurPageSize()); svg.setViewBox(ui->notebookView->getCurPageTrim()); svg.setTitle(_curNotebookName + " p." + QString::number(ui->notebookView->curPage())); svg.setDescription("Page " + QString::number(ui->notebookView->curPage()) + " of " + _curNotebookName + " from " + _curPenName); qDebug() << "Exporting current page" << ui->notebookView->curPage() << "to" << file; QPainter painter; painter.begin(&svg); ui->notebookView->renderPage(&painter, ui->notebookView->curPage()); 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 = currentPlayerMediaPath(); if (src.isNull()) { QMessageBox::warning(this, tr("Export audio"), tr("No audio file is selected")); return; } qDebug() << "Exporting current audio" << src << "to" << file; if (!QFile::copy(src, file)) { QMessageBox::warning(this, tr("Export audio"), tr("Could not export current audio to '%s'").arg(file)); } } void MainWindow::handleNotebookSelected(const QModelIndex &index) { if (!index.isValid()) { closeNotebook(); return; } QModelIndex parent = index.parent(); if (!parent.isValid()) { closeNotebook(); return; } // Get column 0, which corresponds to notebook name QModelIndex nb = _notebooks->index(index.row(), 0, parent); openNotebook(_notebooks->data(parent, NotebookModel::FileNameRole).toString(), _notebooks->data(nb, NotebookModel::FileNameRole).toString()); } void MainWindow::handleNotebookRowsInserted(const QModelIndex &index, int start, int end) { Q_UNUSED(index); Q_UNUSED(start); Q_UNUSED(end); QTimer::singleShot(200, ui->notebookTree, SLOT(expandAll())); } void MainWindow::handleCurPageChanged() { ui->pageEdit->setText(QString::number(ui->notebookView->curPage() + 1)); } void MainWindow::handlePaperReplaySelected(const QModelIndex &index) { QString file = _replayModel->sessionFilename(index); handlePaperReplayRequested(file, 0); } void MainWindow::handlePaperReplayRequested(const QString &file, qint64 time) { QFileInfo finfo(file); if (!finfo.exists()) { qWarning() << "Cannot open paper replay media file:" << finfo.canonicalFilePath(); } QString filePath = finfo.canonicalFilePath(); if (currentPlayerMediaPath() != filePath) { qDebug() << "requesting media " << filePath; _player->setMedia(QUrl::fromLocalFile(filePath)); } qDebug() << "requesting media seek to" << time << "/" << _player->duration(); if (_player->isSeekable()) { // Media is loaded and ready to go _pendingSeek = 0; _player->setPosition(time); } else { // Otherwise delay the seek until after media starts playing _pendingSeek = time; } _player->play(); } void MainWindow::handlePaperReplayPlay() { _player->play(); } void MainWindow::handlePaperReplayPause() { _player->pause(); } void MainWindow::handlePaperReplaySliderChanged(int value) { _player->setPosition(value * PAPER_REPLAY_SLIDER_SCALE); } void MainWindow::handlePlayerStateChanged(QMediaPlayer::State state) { switch (state) { case QMediaPlayer::PlayingState: ui->playButton->setVisible(false); ui->pauseButton->setVisible(true); break; case QMediaPlayer::PausedState: ui->playButton->setVisible(true); ui->pauseButton->setVisible(false); break; default: ui->playButton->setVisible(true); ui->pauseButton->setVisible(false); break; } } void MainWindow::handlePlayerDurationChanged(qint64 time) { ui->mediaLenLabel->setText("/ " + formatDuration(time)); ui->replaySlider->setMaximum(time / PAPER_REPLAY_SLIDER_SCALE); } 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() { QStringList pens = _manager->pensBeingSynchronized(); if (pens.isEmpty()) { _statusLabel->setText(QString()); } else { _statusLabel->setText(tr("Synchronizing %1...").arg(pens.join(", "))); } } void MainWindow::handlePenSyncComplete(const QString &penName) { ui->statusBar->showMessage(tr("Completed synchronization with %1").arg(penName), 10000); } void MainWindow::handlePenSyncFailed(const QString &penName) { ui->statusBar->showMessage(tr("Failed synchronization with %1").arg(penName), 10000); } 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"), dir, filters.join(";;"), &filter); if (fileName.isEmpty()) return; int filterIndex = filters.indexOf(filter); switch (filterIndex) { case 0: if (!fileName.endsWith(".aac", Qt::CaseInsensitive)) { fileName.append(".aac"); } 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)"); 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; filterIndex = filters.indexOf(filter); switch (filterIndex) { case 0: if (!fileName.endsWith(".png", Qt::CaseInsensitive)) { fileName.append(".png"); } exportCurrentPageAsPng(fileName); break; case 1: if (!fileName.endsWith(".svg", Qt::CaseInsensitive)) { fileName.append(".svg"); } 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")); } } void MainWindow::handleAbout() { QMessageBox::about(this, tr("About Scribiu"), tr("Read notebooks and audio notes from your Livescribe Echo pen")); } void MainWindow::closeEvent(QCloseEvent *event) { QSettings settings; Q_UNUSED(event); settings.beginGroup("mainwindow"); settings.setValue("geometry", saveGeometry()); settings.setValue("state", saveState()); settings.setValue("splitter", ui->splitter->saveState()); settings.endGroup(); } QString MainWindow::formatDuration(qint64 time) const { int secs = time / 1000; int mins = secs / 60; secs %= 60; int hours = mins / 60; mins %= 60; const QChar fill('0'); if (hours) { return QString("%1:%2:%3").arg(hours).arg(mins, 2, 10, fill).arg(secs, 2, 10, fill); } else { return QString("%2:%3").arg(mins).arg(secs, 2, 10, fill); } } QT_WARNING_DISABLE_DEPRECATED QString MainWindow::currentPlayerMediaPath() const { return _player->media().canonicalUrl().toLocalFile(); }