From 4b511c45c0f4cbf1fe087549cf8fe38e62cda07f Mon Sep 17 00:00:00 2001 From: Damien Caliste Date: Thu, 17 Sep 2015 20:02:01 +0200 Subject: [PATCH 1/7] Add thread infrastructure to retrieve a list of tags on every document returned by tracker. --- CMakeLists.txt | 1 + models/documentlistmodel.cpp | 34 +++++++- models/documentlistmodel.h | 5 ++ models/tagsthread.cpp | 163 +++++++++++++++++++++++++++++++++++ models/tagsthread.h | 54 ++++++++++++ 5 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 models/tagsthread.cpp create mode 100644 models/tagsthread.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 76e7b333..37ddd59d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ set(sailfishoffice_SRCS main.cpp dbusadaptor.cpp models/filtermodel.cpp + models/tagsthread.cpp models/documentlistmodel.cpp models/documentproviderlistmodel.cpp models/documentprovider.cpp diff --git a/models/documentlistmodel.cpp b/models/documentlistmodel.cpp index a31573e9..7f715c1b 100644 --- a/models/documentlistmodel.cpp +++ b/models/documentlistmodel.cpp @@ -28,6 +28,8 @@ struct DocumentListModelEntry int fileSize; QDateTime fileRead; QString mimeType; + QList tags; + TagsThreadJob *job; DocumentListModel::DocumentClass documentClass; bool dirty; // When true, should be removed from list. }; @@ -46,16 +48,20 @@ class DocumentListModel::Private roles.insert(FileDocumentClass, "fileDocumentClass"); } QList entries; - QHash roles; + QHash< int, QByteArray > roles; + TagsThread *tagsThread; }; DocumentListModel::DocumentListModel(QObject *parent) : QAbstractListModel(parent), d(new Private) { + d->tagsThread = new TagsThread( this ); + connect( d->tagsThread, &TagsThread::jobFinished, this, &DocumentListModel::jobFinished ); } DocumentListModel::~DocumentListModel() { + delete d->tagsThread; } QVariant DocumentListModel::data(const QModelIndex &index, int role) const @@ -129,6 +135,9 @@ void DocumentListModel::addItem(QString name, QString path, QString type, int si entry.fileRead = lastRead; entry.mimeType = mimeType; entry.documentClass = static_cast(mimeTypeToDocumentClass(mimeType)); + entry.job = new TagsThreadJob(path); + //entry.job.setTarget(entry.tags); + d->tagsThread->queueJob(entry.job); int index = 0; for (; index < d->entries.count(); ++index) { @@ -145,9 +154,7 @@ void DocumentListModel::removeItemsDirty() { for (int index=0; index < d->entries.count(); index++) { if (d->entries.at(index).dirty) { - beginRemoveRows(QModelIndex(), index, index); - d->entries.removeAt(index); - endRemoveRows(); + removeAt(index); } } } @@ -156,6 +163,7 @@ void DocumentListModel::removeItemsDirty() void DocumentListModel::removeAt(int index) { if (index > -1 && index < d->entries.count()) { + d->tagsThread->cancelJob(d->entries.at(index).job); beginRemoveRows(QModelIndex(), index, index); d->entries.removeAt(index); endRemoveRows(); @@ -164,11 +172,29 @@ void DocumentListModel::removeAt(int index) void DocumentListModel::clear() { + d->tagsThread->cancelJob(0); beginResetModel(); d->entries.clear(); endResetModel(); } +void DocumentListModel::jobFinished(TagsThreadJob *job) +{ + int index = 0; + for(QList::iterator entry = d->entries.begin(); + entry != d->entries.end(); entry++) { + if (entry->filePath == job->path) { + // Notify tags ready at index. + entry->job = 0; + entry->tags = job->tags; + fprintf(stdout, "Ok, copy tags for index %d\n", index); + break; + } + index += 1; + } + job->deleteLater(); +} + int DocumentListModel::mimeTypeToDocumentClass(QString mimeType) const { DocumentClass documentClass = UnknownDocument; diff --git a/models/documentlistmodel.h b/models/documentlistmodel.h index a8d2dfe7..c582dad1 100644 --- a/models/documentlistmodel.h +++ b/models/documentlistmodel.h @@ -22,6 +22,8 @@ #include #include +#include "tagsthread.h" + class DocumentListModelPrivate; class DocumentListModel : public QAbstractListModel @@ -66,6 +68,9 @@ class DocumentListModel : public QAbstractListModel Q_INVOKABLE int mimeTypeToDocumentClass(QString mimeType) const; +public Q_SLOTS: + void jobFinished(TagsThreadJob* job); + private: class Private; const QScopedPointer d; diff --git a/models/tagsthread.cpp b/models/tagsthread.cpp new file mode 100644 index 00000000..a553c5fd --- /dev/null +++ b/models/tagsthread.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2015 Damien Caliste + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "tagsthread.h" + +#include +#include +#include +#include +#include + +class TagsThreadQueue; + +const QEvent::Type Event_JobPending = QEvent::Type(QEvent::User + 1); + +class Thread : public QThread +{ + Q_OBJECT; +public: + Thread() + : jobQueue(0) + { + } + + void run() { + QThread::exec(); + deleteLater(); + } + + bool event(QEvent *e) { + // intercept deleteLater to wait for this thread to be + // finished. Called on the GUI thread. + if (e->type() == QEvent::DeferredDelete) + wait(); + return QThread::event(e); + } + + TagsThreadQueue *jobQueue; + QMutex mutex; +}; + +class TagsThreadPrivate +{ +public: + TagsThreadPrivate() { } + + Thread *thread; +}; + +class TagsThreadQueue : public QObject, public QQueue< TagsThreadJob* > +{ +public: + TagsThread *tagsThread; +protected: + bool event(QEvent *); + void processPendingJob(); +}; + +TagsThread::TagsThread(QObject* parent) + : QObject( parent ), priv( new TagsThreadPrivate() ) +{ + priv->thread = new Thread(); + priv->thread->jobQueue = new TagsThreadQueue(); + priv->thread->jobQueue->tagsThread = this; + priv->thread->start(); + priv->thread->jobQueue->moveToThread(priv->thread); +} + +TagsThread::~TagsThread() +{ + // Cancel outstanding render jobs and schedule the queue + // for deletion. Also set the jobQueue to 0 so we don't + // end up calling back to the now deleted documents object. + cancelJob(0); + priv->thread->mutex.lock(); + priv->thread->jobQueue->deleteLater(); + priv->thread->jobQueue = 0; + priv->thread->mutex.unlock(); + priv->thread->exit(); + + delete priv; +} + +void TagsThread::queueJob(TagsThreadJob *job) +{ + QMutexLocker locker{ &priv->thread->mutex }; + job->moveToThread( priv->thread ); + priv->thread->jobQueue->enqueue( job ); + QCoreApplication::postEvent(priv->thread->jobQueue, new QEvent(Event_JobPending)); +} + +void TagsThread::cancelJob(TagsThreadJob *job) +{ + QMutexLocker locker{ &priv->thread->mutex }; + for (QList::iterator it = priv->thread->jobQueue->begin(); it != priv->thread->jobQueue->end(); ) { + TagsThreadJob *j = *it; + if (!job || j == job) { + it = priv->thread->jobQueue->erase(it); + j->deleteLater(); + if (job) { + continue; // to skip the ++it at the end of the loop + } else { + return; + } + } + ++it; + } +} + +void TagsThreadQueue::processPendingJob() +{ + Thread *t = qobject_cast(QThread::currentThread()); + + QMutexLocker locker{ &t->mutex }; + if (!t->jobQueue || count() == 0) + return; + + TagsThreadJob* job = dequeue(); + locker.unlock(); + + // Retrieve tags here + //job->run(); + + locker.relock(); + + if (!qobject_cast(QThread::currentThread())->jobQueue) { + delete job; + return; + } + + emit tagsThread->jobFinished(job); +} + +bool TagsThreadQueue::event(QEvent *e) +{ + if (e->type() == Event_JobPending) { + processPendingJob(); + return true; + } + return QObject::event(e); +} + +TagsThreadJob::~TagsThreadJob() +{ + fprintf(stdout, "killing job for %s\n", path.toLocal8Bit().data()); +} + +#include "tagsthread.moc" diff --git a/models/tagsthread.h b/models/tagsthread.h new file mode 100644 index 00000000..8ec54fd5 --- /dev/null +++ b/models/tagsthread.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2015 Damien Caliste + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TAGSTHREAD_H +#define TAGSTHREAD_H + +#include + +class TagsThreadJob : public QObject +{ + Q_OBJECT +public: + TagsThreadJob( QString &path ): path(path) {}; + ~TagsThreadJob(); + + QList tags; + QString path; +}; + +class TagsThreadPrivate; +class TagsThread : public QObject +{ + Q_OBJECT +public: + TagsThread( QObject* parent = 0 ); + ~TagsThread(); + + void queueJob( TagsThreadJob *job ); + void cancelJob( TagsThreadJob *job ); + +Q_SIGNALS: + void jobFinished( TagsThreadJob *job ); + +private: + friend class TagsThreadPrivate; + TagsThreadPrivate * const priv; +}; + +#endif // TAGSTHREAD_H From d104124fa8e7641e4d06c550e5c19bf30cf97f12 Mon Sep 17 00:00:00 2001 From: Damien Caliste Date: Thu, 17 Sep 2015 20:49:25 +0200 Subject: [PATCH 2/7] Begin to implement tag filtering. --- CMakeLists.txt | 3 + main.cpp | 2 + models/documentlistmodel.cpp | 71 +++++++++++++++-- models/documentlistmodel.h | 11 +++ models/filtermodel.cpp | 33 ++++++++ models/filtermodel.h | 11 +++ models/taglistmodel.cpp | 132 ++++++++++++++++++++++++++++++++ models/taglistmodel.h | 58 ++++++++++++++ models/tagsthread.cpp | 10 ++- models/tagsthread.h | 2 + qml/FileListPage.qml | 74 +++++++++++++++++- qml/Tag.qml | 143 +++++++++++++++++++++++++++++++++++ qml/TagsSelector.qml | 140 ++++++++++++++++++++++++++++++++++ 13 files changed, 679 insertions(+), 11 deletions(-) create mode 100644 models/taglistmodel.cpp create mode 100644 models/taglistmodel.h create mode 100644 qml/Tag.qml create mode 100644 qml/TagsSelector.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 37ddd59d..c8216a34 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ set(sailfishoffice_SRCS dbusadaptor.cpp models/filtermodel.cpp models/tagsthread.cpp + models/taglistmodel.cpp models/documentlistmodel.cpp models/documentproviderlistmodel.cpp models/documentprovider.cpp @@ -45,6 +46,8 @@ set(sailfishoffice_SRCS set(sailfishoffice_QML_SRCS qml/CoverPage.qml + qml/Tag.qml + qml/TagsSelector.qml qml/FileListPage.qml qml/Main.qml qml/CoverFileItem.qml diff --git a/main.cpp b/main.cpp index ee954450..aef93df2 100644 --- a/main.cpp +++ b/main.cpp @@ -33,6 +33,7 @@ #include #include "config.h" +#include "models/taglistmodel.h" #include "models/filtermodel.h" #include "models/documentlistmodel.h" #include "models/trackerdocumentprovider.h" @@ -66,6 +67,7 @@ QSharedPointer createView(const QString &file) qmlRegisterType("Sailfish.Office.Files", 1, 0, "DocumentListModel"); qmlRegisterType("Sailfish.Office.Files", 1, 0, "DocumentProviderListModel"); qmlRegisterType("Sailfish.Office.Files", 1, 0, "TrackerDocumentProvider"); + qmlRegisterType("Sailfish.Office.Files", 1, 0, "TagListModel"); qmlRegisterType("Sailfish.Office.Files", 1, 0, "FilterModel"); qmlRegisterInterface("DocumentProvider"); diff --git a/models/documentlistmodel.cpp b/models/documentlistmodel.cpp index 7f715c1b..1a8bd8ca 100644 --- a/models/documentlistmodel.cpp +++ b/models/documentlistmodel.cpp @@ -19,6 +19,7 @@ #include "documentlistmodel.h" #include +#include struct DocumentListModelEntry { @@ -28,7 +29,7 @@ struct DocumentListModelEntry int fileSize; QDateTime fileRead; QString mimeType; - QList tags; + QSet tags; TagsThreadJob *job; DocumentListModel::DocumentClass documentClass; bool dirty; // When true, should be removed from list. @@ -50,6 +51,7 @@ class DocumentListModel::Private QList entries; QHash< int, QByteArray > roles; TagsThread *tagsThread; + TagListModel tagsModel; }; DocumentListModel::DocumentListModel(QObject *parent) @@ -90,6 +92,59 @@ QVariant DocumentListModel::data(const QModelIndex &index, int role) const return QVariant(); } +bool DocumentListModel::hasTagAt(int row, const QString &tag) const +{ + if (row < 0 && row >= d->entries.count()) + return false; + + return d->entries.at(row).tags.contains(tag); +} +bool DocumentListModel::hasTag(const QString &path, const QString &tag) const +{ + for (QList::iterator entry = d->entries.begin(); + entry != d->entries.end(); entry++) { + if ( path == entry->filePath ) + return entry->tags.contains(tag); + } + + return false; +} +void DocumentListModel::addTag(const QString &path, const QString &tag) +{ + int row = 0; + for (QList::iterator entry = d->entries.begin(); + entry != d->entries.end(); entry++) { + if ( path == entry->filePath ) { + if ( !entry->tags.contains(tag) ) { + entry->tags.insert(tag); + dataChanged(index(row), index(row)); + d->tagsModel.addItem(tag); + } + return; + } + row += 1; + } +} +void DocumentListModel::removeTag(const QString &path, const QString &tag) +{ + int row = 0; + for (QList::iterator entry = d->entries.begin(); + entry != d->entries.end(); entry++) { + if ( path == entry->filePath ) { + if ( entry->tags.contains(tag) ) { + entry->tags.remove(tag); + dataChanged(index(row), index(row)); + d->tagsModel.removeItem(tag); + } + return; + } + row += 1; + } +} +TagListModel* DocumentListModel::tags() const +{ + return &d->tagsModel; +} int DocumentListModel::rowCount(const QModelIndex& parent) const { @@ -180,17 +235,21 @@ void DocumentListModel::clear() void DocumentListModel::jobFinished(TagsThreadJob *job) { - int index = 0; + int row = 0; for(QList::iterator entry = d->entries.begin(); entry != d->entries.end(); entry++) { if (entry->filePath == job->path) { - // Notify tags ready at index. entry->job = 0; - entry->tags = job->tags; - fprintf(stdout, "Ok, copy tags for index %d\n", index); + entry->tags.clear(); + for (int i=0; i < job->tags.count(); i++) { + entry->tags.insert(job->tags.at(i)); + d->tagsModel.addItem(job->tags.at(i)); + } + dataChanged(index(row), index(row)); + fprintf(stdout, "Ok, copy tags for index %d\n", row); break; } - index += 1; + row += 1; } job->deleteLater(); } diff --git a/models/documentlistmodel.h b/models/documentlistmodel.h index c582dad1..328e2eb6 100644 --- a/models/documentlistmodel.h +++ b/models/documentlistmodel.h @@ -23,12 +23,14 @@ #include #include "tagsthread.h" +#include "taglistmodel.h" class DocumentListModelPrivate; class DocumentListModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(TagListModel* tags READ tags NOTIFY tagsChanged) public: enum DocumentClass { UnknownDocument, @@ -66,11 +68,20 @@ class DocumentListModel : public QAbstractListModel void removeAt(int index); void clear(); + TagListModel* tags() const; + Q_INVOKABLE int mimeTypeToDocumentClass(QString mimeType) const; + bool hasTagAt(int row, const QString &tag) const; + Q_INVOKABLE bool hasTag(const QString &path, const QString &tag) const; + Q_INVOKABLE void addTag(const QString &path, const QString &tag); + Q_INVOKABLE void removeTag(const QString &path, const QString &tag); public Q_SLOTS: void jobFinished(TagsThreadJob* job); +Q_SIGNALS: + void tagsChanged(); + private: class Private; const QScopedPointer d; diff --git a/models/filtermodel.cpp b/models/filtermodel.cpp index 56683cd8..639f458a 100644 --- a/models/filtermodel.cpp +++ b/models/filtermodel.cpp @@ -39,3 +39,36 @@ DocumentListModel* FilterModel::sourceModel() const { return static_cast(QSortFilterProxyModel::sourceModel()); } + +bool FilterModel::tagFiltered() const +{ + return !tags.empty(); +} +bool FilterModel::hasTag(const QString &tag) const +{ + return tags.contains(tag); +} +void FilterModel::addTag(const QString &tag) +{ + tags.insert(tag, true); + invalidateFilter(); + emit tagFilteringChanged(); +} +void FilterModel::removeTag(const QString &tag) +{ + tags.remove(tag); + invalidateFilter(); + emit tagFilteringChanged(); +} +bool FilterModel::filterAcceptsRow(int source_row, const QModelIndex & source_parent) const +{ + bool ret; + + ret = true; + for (QMap::const_iterator it = tags.begin(); + it != tags.end() && ret; it++) { + ret = sourceModel()->hasTagAt(source_row, it.key()); + } + + return ret && QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); +} diff --git a/models/filtermodel.h b/models/filtermodel.h index a9ec29ed..2b9f830f 100644 --- a/models/filtermodel.h +++ b/models/filtermodel.h @@ -27,18 +27,29 @@ class FilterModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(DocumentListModel *sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) + Q_PROPERTY(bool tagFiltered READ tagFiltered NOTIFY tagFilteringChanged) public: FilterModel(QObject *parent = 0); ~FilterModel(); + virtual bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const; + public: DocumentListModel* sourceModel() const; + bool tagFiltered() const; public Q_SLOTS: void setSourceModel(DocumentListModel *model); + bool hasTag(const QString &tag) const; + void addTag(const QString &tag); + void removeTag(const QString &tag); Q_SIGNALS: void sourceModelChanged(); + void tagFilteringChanged(); + +private: + QMap tags; }; #endif // FILTERMODEL_H diff --git a/models/taglistmodel.cpp b/models/taglistmodel.cpp new file mode 100644 index 00000000..857aa323 --- /dev/null +++ b/models/taglistmodel.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2015 Damien Caliste + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "taglistmodel.h" + +struct TagListModelEntry +{ + QString label; + unsigned int index; + unsigned int usage; +}; + +class TagListModel::Private +{ +public: + Private() { + roles.insert(TagLabelRole, "label"); + roles.insert(TagUsageRole, "usage"); + } + QList tags; + QHash roles; +}; + +TagListModel::TagListModel(QObject* parent) + : QAbstractListModel(parent), d(new Private) +{ +} + +TagListModel::~TagListModel() +{ +} + +QVariant TagListModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= d->tags.count()) + return QVariant(); + + switch (role) { + case TagLabelRole: + return d->tags.at(index.row()).label; + case TagUsageRole: + return d->tags.at(index.row()).usage; + default: + break; + } + + return QVariant(); +} + +int TagListModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return d->tags.count(); +} +int TagListModel::count() const +{ + return d->tags.count(); +} + +QHash< int, QByteArray > TagListModel::roleNames() const +{ + return d->roles; +} + +bool TagListModel::contains(const QString &tag) const +{ + for (QList::const_iterator entry = d->tags.begin(); + entry != d->tags.end(); entry++) { + if (entry->label == tag) + return true; + } + return false; +} + +void TagListModel::addItem(const QString &tag) +{ + for (QList::iterator entry = d->tags.begin(); + entry != d->tags.end(); entry++) { + if (QString::localeAwareCompare(entry->label, tag) == 0) { + entry->usage += 1; + dataChanged(index(entry->index), index(entry->index)); + return; + } + } + + TagListModelEntry entry; + entry.label = tag; + entry.usage = 1; + // Add the new tag in the alphabetic order. + for (entry.index = 0; entry.index < d->tags.count(); entry.index++) + if (QString::localeAwareCompare(d->tags.at(entry.index).label, tag) > 0) + break; + beginInsertRows(QModelIndex(), entry.index, entry.index); + d->tags.insert(entry.index, entry); + endInsertRows(); + emit countChanged(); +} + +void TagListModel::removeItem(const QString &tag) +{ + for(QList::iterator entry = d->tags.begin(); + entry != d->tags.end(); entry++) { + if (entry->label == tag) { + entry->usage -= 1; + if (entry->usage) { + dataChanged(index(entry->index), index(entry->index)); + } else { + beginRemoveRows(QModelIndex(), entry->index, entry->index); + d->tags.removeAt(entry->index); + endRemoveRows(); + emit countChanged(); + } + return; + } + } +} diff --git a/models/taglistmodel.h b/models/taglistmodel.h new file mode 100644 index 00000000..1faab63c --- /dev/null +++ b/models/taglistmodel.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 Damien Caliste + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TAGLISTMODEL_H +#define TAGLISTMODEL_H + +#include + +class TagListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) +public: + enum Roles + { + TagLabelRole = Qt::UserRole + 1, + TagUsageRole + }; + + TagListModel( QObject* parent = 0 ); + ~TagListModel(); + + TagListModel( const TagListModel& ) = delete; + TagListModel& operator=( const TagListModel& ) = delete; + + virtual QVariant data(const QModelIndex& index, int role) const; + virtual int rowCount(const QModelIndex& parent) const; + virtual QHash< int, QByteArray > roleNames() const; + int count() const; + + Q_INVOKABLE bool contains(const QString &tag) const; + void addItem(const QString &tag); + void removeItem(const QString &tag); + +Q_SIGNALS: + void countChanged(); + +private: + class Private; + const QScopedPointer< Private > d; +}; + +#endif // TAGLISTMODEL_H diff --git a/models/tagsthread.cpp b/models/tagsthread.cpp index a553c5fd..d6b12c6e 100644 --- a/models/tagsthread.cpp +++ b/models/tagsthread.cpp @@ -134,7 +134,7 @@ void TagsThreadQueue::processPendingJob() locker.unlock(); // Retrieve tags here - //job->run(); + job->run(); locker.relock(); @@ -160,4 +160,12 @@ TagsThreadJob::~TagsThreadJob() fprintf(stdout, "killing job for %s\n", path.toLocal8Bit().data()); } +void TagsThreadJob::run() +{ + if (!strstr(path.toLocal8Bit().data(), ".pdf")) { + tags.append(QString("PDF")); + } + tags.append(QString("newt")); +} + #include "tagsthread.moc" diff --git a/models/tagsthread.h b/models/tagsthread.h index 8ec54fd5..a88cc3c8 100644 --- a/models/tagsthread.h +++ b/models/tagsthread.h @@ -28,6 +28,8 @@ class TagsThreadJob : public QObject TagsThreadJob( QString &path ): path(path) {}; ~TagsThreadJob(); + void run(); + QList tags; QString path; }; diff --git a/qml/FileListPage.qml b/qml/FileListPage.qml index 1b2d85d9..e17aa2da 100644 --- a/qml/FileListPage.qml +++ b/qml/FileListPage.qml @@ -104,6 +104,41 @@ Page { value: 0 } } + + Flow { + width: parent.width - 2 * Theme.horizontalPageMargin + anchors.horizontalCenter: parent.horizontalCenter + opacity: filteredModel.tagFiltered ? 1.0 : 0.0 + visible: opacity > 0 + + spacing: Theme.paddingMedium + + Label { + //% "Filtered by: " + text: qsTrId("sailfish-office-lbl-tag-filter") + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeSmall + } + + Repeater { + id: tags + delegate: Tag { + id: tagDelegate + + enabled: false + tag: model.label + fontSize: Theme.fontSizeSmall + visible: selected + Connections { + target: filteredModel + onTagFilteringChanged: selected = filteredModel.hasTag(model.label) + } + } + model: page.model.tags + } + + Behavior on opacity { FadeAnimation { duration: 150 } } + } } Connections { @@ -128,6 +163,17 @@ Page { } } + MenuItem { + //: Tag filter menu entry + //% "Filter by tags" + text: qsTrId("sailfish-office-me-tag-filter") + enabled: page.model.tags.count > 0 + onClicked: pageStack.push("TagsSelector.qml", { + //% "Filtering tags" + title: qsTrId("sailfish-office-he-filtering-tags"), + model: page.model.tags, + highlight: filteredModel }) + } MenuItem { text: !menu._searchEnabled ? //% "Show search" qsTrId("sailfish-office-me-show_search") @@ -141,10 +187,11 @@ Page { parent: listView.contentItem y: listView.headerItem.y + listView.headerItem.height + Theme.paddingLarge //: View placeholder shown when there are no documents - //% "No documents" - text: searchText.length == 0 ? qsTrId("sailfish-office-la-no_documents") - : //% "No documents found" - qsTrId("sailfish-office-la-not-found") + text: (searchText.length == 0 && !filteredModel.tagFiltered) ? + //% "No documents" + qsTrId("sailfish-office-la-no_documents") + : //% "No documents found" + qsTrId("sailfish-office-la-not-found") visible: opacity > 0 opacity: listView.count > 0 ? 0.0 : 1.0 Behavior on opacity { FadeAnimation {} } @@ -251,6 +298,25 @@ Page { listItem.deleteFile() } } + MenuItem { + //% "Manage tags" + text: qsTrId("sailfish-office-me-tags") + onClicked: { + var Doc = function(model, path) { + this.model = model + this.path = path + this.hasTag = function(tag) { return this.model.hasTag(this.path, tag) } + this.addTag = function(tag) { this.model.addTag(this.path, tag) } + this.removeTag = function(tag) { this.model.removeTag(this.path, tag) } + } + pageStack.push("TagsSelector.qml", { + //% "Manage tags" + title: qsTrId("sailfish-office-he-manage-tags"), + editing: true, + model: page.model.tags, + highlight: new Doc(page.model, model.filePath) }) + } + } } } } diff --git a/qml/Tag.qml b/qml/Tag.qml new file mode 100644 index 00000000..c594ab76 --- /dev/null +++ b/qml/Tag.qml @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2015 François Kubler. + * Contact: François Kubler + * + * 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; version 2 only. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + + +/** + * The default Tag is a Label with a transparent background and a Theme.primaryColor color. + * + * When enabled, a Tag can be selected or deselected. + * + * If enabled and selected : + * The background is shown, + * The label color is Theme.highlightColor. + * + * If not enabled and selected : (typically, when you just want to display a list of Tags) + * The background is shown, + * The label color is Theme.primaryColor. + * + * If unselected (regardless of enabled) : + * The background is transparent, + * The label color is Theme.primaryColor. + * + */ + +MouseArea { + id: root + + + property alias color: rect.color + property alias fontColor: label.color + property alias fontSize: label.font.pixelSize + property alias tag: label.text + + + property bool highlighted: root.pressed && root.containsMouse + property bool selected: false + + + + + height: label.height + (enabled ? Theme.paddingSmall : 0) * 2 + width: label.width + (enabled ? Theme.paddingLarge : Theme.paddingSmall) * 2 + + + + + Rectangle { + id: rect + + + anchors { + centerIn: parent + fill: parent + } + color: "transparent" + radius: 9 + + + Label { + id: label + + + anchors { + centerIn: parent + } + color: root.enabled ? root.highlighted ? Theme.highlightColor + : Theme.primaryColor + : Theme.highlightColor + font { + //capitalization: Font.AllLowercase + pixelSize: Theme.fontSizeMedium + } + } + } + + + ListView.onAdd: AddAnimation { + target: root + } + + + ListView.onRemove: RemoveAnimation { + target: root + } + + + states: [ + State { + name: "SELECTED" + when: root.selected + + + PropertyChanges { + color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) + target: rect + } + }, + + + State { + name: "NOT_SELECTED" + when: !root.selected + + + PropertyChanges { + color: "transparent" + target: rect + } + } + ] + + + transitions: [ + Transition { + reversible: true + to: "*" + + + ColorAnimation { + duration: 100 + easing.type: Easing.InOutQuad + target: rect + } + } + ] +} \ No newline at end of file diff --git a/qml/TagsSelector.qml b/qml/TagsSelector.qml new file mode 100644 index 00000000..5be231a9 --- /dev/null +++ b/qml/TagsSelector.qml @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2015 François Kubler and Caliste Damien. + * Contact: François Kubler + * Damien Caliste + * + * 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; version 2 only. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: page + + property bool editing + property alias title: header.title + property alias model: tags.model //selectionModel.sourceModel + property var highlight // An object with three methods : + // hasTag(tag), addTag(tag), removeTag(tag) + + /*SortFilterSelectionModel { + id: selectionModel + + filter { + property: "label" + value: searchField.text + } + sort { + property: "label" + order: Qt.AscendingOrder + } + }*/ + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + + Column { + id: column + + anchors { + left: parent.left + right: parent.right + } + + PageHeader { + id: header + } + + SearchField { + id: searchField + + anchors { + left: parent.left + right: parent.right + } + + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + } + + Flow { + id: selection + + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + //height: parent.height - searchField.height - column.spacing + spacing: Theme.paddingMedium + + MouseArea { + width: newItem.width + icon.width + height: Math.max(newItem.height, icon.height) + // This item always visible unless : + // - searchField is empty ; + // - OR there is already a tag by that exact name. + visible: page.editing && !(searchField.text === "" || + page.model.contains(searchField.text)) + Tag { + id: newItem + anchors { + verticalCenter: parent.verticalCenter + } + + tag: searchField.text.trim() + selected: false + } + Image { + id: icon + anchors { + left: newItem.right + verticalCenter: parent.verticalCenter + } + source: "image://theme/icon-m-add" + } + + onClicked: { + page.highlight.addTag(newItem.tag) + searchField.text = "" + } + } + + Repeater { + id: tags + delegate: Tag { + id: tagDelegate + + selected: page.highlight.hasTag(model.label) + tag: model.label + + onClicked: if (selected) { + selected = false + page.highlight.removeTag(model.label) + } else { + selected = true + page.highlight.addTag(model.label) + } + } + //model: selectionModel + } + } + } + + VerticalScrollDecorator {} + } +} From a33fa992fa9e1068dc1d726107b4dfb2c25ec068 Mon Sep 17 00:00:00 2001 From: Damien Caliste Date: Thu, 15 Oct 2015 21:53:08 +0200 Subject: [PATCH 3/7] Add the filter for tags. --- main.cpp | 1 + models/documentlistmodel.cpp | 1 - models/filtermodel.cpp | 6 ++--- models/filtermodel.h | 3 ++- models/taglistmodel.cpp | 47 +++++++++++++++++++++++++++--------- models/taglistmodel.h | 19 +++++++++++++++ models/tagsthread.cpp | 7 +++--- qml/TagsSelector.qml | 46 ++++++++++++++++++++--------------- 8 files changed, 91 insertions(+), 39 deletions(-) diff --git a/main.cpp b/main.cpp index aef93df2..ed3499e2 100644 --- a/main.cpp +++ b/main.cpp @@ -68,6 +68,7 @@ QSharedPointer createView(const QString &file) qmlRegisterType("Sailfish.Office.Files", 1, 0, "DocumentProviderListModel"); qmlRegisterType("Sailfish.Office.Files", 1, 0, "TrackerDocumentProvider"); qmlRegisterType("Sailfish.Office.Files", 1, 0, "TagListModel"); + qmlRegisterType("Sailfish.Office.Files", 1, 0, "TagFilterModel"); qmlRegisterType("Sailfish.Office.Files", 1, 0, "FilterModel"); qmlRegisterInterface("DocumentProvider"); diff --git a/models/documentlistmodel.cpp b/models/documentlistmodel.cpp index 1a8bd8ca..579ab3f1 100644 --- a/models/documentlistmodel.cpp +++ b/models/documentlistmodel.cpp @@ -246,7 +246,6 @@ void DocumentListModel::jobFinished(TagsThreadJob *job) d->tagsModel.addItem(job->tags.at(i)); } dataChanged(index(row), index(row)); - fprintf(stdout, "Ok, copy tags for index %d\n", row); break; } row += 1; diff --git a/models/filtermodel.cpp b/models/filtermodel.cpp index 639f458a..16e8cb5b 100644 --- a/models/filtermodel.cpp +++ b/models/filtermodel.cpp @@ -50,7 +50,7 @@ bool FilterModel::hasTag(const QString &tag) const } void FilterModel::addTag(const QString &tag) { - tags.insert(tag, true); + tags.insert(tag); invalidateFilter(); emit tagFilteringChanged(); } @@ -65,9 +65,9 @@ bool FilterModel::filterAcceptsRow(int source_row, const QModelIndex & source_pa bool ret; ret = true; - for (QMap::const_iterator it = tags.begin(); + for (QSet::const_iterator it = tags.begin(); it != tags.end() && ret; it++) { - ret = sourceModel()->hasTagAt(source_row, it.key()); + ret = sourceModel()->hasTagAt(source_row, *it); } return ret && QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); diff --git a/models/filtermodel.h b/models/filtermodel.h index 2b9f830f..a93a47f6 100644 --- a/models/filtermodel.h +++ b/models/filtermodel.h @@ -19,6 +19,7 @@ #ifndef FILTERMODEL_H #define FILTERMODEL_H +#include #include #include "documentlistmodel.h" @@ -49,7 +50,7 @@ public Q_SLOTS: void tagFilteringChanged(); private: - QMap tags; + QSet tags; }; #endif // FILTERMODEL_H diff --git a/models/taglistmodel.cpp b/models/taglistmodel.cpp index 857aa323..69a5a38b 100644 --- a/models/taglistmodel.cpp +++ b/models/taglistmodel.cpp @@ -21,7 +21,6 @@ struct TagListModelEntry { QString label; - unsigned int index; unsigned int usage; }; @@ -90,43 +89,67 @@ bool TagListModel::contains(const QString &tag) const void TagListModel::addItem(const QString &tag) { + int row = 0; for (QList::iterator entry = d->tags.begin(); entry != d->tags.end(); entry++) { - if (QString::localeAwareCompare(entry->label, tag) == 0) { + if ( entry->label == tag ) { entry->usage += 1; - dataChanged(index(entry->index), index(entry->index)); + dataChanged(index(row), index(row)); return; } + row += 1; } TagListModelEntry entry; entry.label = tag; entry.usage = 1; - // Add the new tag in the alphabetic order. - for (entry.index = 0; entry.index < d->tags.count(); entry.index++) - if (QString::localeAwareCompare(d->tags.at(entry.index).label, tag) > 0) - break; - beginInsertRows(QModelIndex(), entry.index, entry.index); - d->tags.insert(entry.index, entry); + beginInsertRows(QModelIndex(), d->tags.count(), d->tags.count()); + d->tags.append(entry); endInsertRows(); emit countChanged(); } void TagListModel::removeItem(const QString &tag) { + int row = 0; for(QList::iterator entry = d->tags.begin(); entry != d->tags.end(); entry++) { if (entry->label == tag) { entry->usage -= 1; if (entry->usage) { - dataChanged(index(entry->index), index(entry->index)); + dataChanged(index(row), index(row)); } else { - beginRemoveRows(QModelIndex(), entry->index, entry->index); - d->tags.removeAt(entry->index); + beginRemoveRows(QModelIndex(), row, row); + d->tags.removeAt(row); endRemoveRows(); emit countChanged(); } return; } + row += 1; } } + +TagFilterModel::TagFilterModel(QObject* parent) + : QSortFilterProxyModel(parent) +{ + this->setFilterRole(TagListModel::Roles::TagLabelRole); + this->setSortRole(TagListModel::Roles::TagLabelRole); + this->setSortLocaleAware(true); + this->setSortCaseSensitivity(Qt::CaseInsensitive); + sort(0); +} + +TagFilterModel::~TagFilterModel() +{ +} + +void TagFilterModel::setSourceModel(TagListModel *model) +{ + QSortFilterProxyModel::setSourceModel(static_cast(model)); +} + +TagListModel* TagFilterModel::sourceModel() const +{ + return static_cast(QSortFilterProxyModel::sourceModel()); +} diff --git a/models/taglistmodel.h b/models/taglistmodel.h index 1faab63c..ce28b8ec 100644 --- a/models/taglistmodel.h +++ b/models/taglistmodel.h @@ -20,6 +20,7 @@ #define TAGLISTMODEL_H #include +#include class TagListModel : public QAbstractListModel { @@ -55,4 +56,22 @@ class TagListModel : public QAbstractListModel const QScopedPointer< Private > d; }; +class TagFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(TagListModel *sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) +public: + TagFilterModel(QObject* parent = 0); + ~TagFilterModel(); + +public: + TagListModel* sourceModel() const; + +public Q_SLOTS: + void setSourceModel(TagListModel *model); + +Q_SIGNALS: + void sourceModelChanged(); +}; + #endif // TAGLISTMODEL_H diff --git a/models/tagsthread.cpp b/models/tagsthread.cpp index d6b12c6e..9aea85a4 100644 --- a/models/tagsthread.cpp +++ b/models/tagsthread.cpp @@ -157,15 +157,16 @@ bool TagsThreadQueue::event(QEvent *e) TagsThreadJob::~TagsThreadJob() { - fprintf(stdout, "killing job for %s\n", path.toLocal8Bit().data()); } void TagsThreadJob::run() { - if (!strstr(path.toLocal8Bit().data(), ".pdf")) { + /*if (!strstr(path.toLocal8Bit().data(), ".pdf")) { tags.append(QString("PDF")); } - tags.append(QString("newt")); + tags.append(QString("newt"));*/ } + + #include "tagsthread.moc" diff --git a/qml/TagsSelector.qml b/qml/TagsSelector.qml index 5be231a9..7c49ddfe 100644 --- a/qml/TagsSelector.qml +++ b/qml/TagsSelector.qml @@ -19,28 +19,21 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +import Sailfish.Office.Files 1.0 Page { id: page property bool editing property alias title: header.title - property alias model: tags.model //selectionModel.sourceModel + property alias model: selectionModel.sourceModel property var highlight // An object with three methods : // hasTag(tag), addTag(tag), removeTag(tag) - /*SortFilterSelectionModel { + TagFilterModel { id: selectionModel - - filter { - property: "label" - value: searchField.text - } - sort { - property: "label" - order: Qt.AscendingOrder - } - }*/ + filterRegExp: RegExp(searchField.text, "i") + } SilicaFlickable { anchors.fill: parent @@ -70,6 +63,23 @@ Page { EnterKey.onClicked: focus = false } + InfoLabel { + id: placeholder + //% "No tag exists." + text: qsTrId("sailfish-office-no-tag") + visible: opacity > 0 + opacity: searchField.text === "" && page.model.count == 0 ? 1.0 : 0.0 + Behavior on opacity { FadeAnimation {} } + } + InfoLabel { + font.pixelSize: Theme.fontSizeLarge + color: Theme.rgba(Theme.highlightColor, 0.4) + //% "Create one by typing in the above search field." + text: qsTrId("sailfish-office-no-tag-hint") + visible: opacity > 0 + opacity: placeholder.opacity + } + Flow { id: selection @@ -79,7 +89,6 @@ Page { right: parent.right rightMargin: Theme.horizontalPageMargin } - //height: parent.height - searchField.height - column.spacing spacing: Theme.paddingMedium MouseArea { @@ -122,15 +131,14 @@ Page { selected: page.highlight.hasTag(model.label) tag: model.label - onClicked: if (selected) { - selected = false - page.highlight.removeTag(model.label) - } else { - selected = true + onClicked: selected = !selected + onSelectedChanged: if (selected) { page.highlight.addTag(model.label) + } else { + page.highlight.removeTag(model.label) } } - //model: selectionModel + model: selectionModel } } } From cae1899b7f3ec1a3ec11e1e9c15d170f2cb5bfac Mon Sep 17 00:00:00 2001 From: Damien Caliste Date: Thu, 15 Oct 2015 22:55:48 +0200 Subject: [PATCH 4/7] Add SQL backend for tag storage. --- CMakeLists.txt | 3 +- models/documentlistmodel.cpp | 27 +++++++--- models/tagsthread.cpp | 100 +++++++++++++++++++++++++++++++++++ models/tagsthread.h | 13 ++++- 4 files changed, 133 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c8216a34..840d1b5d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ find_package(Qt5Gui REQUIRED) find_package(Qt5DBus REQUIRED) find_package(Qt5LinguistTools REQUIRED) find_package(QtSparql REQUIRED) +find_package(Qt5Sql REQUIRED) find_package(Booster REQUIRED) include(cmake/QtTranslationWithID.cmake) @@ -64,7 +65,7 @@ set(sailfishoffice_TS_SRCS create_translation(engen_qm_file ${CMAKE_BINARY_DIR}/sailfish-office.ts ${sailfishoffice_TS_SRCS}) add_executable(sailfish-office ${sailfishoffice_SRCS} ${engen_qm_file}) -qt5_use_modules(sailfish-office Widgets Quick DBus) +qt5_use_modules(sailfish-office Widgets Quick DBus Sql) target_link_libraries(sailfish-office stdc++ ${QT_LIBRARIES} ${BOOSTER_LIBRARY} ${QTSPARQL_LIBRARY}) install(TARGETS sailfish-office DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) diff --git a/models/documentlistmodel.cpp b/models/documentlistmodel.cpp index 579ab3f1..bb900e1b 100644 --- a/models/documentlistmodel.cpp +++ b/models/documentlistmodel.cpp @@ -119,6 +119,11 @@ void DocumentListModel::addTag(const QString &path, const QString &tag) entry->tags.insert(tag); dataChanged(index(row), index(row)); d->tagsModel.addItem(tag); + if (!entry->job) { + entry->job = new TagsThreadJob(path, TagsThreadJob::TaskAddTags); + entry->job->tags.append(tag); + d->tagsThread->queueJob(entry->job); + } } return; } @@ -135,6 +140,11 @@ void DocumentListModel::removeTag(const QString &path, const QString &tag) entry->tags.remove(tag); dataChanged(index(row), index(row)); d->tagsModel.removeItem(tag); + if (!entry->job) { + entry->job = new TagsThreadJob(path, TagsThreadJob::TaskRemoveTags); + entry->job->tags.append(tag); + d->tagsThread->queueJob(entry->job); + } } return; } @@ -190,8 +200,7 @@ void DocumentListModel::addItem(QString name, QString path, QString type, int si entry.fileRead = lastRead; entry.mimeType = mimeType; entry.documentClass = static_cast(mimeTypeToDocumentClass(mimeType)); - entry.job = new TagsThreadJob(path); - //entry.job.setTarget(entry.tags); + entry.job = new TagsThreadJob(path, TagsThreadJob::TaskLoadTags); d->tagsThread->queueJob(entry.job); int index = 0; @@ -239,13 +248,15 @@ void DocumentListModel::jobFinished(TagsThreadJob *job) for(QList::iterator entry = d->entries.begin(); entry != d->entries.end(); entry++) { if (entry->filePath == job->path) { - entry->job = 0; - entry->tags.clear(); - for (int i=0; i < job->tags.count(); i++) { - entry->tags.insert(job->tags.at(i)); - d->tagsModel.addItem(job->tags.at(i)); + entry->job = 0; + if (job->task == TagsThreadJob::TaskLoadTags) { + entry->tags.clear(); + for (int i=0; i < job->tags.count(); i++) { + entry->tags.insert(job->tags.at(i)); + d->tagsModel.addItem(job->tags.at(i)); + } + dataChanged(index(row), index(row)); } - dataChanged(index(row), index(row)); break; } row += 1; diff --git a/models/tagsthread.cpp b/models/tagsthread.cpp index 9aea85a4..0bd5078b 100644 --- a/models/tagsthread.cpp +++ b/models/tagsthread.cpp @@ -23,6 +23,10 @@ #include #include #include +#include +#include +#include +#include class TagsThreadQueue; @@ -165,8 +169,104 @@ void TagsThreadJob::run() tags.append(QString("PDF")); } tags.append(QString("newt"));*/ + switch (task) { + case (TaskLoadTags): + loadTagsFromDb(); + break; + case (TaskAddTags): + addTagsToDb(); + break; + case (TaskRemoveTags): + removeTagsFromDb(); + break; + } +} + + +/* Part related to access the local storage, as available from QML side. */ +static const QString dbConnection{"TagsStorage"}; +static void addDatabase() +{ + QString basename = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + + QDir::separator() + dbConnection + QLatin1String(".sqlite"); + QSqlDatabase database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), + dbConnection); + database.setDatabaseName(basename); +} + +static void ensureDbConnection() +{ + if (!QSqlDatabase::contains(dbConnection)) { + addDatabase(); + } +} +static bool ensureTable(QSqlDatabase &db) +{ + bool ret; + QSqlQuery qCreate = QSqlQuery(db); + ret = qCreate.exec(QLatin1String("CREATE TABLE IF NOT EXISTS Tags(" + "file TEXT NOT NULL," + "tag TEXT NOT NULL)")); + if (!ret) + return false; + + QSqlQuery qIndex = QSqlQuery(db); + ret = qIndex.exec(QLatin1String("CREATE INDEX IF NOT EXISTS idx_file ON Tags(file)")); + return ret; } +void TagsThreadJob::loadTagsFromDb() +{ + ensureDbConnection(); + QSqlDatabase db = QSqlDatabase::database(dbConnection); + if (!ensureTable(db)) + return; + + QSqlQuery query = QSqlQuery(db); + query.prepare(QLatin1String("SELECT tag FROM Tags WHERE file = ?")); + query.addBindValue(path); + + if (!query.exec()) + return; + + while (query.next()) { + tags.append(query.value(0).toString()); + } +} +void TagsThreadJob::addTagsToDb() +{ + ensureDbConnection(); + QSqlDatabase db = QSqlDatabase::database(dbConnection); + if (!ensureTable(db)) + return; + + for (QList::const_iterator tag = tags.begin(); tag != tags.end(); tag++ ) { + QSqlQuery query = QSqlQuery(db); + query.prepare(QLatin1String("INSERT INTO Tags(file, tag) VALUES (?, ?)")); + query.addBindValue(path); + query.addBindValue(*tag); + + if (!query.exec()) + return; + } +} +void TagsThreadJob::removeTagsFromDb() +{ + ensureDbConnection(); + QSqlDatabase db = QSqlDatabase::database(dbConnection); + if (!ensureTable(db)) + return; + + for (QList::const_iterator tag = tags.begin(); tag != tags.end(); tag++ ) { + QSqlQuery query = QSqlQuery(db); + query.prepare(QLatin1String("DELETE FROM Tags WHERE file = ? AND tag = ?")); + query.addBindValue(path); + query.addBindValue(*tag); + + if (!query.exec()) + return; + } +} #include "tagsthread.moc" diff --git a/models/tagsthread.h b/models/tagsthread.h index a88cc3c8..7534961e 100644 --- a/models/tagsthread.h +++ b/models/tagsthread.h @@ -25,13 +25,24 @@ class TagsThreadJob : public QObject { Q_OBJECT public: - TagsThreadJob( QString &path ): path(path) {}; + enum Task + { + TaskLoadTags, + TaskAddTags, + TaskRemoveTags + }; + TagsThreadJob( const QString &path, Task task ): path(path), task(task) {}; ~TagsThreadJob(); void run(); QList tags; QString path; + Task task; +private: + void loadTagsFromDb(); + void addTagsToDb(); + void removeTagsFromDb(); }; class TagsThreadPrivate; From 8b378523d8cb3fa8e8fdcda8b600e2bcfcfbb763 Mon Sep 17 00:00:00 2001 From: Damien Caliste Date: Fri, 16 Oct 2015 14:52:41 +0200 Subject: [PATCH 5/7] Use path to identify jobs in threads. --- models/documentlistmodel.cpp | 46 ++++++++++++++++-------------------- models/tagsthread.cpp | 23 +++++++++++------- models/tagsthread.h | 3 ++- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/models/documentlistmodel.cpp b/models/documentlistmodel.cpp index bb900e1b..89961ab5 100644 --- a/models/documentlistmodel.cpp +++ b/models/documentlistmodel.cpp @@ -30,7 +30,6 @@ struct DocumentListModelEntry QDateTime fileRead; QString mimeType; QSet tags; - TagsThreadJob *job; DocumentListModel::DocumentClass documentClass; bool dirty; // When true, should be removed from list. }; @@ -119,11 +118,9 @@ void DocumentListModel::addTag(const QString &path, const QString &tag) entry->tags.insert(tag); dataChanged(index(row), index(row)); d->tagsModel.addItem(tag); - if (!entry->job) { - entry->job = new TagsThreadJob(path, TagsThreadJob::TaskAddTags); - entry->job->tags.append(tag); - d->tagsThread->queueJob(entry->job); - } + TagsThreadJob *job = new TagsThreadJob(path, TagsThreadJob::TaskAddTags); + job->tags.append(tag); + d->tagsThread->queueJob(job); } return; } @@ -140,11 +137,9 @@ void DocumentListModel::removeTag(const QString &path, const QString &tag) entry->tags.remove(tag); dataChanged(index(row), index(row)); d->tagsModel.removeItem(tag); - if (!entry->job) { - entry->job = new TagsThreadJob(path, TagsThreadJob::TaskRemoveTags); - entry->job->tags.append(tag); - d->tagsThread->queueJob(entry->job); - } + TagsThreadJob *job = new TagsThreadJob(path, TagsThreadJob::TaskRemoveTags); + job->tags.append(tag); + d->tagsThread->queueJob(job); } return; } @@ -200,8 +195,7 @@ void DocumentListModel::addItem(QString name, QString path, QString type, int si entry.fileRead = lastRead; entry.mimeType = mimeType; entry.documentClass = static_cast(mimeTypeToDocumentClass(mimeType)); - entry.job = new TagsThreadJob(path, TagsThreadJob::TaskLoadTags); - d->tagsThread->queueJob(entry.job); + d->tagsThread->queueJob(new TagsThreadJob(path, TagsThreadJob::TaskLoadTags)); int index = 0; for (; index < d->entries.count(); ++index) { @@ -227,7 +221,7 @@ void DocumentListModel::removeItemsDirty() void DocumentListModel::removeAt(int index) { if (index > -1 && index < d->entries.count()) { - d->tagsThread->cancelJob(d->entries.at(index).job); + d->tagsThread->cancelJobsForPath(d->entries.at(index).filePath); beginRemoveRows(QModelIndex(), index, index); d->entries.removeAt(index); endRemoveRows(); @@ -236,7 +230,7 @@ void DocumentListModel::removeAt(int index) void DocumentListModel::clear() { - d->tagsThread->cancelJob(0); + d->tagsThread->cancelAllJobs(); beginResetModel(); d->entries.clear(); endResetModel(); @@ -244,22 +238,22 @@ void DocumentListModel::clear() void DocumentListModel::jobFinished(TagsThreadJob *job) { - int row = 0; - for(QList::iterator entry = d->entries.begin(); - entry != d->entries.end(); entry++) { - if (entry->filePath == job->path) { - entry->job = 0; - if (job->task == TagsThreadJob::TaskLoadTags) { + if (job->task == TagsThreadJob::TaskLoadTags) { + int row = 0; + for(QList::iterator entry = d->entries.begin(); + entry != d->entries.end(); entry++) { + if (entry->filePath == job->path) { entry->tags.clear(); - for (int i=0; i < job->tags.count(); i++) { - entry->tags.insert(job->tags.at(i)); - d->tagsModel.addItem(job->tags.at(i)); + for (QList::const_iterator tag = job->tags.begin(); + tag != job->tags.end(); tag++) { + entry->tags.insert(*tag); + d->tagsModel.addItem(*tag); } dataChanged(index(row), index(row)); + break; } - break; + row += 1; } - row += 1; } job->deleteLater(); } diff --git a/models/tagsthread.cpp b/models/tagsthread.cpp index 0bd5078b..e40bd1fb 100644 --- a/models/tagsthread.cpp +++ b/models/tagsthread.cpp @@ -90,7 +90,7 @@ TagsThread::~TagsThread() // Cancel outstanding render jobs and schedule the queue // for deletion. Also set the jobQueue to 0 so we don't // end up calling back to the now deleted documents object. - cancelJob(0); + cancelAllJobs(); priv->thread->mutex.lock(); priv->thread->jobQueue->deleteLater(); priv->thread->jobQueue = 0; @@ -108,21 +108,26 @@ void TagsThread::queueJob(TagsThreadJob *job) QCoreApplication::postEvent(priv->thread->jobQueue, new QEvent(Event_JobPending)); } -void TagsThread::cancelJob(TagsThreadJob *job) +void TagsThread::cancelAllJobs() { QMutexLocker locker{ &priv->thread->mutex }; for (QList::iterator it = priv->thread->jobQueue->begin(); it != priv->thread->jobQueue->end(); ) { TagsThreadJob *j = *it; - if (!job || j == job) { + it = priv->thread->jobQueue->erase(it); + j->deleteLater(); + } +} +void TagsThread::cancelJobsForPath( const QString &path ) +{ + QMutexLocker locker{ &priv->thread->mutex }; + for (QList::iterator it = priv->thread->jobQueue->begin(); it != priv->thread->jobQueue->end(); ) { + if (path == (*it)->path) { + TagsThreadJob *j = *it; it = priv->thread->jobQueue->erase(it); j->deleteLater(); - if (job) { - continue; // to skip the ++it at the end of the loop - } else { - return; - } + } else { + ++it; } - ++it; } } diff --git a/models/tagsthread.h b/models/tagsthread.h index 7534961e..95b457bb 100644 --- a/models/tagsthread.h +++ b/models/tagsthread.h @@ -54,7 +54,8 @@ class TagsThread : public QObject ~TagsThread(); void queueJob( TagsThreadJob *job ); - void cancelJob( TagsThreadJob *job ); + void cancelAllJobs(); + void cancelJobsForPath( const QString &path ); Q_SIGNALS: void jobFinished( TagsThreadJob *job ); From fa3855430622cde64ef2a3dfa48ebff67a3230e6 Mon Sep 17 00:00:00 2001 From: Damien Caliste Date: Tue, 3 Nov 2015 11:44:04 +0100 Subject: [PATCH 6/7] Update tag memory storage in DocumentListModel. --- models/documentlistmodel.cpp | 106 ++++++++++++++++------------------- models/documentlistmodel.h | 4 +- models/filtermodel.cpp | 2 +- 3 files changed, 51 insertions(+), 61 deletions(-) diff --git a/models/documentlistmodel.cpp b/models/documentlistmodel.cpp index 89961ab5..f2dea67d 100644 --- a/models/documentlistmodel.cpp +++ b/models/documentlistmodel.cpp @@ -29,7 +29,6 @@ struct DocumentListModelEntry int fileSize; QDateTime fileRead; QString mimeType; - QSet tags; DocumentListModel::DocumentClass documentClass; bool dirty; // When true, should be removed from list. }; @@ -48,9 +47,10 @@ class DocumentListModel::Private roles.insert(FileDocumentClass, "fileDocumentClass"); } QList entries; - QHash< int, QByteArray > roles; - TagsThread *tagsThread; - TagListModel tagsModel; + QHash roles; + TagsThread *tagsThread; // To delegate tag storage with SQL backend. + TagListModel tagsModel; // A QML list of all tags. + QHash> tags; // The association tag <-> [set of filenames] }; DocumentListModel::DocumentListModel(QObject *parent) @@ -91,60 +91,56 @@ QVariant DocumentListModel::data(const QModelIndex &index, int role) const return QVariant(); } -bool DocumentListModel::hasTagAt(int row, const QString &tag) const +void DocumentListModel::notifyForPath(const QString &path) +{ + int row = 0; + for (QList::iterator entry = d->entries.begin(); + entry != d->entries.end(); entry++) { + if (path == entry->filePath) { + dataChanged(index(row), index(row)); + return; + } + row += 1; + } +} +bool DocumentListModel::hasTag(int row, const QString &tag) const { if (row < 0 && row >= d->entries.count()) return false; - return d->entries.at(row).tags.contains(tag); + return hasTag(d->entries.at(row).filePath, tag); } bool DocumentListModel::hasTag(const QString &path, const QString &tag) const { - for (QList::iterator entry = d->entries.begin(); - entry != d->entries.end(); entry++) { - if ( path == entry->filePath ) - return entry->tags.contains(tag); - } - - return false; + return d->tags.value(tag).contains(path); } void DocumentListModel::addTag(const QString &path, const QString &tag) { - int row = 0; - for (QList::iterator entry = d->entries.begin(); - entry != d->entries.end(); entry++) { - if ( path == entry->filePath ) { - if ( !entry->tags.contains(tag) ) { - entry->tags.insert(tag); - dataChanged(index(row), index(row)); - d->tagsModel.addItem(tag); - TagsThreadJob *job = new TagsThreadJob(path, TagsThreadJob::TaskAddTags); - job->tags.append(tag); - d->tagsThread->queueJob(job); - } - return; - } - row += 1; - } + QSet &files = d->tags[tag]; + if (files.contains(path)) + return; // This path has already this tag. + + files.insert(path); + TagsThreadJob *job = new TagsThreadJob(path, TagsThreadJob::TaskAddTags); + job->tags.append(tag); + d->tagsThread->queueJob(job); + d->tagsModel.addItem(tag); + notifyForPath(path); } void DocumentListModel::removeTag(const QString &path, const QString &tag) { - int row = 0; - for (QList::iterator entry = d->entries.begin(); - entry != d->entries.end(); entry++) { - if ( path == entry->filePath ) { - if ( entry->tags.contains(tag) ) { - entry->tags.remove(tag); - dataChanged(index(row), index(row)); - d->tagsModel.removeItem(tag); - TagsThreadJob *job = new TagsThreadJob(path, TagsThreadJob::TaskRemoveTags); - job->tags.append(tag); - d->tagsThread->queueJob(job); - } - return; - } - row += 1; - } + QSet &files = d->tags[tag]; + if (!files.contains(path)) + return; // This path has not this tag. + + files.remove(path); + if (files.empty()) + d->tags.remove(tag); + TagsThreadJob *job = new TagsThreadJob(path, TagsThreadJob::TaskRemoveTags); + job->tags.append(tag); + d->tagsThread->queueJob(job); + d->tagsModel.removeItem(tag); + notifyForPath(path); } TagListModel* DocumentListModel::tags() const { @@ -239,21 +235,13 @@ void DocumentListModel::clear() void DocumentListModel::jobFinished(TagsThreadJob *job) { if (job->task == TagsThreadJob::TaskLoadTags) { - int row = 0; - for(QList::iterator entry = d->entries.begin(); - entry != d->entries.end(); entry++) { - if (entry->filePath == job->path) { - entry->tags.clear(); - for (QList::const_iterator tag = job->tags.begin(); - tag != job->tags.end(); tag++) { - entry->tags.insert(*tag); - d->tagsModel.addItem(*tag); - } - dataChanged(index(row), index(row)); - break; - } - row += 1; + for (QList::const_iterator tag = job->tags.begin(); + tag != job->tags.end(); tag++) { + QSet &files = d->tags[*tag]; + files.insert(job->path); + d->tagsModel.addItem(*tag); } + notifyForPath(job->path); } job->deleteLater(); } diff --git a/models/documentlistmodel.h b/models/documentlistmodel.h index 328e2eb6..70c5d7ef 100644 --- a/models/documentlistmodel.h +++ b/models/documentlistmodel.h @@ -71,7 +71,7 @@ class DocumentListModel : public QAbstractListModel TagListModel* tags() const; Q_INVOKABLE int mimeTypeToDocumentClass(QString mimeType) const; - bool hasTagAt(int row, const QString &tag) const; + bool hasTag(int row, const QString &tag) const; Q_INVOKABLE bool hasTag(const QString &path, const QString &tag) const; Q_INVOKABLE void addTag(const QString &path, const QString &tag); Q_INVOKABLE void removeTag(const QString &path, const QString &tag); @@ -85,6 +85,8 @@ public Q_SLOTS: private: class Private; const QScopedPointer d; + + void notifyForPath(const QString &path); }; #endif // DOCUMENTLISTMODEL_H diff --git a/models/filtermodel.cpp b/models/filtermodel.cpp index 16e8cb5b..897c86ae 100644 --- a/models/filtermodel.cpp +++ b/models/filtermodel.cpp @@ -67,7 +67,7 @@ bool FilterModel::filterAcceptsRow(int source_row, const QModelIndex & source_pa ret = true; for (QSet::const_iterator it = tags.begin(); it != tags.end() && ret; it++) { - ret = sourceModel()->hasTagAt(source_row, *it); + ret = sourceModel()->hasTag(source_row, *it); } return ret && QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); From 03bde3db5ae3ca80290b2bc35283033d9e66cf94 Mon Sep 17 00:00:00 2001 From: Damien Caliste Date: Thu, 11 Feb 2016 22:20:50 +0100 Subject: [PATCH 7/7] [office] Use Tracker as a tag provider. --- CMakeLists.txt | 1 + models/documentlistmodel.cpp | 42 ++++++--- models/documentlistmodel.h | 4 +- models/trackertagprovider.cpp | 159 ++++++++++++++++++++++++++++++++++ models/trackertagprovider.h | 53 ++++++++++++ 5 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 models/trackertagprovider.cpp create mode 100644 models/trackertagprovider.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 840d1b5d..dfc18437 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ set(sailfishoffice_SRCS dbusadaptor.cpp models/filtermodel.cpp models/tagsthread.cpp + models/trackertagprovider.cpp models/taglistmodel.cpp models/documentlistmodel.cpp models/documentproviderlistmodel.cpp diff --git a/models/documentlistmodel.cpp b/models/documentlistmodel.cpp index f2dea67d..988c321a 100644 --- a/models/documentlistmodel.cpp +++ b/models/documentlistmodel.cpp @@ -49,6 +49,7 @@ class DocumentListModel::Private QList entries; QHash roles; TagsThread *tagsThread; // To delegate tag storage with SQL backend. + TrackerTagProvider trackerTag; TagListModel tagsModel; // A QML list of all tags. QHash> tags; // The association tag <-> [set of filenames] }; @@ -56,13 +57,16 @@ class DocumentListModel::Private DocumentListModel::DocumentListModel(QObject *parent) : QAbstractListModel(parent), d(new Private) { - d->tagsThread = new TagsThread( this ); - connect( d->tagsThread, &TagsThread::jobFinished, this, &DocumentListModel::jobFinished ); + // d->tagsThread = new TagsThread(this); + // connect(d->tagsThread, &TagsThread::jobFinished, + // this, &DocumentListModel::jobFinished); + connect(&d->trackerTag, &TrackerTagProvider::tagLoaded, + this, &DocumentListModel::tagLoaded); } DocumentListModel::~DocumentListModel() { - delete d->tagsThread; + // delete d->tagsThread; } QVariant DocumentListModel::data(const QModelIndex &index, int role) const @@ -121,9 +125,10 @@ void DocumentListModel::addTag(const QString &path, const QString &tag) return; // This path has already this tag. files.insert(path); - TagsThreadJob *job = new TagsThreadJob(path, TagsThreadJob::TaskAddTags); - job->tags.append(tag); - d->tagsThread->queueJob(job); + // TagsThreadJob *job = new TagsThreadJob(path, TagsThreadJob::TaskAddTags); + // job->tags.append(tag); + // d->tagsThread->queueJob(job); + d->trackerTag.addTag(path, tag); d->tagsModel.addItem(tag); notifyForPath(path); } @@ -136,9 +141,10 @@ void DocumentListModel::removeTag(const QString &path, const QString &tag) files.remove(path); if (files.empty()) d->tags.remove(tag); - TagsThreadJob *job = new TagsThreadJob(path, TagsThreadJob::TaskRemoveTags); - job->tags.append(tag); - d->tagsThread->queueJob(job); + // TagsThreadJob *job = new TagsThreadJob(path, TagsThreadJob::TaskRemoveTags); + // job->tags.append(tag); + // d->tagsThread->queueJob(job); + d->trackerTag.removeTag(path, tag); d->tagsModel.removeItem(tag); notifyForPath(path); } @@ -191,7 +197,8 @@ void DocumentListModel::addItem(QString name, QString path, QString type, int si entry.fileRead = lastRead; entry.mimeType = mimeType; entry.documentClass = static_cast(mimeTypeToDocumentClass(mimeType)); - d->tagsThread->queueJob(new TagsThreadJob(path, TagsThreadJob::TaskLoadTags)); + d->trackerTag.loadTags(path); + //d->tagsThread->queueJob(new TagsThreadJob(path, TagsThreadJob::TaskLoadTags)); int index = 0; for (; index < d->entries.count(); ++index) { @@ -217,7 +224,7 @@ void DocumentListModel::removeItemsDirty() void DocumentListModel::removeAt(int index) { if (index > -1 && index < d->entries.count()) { - d->tagsThread->cancelJobsForPath(d->entries.at(index).filePath); + // d->tagsThread->cancelJobsForPath(d->entries.at(index).filePath); beginRemoveRows(QModelIndex(), index, index); d->entries.removeAt(index); endRemoveRows(); @@ -226,7 +233,7 @@ void DocumentListModel::removeAt(int index) void DocumentListModel::clear() { - d->tagsThread->cancelAllJobs(); + // d->tagsThread->cancelAllJobs(); beginResetModel(); d->entries.clear(); endResetModel(); @@ -246,6 +253,17 @@ void DocumentListModel::jobFinished(TagsThreadJob *job) job->deleteLater(); } +void DocumentListModel::tagLoaded(const QString &path, const QList &tags) +{ + for (QList::const_iterator tag = tags.begin(); + tag != tags.end(); tag++) { + QSet &files = d->tags[*tag]; + files.insert(path); + d->tagsModel.addItem(*tag); + } + notifyForPath(path); +} + int DocumentListModel::mimeTypeToDocumentClass(QString mimeType) const { DocumentClass documentClass = UnknownDocument; diff --git a/models/documentlistmodel.h b/models/documentlistmodel.h index 70c5d7ef..31f59a29 100644 --- a/models/documentlistmodel.h +++ b/models/documentlistmodel.h @@ -23,6 +23,7 @@ #include #include "tagsthread.h" +#include "trackertagprovider.h" #include "taglistmodel.h" class DocumentListModelPrivate; @@ -76,8 +77,9 @@ class DocumentListModel : public QAbstractListModel Q_INVOKABLE void addTag(const QString &path, const QString &tag); Q_INVOKABLE void removeTag(const QString &path, const QString &tag); -public Q_SLOTS: +private Q_SLOTS: void jobFinished(TagsThreadJob* job); + void tagLoaded(const QString &path, const QList &tags); Q_SIGNALS: void tagsChanged(); diff --git a/models/trackertagprovider.cpp b/models/trackertagprovider.cpp new file mode 100644 index 00000000..82596867 --- /dev/null +++ b/models/trackertagprovider.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2016 Damien Caliste + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "trackertagprovider.h" + +#include +#include +#include + +//The Tracker driver to use. +static const QString trackerDriver{"QTRACKER"}; + +class TrackerTagProvider::Private { +public: + Private() + : connection(new QSparqlConnection(trackerDriver)) + { + } + + ~Private() { + delete connection; + } + + QSparqlConnection *connection; +}; + +TrackerTagProvider::TrackerTagProvider(QObject *parent) + : QObject(parent) + , d(new Private()) +{ +} + +TrackerTagProvider::~TrackerTagProvider() +{ + delete d; +} + +void TrackerTagProvider::loadTags(const QString &path) +{ + QSparqlQuery q(QString("SELECT ?label WHERE {" + " ?f nie:isStoredAs ?p ; nao:hasTag ?tag ." + " ?p nie:url '%1' ." + " ?tag a nao:Tag ; nao:prefLabel ?label ." + "} ORDER BY ASC(?label)").arg(QString(path).replace('\'', "\\\'"))); + QSparqlResult* result = d->connection->exec(q); + result->setProperty("path", QVariant(path)); + connect(result, &QSparqlResult::finished, this, &TrackerTagProvider::loadTagFinished); +} + +void TrackerTagProvider::addTag(const QString &path, const QString &tag) +{ + // First, check if tag exists. + QSparqlQuery q(QString("SELECT ?tag WHERE {" + " ?tag a nao:Tag ; nao:prefLabel '%1' ." + "} ORDER BY ASC(?label)").arg(QString(tag).replace('\'', "\\\'"))); + QSparqlResult* result = d->connection->exec(q); + result->setProperty("path", QVariant(path)); + result->setProperty("tag", QVariant(tag)); + connect(result, &QSparqlResult::finished, this, &TrackerTagProvider::existTagFinished); +} + +void TrackerTagProvider::addExistingTag(const QString &path, const QString &tag) +{ + QSparqlQuery q(QString("INSERT {" + " ?f nao:hasTag ?tag" + "} WHERE {" + " ?f nie:isStoredAs ?p ." + " ?p nie:url '%1' ." + " ?tag nao:prefLabel '%2'" + "}").arg(QString(path).replace('\'', "\\\'")).arg(QString(tag).replace('\'', "\\\'")), QSparqlQuery::InsertStatement); + QSparqlResult* result = d->connection->exec(q); + connect(result, &QSparqlResult::finished, this, &TrackerTagProvider::addTagFinished); +} + +void TrackerTagProvider::addNewTag(const QString &path, const QString &tag) +{ + QSparqlQuery q(QString("INSERT {" + " _:tag a nao:Tag ; nao:prefLabel '%2' ." + " ?f nao:hasTag _:tag" + "} WHERE {" + " ?f nie:isStoredAs ?p ." + " ?p nie:url '%1' ." + "}").arg(QString(path).replace('\'', "\\\'")).arg(QString(tag).replace('\'', "\\\'")), QSparqlQuery::InsertStatement); + QSparqlResult* result = d->connection->exec(q); + connect(result, &QSparqlResult::finished, this, &TrackerTagProvider::addTagFinished); +} + +void TrackerTagProvider::removeTag(const QString &path, const QString &tag) +{ + QSparqlQuery q(QString("DELETE {" + " ?f nao:hasTag ?tag" + "} WHERE {" + " ?f nie:isStoredAs ?p ." + " ?p nie:url '%1' ." + " ?tag nao:prefLabel '%2' ." + "}").arg(QString(path).replace('\'', "\\\'")).arg(QString(tag).replace('\'', "\\\'")), QSparqlQuery::DeleteStatement); + QSparqlResult* result = d->connection->exec(q); + connect(result, &QSparqlResult::finished, this, &TrackerTagProvider::removeTagFinished); +} + +void TrackerTagProvider::loadTagFinished() +{ + QSparqlResult *r = qobject_cast(sender()); + QList tags; + + if (!r->hasError()) { + while (r->next()) { + tags.append(r->binding(0).value().toString()); + } + + emit tagLoaded(r->property("path").toString(), tags); + } + + r->deleteLater(); +} + +void TrackerTagProvider::existTagFinished() +{ + QSparqlResult *r = qobject_cast(sender()); + + if (!r->hasError()) { + if (r->next()) { + addExistingTag(r->property("path").toString(), r->property("tag").toString()); + } else { + addNewTag(r->property("path").toString(), r->property("tag").toString()); + } + } + + r->deleteLater(); +} + +void TrackerTagProvider::addTagFinished() +{ + QSparqlResult *r = qobject_cast(sender()); + + r->deleteLater(); +} + +void TrackerTagProvider::removeTagFinished() +{ + QSparqlResult *r = qobject_cast(sender()); + + r->deleteLater(); +} diff --git a/models/trackertagprovider.h b/models/trackertagprovider.h new file mode 100644 index 00000000..9148309f --- /dev/null +++ b/models/trackertagprovider.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 Damien Caliste + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TRACKERTAGPROVIDER_H +#define TRACKERTAGPROVIDER_H + +#include + +class TrackerTagProvider : public QObject +{ + Q_OBJECT + +public: + TrackerTagProvider(QObject *parent = 0); + ~TrackerTagProvider(); + + void loadTags(const QString &path); + void addTag(const QString &path, const QString &tag); + void removeTag(const QString &path, const QString &tag); + +signals: + void tagLoaded(const QString &path, const QList &tags); + +private Q_SLOTS: + void loadTagFinished(); + void existTagFinished(); + void addTagFinished(); + void removeTagFinished(); + +private: + class Private; + Private *d; + + void addNewTag(const QString &path, const QString &tag); + void addExistingTag(const QString &path, const QString &tag); +}; + +#endif