/* * 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 "paperreplay.h" #include "afdnotebook.h" #include "notebookmodel.h" #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()), _watcher() { if (!_dataDir.exists()) { if (!_dataDir.mkpath(".")) { qWarning() << "Cannot create my data directory:" << _dataDir.absolutePath(); } } _watcher.addPath(_dataDir.absolutePath()); connect(&_watcher, SIGNAL(directoryChanged(QString)), SLOT(handleChangedDirectory(QString))); refresh(); } QString NotebookModel::defaultDataDirectory() { QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); if (path.isEmpty()) { path = QDir::home().absoluteFilePath(".scribiu"); } return path; } QString NotebookModel::userDataDirectory() { QSettings settings; QString path = settings.value("data/directory").toString(); if (path.isEmpty()) { path = defaultDataDirectory(); settings.setValue("data/directory", QString()); } return path; } QString NotebookModel::penDirectory(const QString &name) const { return _dataDir.filePath(name); } NotebookModel::PenTime NotebookModel::penUserTime(const QString &name) const { QDir penDir = penDirectory(name); QFile userTimeFile(penDir.filePath(PEN_USER_TIME_FILE)); if (userTimeFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QString data = QString::fromUtf8(userTimeFile.readLine(32)); userTimeFile.close(); return data.toULongLong(); } qWarning() << "Could not read last user time for pen" << name << "; shown dates are likely to be off"; return 0; } QString NotebookModel::notebookDirectory(const QString &penName, const QString &nbName) const { return _dataDir.filePath(penName + "/" + nbName); } QString NotebookModel::notebookDirectory(const QModelIndex &index) const { if (!index.isValid()) return QString(); quint32 id = index.internalId(); if (id != PEN_INDEX_ID) { const QString &penName = _pens[id]; const QStringList ¬ebooks = _notebooks[penName]; const QString ¬ebookName = notebooks.at(index.row()); return notebookDirectory(penName, notebookName); } return QString(); } QString NotebookModel::paperReplayDirectory(const QString &name) const { return _dataDir.filePath(name + "/" + PAPER_REPLAY); } QVariant NotebookModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); quint32 id = index.internalId(); 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 penDisplayName(penName); } break; case Qt::DecorationRole: switch (index.column()) { case 0: return penIcon(penName); case 2: if (isPenLocked(penName)) { return QApplication::style()->standardIcon(QStyle::SP_BrowserReload); } break; } break; case FileNameRole: return penName; break; } } else { const QString &penName = _pens[id]; const QStringList ¬ebooks = _notebooks[penName]; const QString ¬ebookName = notebooks.at(index.row()); switch (role) { case Qt::DisplayRole: switch (index.column()) { case 0: return notebookDisplayName(penName, notebookName); case 1: if (notebookName != PAPER_REPLAY) { return estimatePagesOfNotebook(penName, notebookName); } break; } break; case Qt::DecorationRole: switch (index.column()) { case 0: return notebookIcon(penName, notebookName); case 1: return QVariant(); case 2: if (isNotebookLocked(penName, notebookName)) { return QApplication::style()->standardIcon(QStyle::SP_BrowserReload); } break; } break; case Qt::TextAlignmentRole: switch (index.column()) { case 0: return Qt::AlignLeft; case 1: case 2: return Qt::AlignCenter; } break; case FileNameRole: return notebookName; } } return QVariant(); } Qt::ItemFlags NotebookModel::flags(const QModelIndex &index) const { switch (index.column()) { case 0: return Qt::ItemIsSelectable | Qt::ItemIsEnabled; default: return Qt::ItemIsSelectable; } } QModelIndex NotebookModel::index(int row, int column, const QModelIndex &parent) const { if (parent.isValid()) { quint32 id = parent.internalId(); if (id == PEN_INDEX_ID) { // Pen index const quint32 penIndex = parent.row(); const QString &penName = _pens.at(penIndex); const QStringList ¬ebooks = _notebooks[penName]; if (row >= 0 && row < notebooks.size() && column >= 0 && column < NUM_COLUMNS) { return createIndex(row, column, penIndex); } else { return QModelIndex(); } } else { // Notebook index return QModelIndex(); // Nothing inside notebooks } } else if (row >= 0 && row < _pens.size() && column >= 0 && column < NUM_COLUMNS) { return createIndex(row, column, PEN_INDEX_ID); } else { return QModelIndex(); } } QModelIndex NotebookModel::parent(const QModelIndex &child) const { if (child.isValid()) { quint32 id = child.internalId(); if (id == PEN_INDEX_ID) { return QModelIndex(); // Pens have no parent } else { return createIndex(id, 0, PEN_INDEX_ID); } } else { return QModelIndex(); } } int NotebookModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { quint32 id = parent.internalId(); if (id == PEN_INDEX_ID) { const quint32 penIndex = parent.row(); const QString &penName = _pens.at(penIndex); const QStringList ¬ebooks = _notebooks[penName]; return notebooks.size(); } else { return 0; // Nothing inside notebooks } } else { return _pens.size(); } } int NotebookModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return NUM_COLUMNS; } void NotebookModel::refresh() { 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); if (comp == 0) { ++i; ++j; } else if (comp > 0) { /* _pens[i] > pens[j] */ beginInsertRows(QModelIndex(), i, i); _pens.insert(i, pens[j]); endInsertRows(); ++i; ++j; } else { /* _pens[i] < pens[j] */ beginRemoveRows(QModelIndex(), i, i); _notebooks.remove(_pens[i]); _pens.removeAt(i); endRemoveRows(); } } if (i < _pens.size()) { Q_ASSERT(j == pens.size()); beginRemoveRows(QModelIndex(), i, _pens.size() - 1); while (i < _pens.size()) { _notebooks.remove(_pens[i]); _pens.removeAt(i); } endRemoveRows(); } else if (j < pens.size()) { Q_ASSERT(i == _pens.size()); beginInsertRows(QModelIndex(), i, i + (pens.size() - j)); _pens.append(pens.mid(j)); endInsertRows(); } foreach (const QString &pen, _pens) { refreshPen(pen); } } void NotebookModel::refreshPen(const QString &name) { QDir penDir(penDirectory(name)); if (!penDir.exists()) { return; } _iconCache.clear(); _watcher.addPath(penDir.canonicalPath()); QStringList &curNotebooks = _notebooks[name]; 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()); emit dataChanged(penIndex, index(penIndex.row(), NUM_COLUMNS - 1, QModelIndex())); int i = 0, j = 0; while (i < curNotebooks.size() && j < diskNotebooks.size()) { int comp = QString::compare(curNotebooks[i], diskNotebooks[j], Qt::CaseInsensitive); if (comp == 0) { ++i; ++j; } else if (comp > 0) { /* _pens[i] > pens[j] */ beginInsertRows(penIndex, i, i); curNotebooks.insert(i, diskNotebooks[j]); endInsertRows(); ++i; ++j; } else { /* _pens[i] < pens[j] */ beginRemoveRows(penIndex, i, i); curNotebooks.removeAt(i); endRemoveRows(); } } if (i < curNotebooks.size()) { Q_ASSERT(j == diskNotebooks.size()); beginRemoveRows(penIndex, i, curNotebooks.size() - 1); curNotebooks.erase(curNotebooks.begin() + i, curNotebooks.end()); endRemoveRows(); } else if (j < diskNotebooks.size()) { Q_ASSERT(i == curNotebooks.size()); beginInsertRows(penIndex, i, i + (diskNotebooks.size() - j)); curNotebooks.append(diskNotebooks.mid(j)); endInsertRows(); } qDebug() << "Found" << curNotebooks.size() << "notebooks for pen" << name; } int NotebookModel::indexOfPen(const QString &name) { auto it = std::lower_bound(_pens.begin(), _pens.end(), name); if (it == _pens.end()) { return -1; } else { return it - _pens.begin(); } } QDir NotebookModel::penDir(const QString &pen) const { return QDir(penDirectory(pen)); } QDir NotebookModel::notebookDir(const QString &pen, const QString ¬ebook) const { return QDir(notebookDirectory(pen, notebook)); } QString NotebookModel::penDisplayName(const QString &pen) const { 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 ¬ebook) 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 ¬ebook) const { if (notebook == PAPER_REPLAY) { return QApplication::style()->standardIcon(QStyle::SP_MediaVolume); } QDir dir = notebookDir(pen, notebook); 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(PEN_SYNC_LOCK_FILE)) { return true; // TODO check if stale } else { return false; } } bool NotebookModel::isNotebookLocked(const QString &pen, const QString ¬ebook) const { QDir dir = notebookDir(pen, notebook); if (dir.exists(PEN_SYNC_LOCK_FILE)) { return true; } else { return false; } } bool NotebookModel::isPaperReplayLocked(const QString &pen) const { return isNotebookLocked(pen, PAPER_REPLAY); } int NotebookModel::estimatePagesOfNotebook(const QString &pen, const QString ¬ebook) const { QDir dataDir(notebookDirectory(pen, notebook) + "/data"); QStringList pages = dataDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); return pages.count(); } void NotebookModel::handleChangedDirectory(const QString &path) { qDebug() << "changed" << path; if (path == _dataDir.absolutePath()) { refresh(); } else if (path.endsWith(".pen")) { QFileInfo finfo(path); refreshPen(finfo.baseName()); } }