diff --git a/src/akonadi/akonadilivequeryhelpers.h b/src/akonadi/akonadilivequeryhelpers.h --- a/src/akonadi/akonadilivequeryhelpers.h +++ b/src/akonadi/akonadilivequeryhelpers.h @@ -28,6 +28,7 @@ #include "akonadi/akonadistorageinterface.h" #include "domain/livequery.h" +#include "domain/task.h" namespace Akonadi { @@ -50,6 +51,10 @@ ItemFetchFunction fetchItems(const Collection &collection) const; ItemFetchFunction fetchItems(const Tag &tag) const; + /// Returns a fetch function which calls a LiveQueryInput::AddFunction (provided as argument to the fetch function) + /// with the given task, then its parent, its grandparent etc. up until the project. + ItemFetchFunction fetchTaskParents(Domain::Task::Ptr task) const; + ItemFetchFunction fetchSiblings(const Item &item) const; TagFetchFunction fetchTags() const; diff --git a/src/akonadi/akonadilivequeryhelpers.cpp b/src/akonadi/akonadilivequeryhelpers.cpp --- a/src/akonadi/akonadilivequeryhelpers.cpp +++ b/src/akonadi/akonadilivequeryhelpers.cpp @@ -150,6 +150,49 @@ #endif } +LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchTaskParents(Domain::Task::Ptr task) const +{ + Akonadi::Item childItem = m_serializer->createItemFromTask(task); + Q_ASSERT(childItem.parentCollection().isValid()); // do I really need a fetchItem first, like fetchSiblings does? + // Note: if the task moves to another collection, this live query will then be invalid... + + const Akonadi::Item::Id childId = childItem.id(); + auto storage = m_storage; + auto serializer = m_serializer; + return [storage, serializer, childItem, childId] (const Domain::LiveQueryInput::AddFunction &add) { + auto job = storage->fetchItems(childItem.parentCollection()); + Utils::JobHandler::install(job->kjob(), [job, add, serializer, childId] { + if (job->kjob()->error() != KJob::NoError) + return; + + const auto items = job->items(); + // The item itself is part of the result, we need that in findProject, to react on changes of the item itself + // To return a correct child item in case it got updated, we can't use childItem, we need to find it in the list. + const auto myself = std::find_if(items.cbegin(), items.cend(), + [childId] (const Akonadi::Item &item) { + return childId == item.id(); + }); + if (myself == items.cend()) { + qWarning() << "Did not find item in the listing for its parent collection. Item ID:" << childId; + return; + } + add(*myself); + auto parentUid = serializer->relatedUidFromItem(*myself); + while (!parentUid.isEmpty()) { + const auto parent = std::find_if(items.cbegin(), items.cend(), + [serializer, parentUid] (const Akonadi::Item &item) { + return serializer->itemUid(item) == parentUid; + }); + if (parent == items.cend()) { + break; + } + add(*parent); + parentUid = serializer->relatedUidFromItem(*parent); + } + }); + }; +} + LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchSiblings(const Item &item) const { auto storage = m_storage; diff --git a/src/akonadi/akonadilivequeryintegrator.h b/src/akonadi/akonadilivequeryintegrator.h --- a/src/akonadi/akonadilivequeryintegrator.h +++ b/src/akonadi/akonadilivequeryintegrator.h @@ -108,7 +108,7 @@ typedef UnaryFunctionTraits AddTraits; typedef UnaryFunctionTraits PredicateTraits; - typedef typename std::decay::type InputType; + typedef typename std::decay::type InputType; // typically Akonadi::Item static_assert(std::is_same::value, "Fetch function must return void"); @@ -139,6 +139,47 @@ output = query; } + template + void bindRelationship(const QByteArray &debugName, + QSharedPointer> &output, + FetchFunction fetch, + PredicateFunction predicate, + ExtraArgs... extra) + { + typedef UnaryFunctionTraits FetchTraits; + typedef UnaryFunctionTraits AddTraits; + typedef UnaryFunctionTraits PredicateTraits; + + typedef typename std::decay::type InputType; // typically Akonadi::Item + + static_assert(std::is_same::value, + "Fetch function must return void"); + static_assert(std::is_same::value, + "Fetch add function must return void"); + static_assert(std::is_same::value, + "Predicate function must return bool"); + + typedef typename std::decay::type AddInputType; + static_assert(std::is_same::value, + "Fetch add and predicate functions must have the same input type"); + + if (output) + return; + + using namespace std::placeholders; + + auto query = Domain::LiveRelationshipQuery::Ptr::create(); + + query->setDebugName(debugName); + query->setFetchFunction(fetch); + query->setPredicateFunction(predicate); + query->setConvertFunction(std::bind(&LiveQueryIntegrator::create, this, _1, extra...)); + query->setRepresentsFunction(std::bind(&LiveQueryIntegrator::represents, this, _1, _2)); + + inputQueries() << query; + output = query; + } + void addRemoveHandler(const CollectionRemoveHandler &handler); void addRemoveHandler(const ItemRemoveHandler &handler); void addRemoveHandler(const TagRemoveHandler &handler); diff --git a/src/akonadi/akonadiserializer.cpp b/src/akonadi/akonadiserializer.cpp --- a/src/akonadi/akonadiserializer.cpp +++ b/src/akonadi/akonadiserializer.cpp @@ -67,7 +67,7 @@ QString Serializer::itemUid(const Item &item) { - if (isTaskItem(item)) { + if (item.hasPayload()) { const auto todo = item.payload(); return todo->uid(); } else { diff --git a/src/akonadi/akonaditaskqueries.h b/src/akonadi/akonaditaskqueries.h --- a/src/akonadi/akonaditaskqueries.h +++ b/src/akonadi/akonaditaskqueries.h @@ -48,6 +48,9 @@ typedef Domain::QueryResultProvider ContextProvider; typedef Domain::QueryResult ContextResult; + typedef Domain::QueryResult ProjectResult; + typedef Domain::LiveQueryOutput ProjectQueryOutput; + TaskQueries(const StorageInterface::Ptr &storage, const SerializerInterface::Ptr &serializer, const MonitorInterface::Ptr &monitor, @@ -62,6 +65,7 @@ TaskResult::Ptr findInboxTopLevel() const Q_DECL_OVERRIDE; TaskResult::Ptr findWorkdayTopLevel() const Q_DECL_OVERRIDE; ContextResult::Ptr findContexts(Domain::Task::Ptr task) const Q_DECL_OVERRIDE; + ProjectResult::Ptr findProject(Domain::Task::Ptr task) const Q_DECL_OVERRIDE; private slots: void onWorkdayPollTimeout(); @@ -76,6 +80,7 @@ mutable TaskQueryOutput::Ptr m_findAll; mutable QHash m_findChildren; + mutable QHash m_findProject; mutable TaskQueryOutput::Ptr m_findTopLevel; mutable TaskQueryOutput::Ptr m_findInboxTopLevel; mutable TaskQueryOutput::Ptr m_findWorkdayTopLevel; diff --git a/src/akonadi/akonaditaskqueries.cpp b/src/akonadi/akonaditaskqueries.cpp --- a/src/akonadi/akonaditaskqueries.cpp +++ b/src/akonadi/akonaditaskqueries.cpp @@ -80,6 +80,19 @@ return query->result(); } +TaskQueries::ProjectResult::Ptr TaskQueries::findProject(Domain::Task::Ptr task) const +{ + Akonadi::Item childItem = m_serializer->createItemFromTask(task); + auto &query = m_findProject[childItem.id()]; + auto fetch = m_helpers->fetchTaskParents(task); + auto predicate = [this, childItem] (const Akonadi::Item &item) { + auto project = m_serializer->createProjectFromItem(item); + return m_serializer->isProjectItem(item); + }; + m_integrator->bindRelationship("TaskQueries::findProject", query, fetch, predicate); + return query->result(); +} + TaskQueries::TaskResult::Ptr TaskQueries::findTopLevel() const { auto fetch = m_helpers->fetchItems(StorageInterface::Tasks); diff --git a/src/domain/livequery.h b/src/domain/livequery.h --- a/src/domain/livequery.h +++ b/src/domain/livequery.h @@ -280,6 +280,163 @@ typename Provider::WeakPtr m_provider; }; +// A query that stores an intermediate list of results (from the fetch), to react on changes on any item in that list +// and then filters that list with the predicate for the final result +// When one of the intermediary items changes, a full fetch is done again. +template +class LiveRelationshipQuery : public LiveQueryInput, public LiveQueryOutput +{ +public: + typedef QSharedPointer> Ptr; + typedef QList List; + + typedef QueryResultProvider Provider; + typedef QueryResult Result; + + typedef typename LiveQueryInput::AddFunction AddFunction; + typedef typename LiveQueryInput::FetchFunction FetchFunction; + typedef typename LiveQueryInput::PredicateFunction PredicateFunction; + + typedef std::function ConvertFunction; + typedef std::function RepresentsFunction; + + LiveRelationshipQuery() = default; + LiveRelationshipQuery(const LiveRelationshipQuery &other) = default; + LiveRelationshipQuery &operator=(const LiveRelationshipQuery &other) = default; + + ~LiveRelationshipQuery() + { + clear(); + } + + typename Result::Ptr result() Q_DECL_OVERRIDE + { + typename Provider::Ptr provider(m_provider.toStrongRef()); + + if (provider) + return Result::create(provider); + provider = Provider::Ptr::create(); + m_provider = provider.toWeakRef(); + + doFetch(); + + return Result::create(provider); + } + + void setFetchFunction(const FetchFunction &fetch) + { + m_fetch = fetch; + } + + void setPredicateFunction(const PredicateFunction &predicate) + { + m_predicate = predicate; + } + + void setConvertFunction(const ConvertFunction &convert) + { + m_convert = convert; + } + + void setDebugName(const QByteArray &name) + { + m_debugName = name; + } + + void setRepresentsFunction(const RepresentsFunction &represents) + { + m_represents = represents; + } + + void reset() Q_DECL_OVERRIDE + { + clear(); + doFetch(); + } + + void onAdded(const InputType &input) Q_DECL_OVERRIDE + { + typename Provider::Ptr provider(m_provider.toStrongRef()); + + if (!provider) + return; + + m_intermediaryResults.append(input); + if (m_predicate(input)) + addToProvider(provider, input); + } + + void onChanged(const InputType &input) Q_DECL_OVERRIDE + { + const bool found = std::any_of(m_intermediaryResults.constBegin(), m_intermediaryResults.constEnd(), + [&input, this](const InputType &output) { + return input.id() == output.id(); }); // ## generalize with another predicate? + if (found) + reset(); + } + + void onRemoved(const InputType &input) Q_DECL_OVERRIDE + { + onChanged(input); + } + +private: + template + bool isValidOutput(const T &/*output*/) + { + return true; + } + + template + bool isValidOutput(const QSharedPointer &output) + { + return !output.isNull(); + } + + template + bool isValidOutput(T *output) + { + return output != Q_NULLPTR; + } + + void addToProvider(const typename Provider::Ptr &provider, const InputType &input) + { + auto output = m_convert(input); + if (isValidOutput(output)) + provider->append(output); + } + + void doFetch() + { + auto addFunction = [this] (const InputType &input) { + onAdded(input); + }; + m_fetch(addFunction); + } + + void clear() + { + m_intermediaryResults.clear(); + + typename Provider::Ptr provider(m_provider.toStrongRef()); + + if (!provider) + return; + + while (!provider->data().isEmpty()) + provider->removeFirst(); + } + + FetchFunction m_fetch; + PredicateFunction m_predicate; + ConvertFunction m_convert; + RepresentsFunction m_represents; + QByteArray m_debugName; + + typename Provider::WeakPtr m_provider; + QList m_intermediaryResults; +}; + } diff --git a/src/domain/taskqueries.h b/src/domain/taskqueries.h --- a/src/domain/taskqueries.h +++ b/src/domain/taskqueries.h @@ -27,6 +27,7 @@ #include "context.h" #include "queryresult.h" #include "task.h" +#include "project.h" namespace Domain { @@ -49,6 +50,8 @@ virtual QueryResult::Ptr findWorkdayTopLevel() const = 0; virtual QueryResult::Ptr findContexts(Task::Ptr task) const = 0; + + virtual QueryResult::Ptr findProject(Task::Ptr task) const = 0; }; } diff --git a/src/presentation/workdaypagemodel.cpp b/src/presentation/workdaypagemodel.cpp --- a/src/presentation/workdaypagemodel.cpp +++ b/src/presentation/workdaypagemodel.cpp @@ -105,20 +105,35 @@ return artifact.dynamicCast() ? (defaultFlags | Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled) : defaultFlags; }; - auto data = [](const Domain::Artifact::Ptr &artifact, int role) -> QVariant { - if (role != Qt::DisplayRole - && role != Qt::EditRole - && role != Qt::CheckStateRole) { - return QVariant(); - } - - if (role == Qt::DisplayRole || role == Qt::EditRole) { - return artifact->title(); - } else if (auto task = artifact.dynamicCast()) { - return task->isDone() ? Qt::Checked : Qt::Unchecked; - } else { - return QVariant(); + auto data = [this](const Domain::Artifact::Ptr &artifact, int role) -> QVariant { + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + return artifact->title(); + case Qt::CheckStateRole: + if (auto task = artifact.dynamicCast()) { + return task->isDone() ? Qt::Checked : Qt::Unchecked; + } + break; + case Qt::ToolTipRole: + if (auto task = artifact.dynamicCast()) { + static Domain::QueryResult::Ptr lastProjectResult; + auto projectResult = m_taskQueries->findProject(task); + if (projectResult) { + // keep a refcount to it, for next time we get here... + lastProjectResult = projectResult; + if (!projectResult->data().isEmpty()) { + Domain::Project::Ptr project = projectResult->data().at(0); + return i18n("Project: %1", project->name()); + } + } + return i18n("Inbox"); + } + break; + default: + break; } + return QVariant(); }; auto setData = [this](const Domain::Artifact::Ptr &artifact, const QVariant &value, int role) { diff --git a/tests/units/akonadi/akonaditaskqueriestest.cpp b/tests/units/akonadi/akonaditaskqueriestest.cpp --- a/tests/units/akonadi/akonaditaskqueriestest.cpp +++ b/tests/units/akonadi/akonaditaskqueriestest.cpp @@ -26,6 +26,7 @@ #include "akonadi/akonadicachingstorage.h" #include "akonadi/akonaditaskqueries.h" #include "akonadi/akonadiserializer.h" +#include "akonadi/akonadiitemfetchjobinterface.h" #include "testlib/akonadifakedata.h" #include "testlib/gencollection.h" @@ -1616,6 +1617,162 @@ QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("43")); } + + void findProjectShouldLookInCollection() + { + // GIVEN + AkonadiFakeData data; + + // One top level collection + auto collection = GenCollection().withId(42).withRootAsParent().withTaskContent(); + data.createCollection(collection); + + // Three tasks in the collection (two being children of the first one) + data.createItem(GenTodo().withId(42).asProject().withParent(42) + .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); + data.createItem(GenTodo().withId(43).withParent(42) + .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")) + .withParentUid(QStringLiteral("uid-42"))); + data.createItem(GenTodo().withId(44).withParent(42) + .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) + .withParentUid(QStringLiteral("uid-42"))); + + // WHEN + auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); + + auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); + auto storage = createCachingStorage(data, cache); + QScopedPointer queries(new Akonadi::TaskQueries(storage, + serializer, + Akonadi::MonitorInterface::Ptr(data.createMonitor()), + cache)); + auto task = serializer->createTaskFromItem(data.item(44)); + // populate cache for collection + auto *fetchJob = storage->fetchItems(collection); + QVERIFY2(fetchJob->kjob()->exec(), qPrintable(fetchJob->kjob()->errorString())); + + auto result = queries->findProject(task); + + // THEN + QVERIFY(result); + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("42")); + + // Should not change anything + result = queries->findProject(task); + + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("42")); + } + + void findProjectShouldReactToRelationshipChange() + { + // GIVEN + AkonadiFakeData data; + + // One top level collection + const Akonadi::Collection::Id colId = 42; + auto collection = GenCollection().withId(colId).withRootAsParent().withTaskContent(); + data.createCollection(collection); + + // Three tasks in the collection (two being children of the first one) + // 1->2->3 (project) where 1 changes to 1->4->5 (project) + data.createItem(GenTodo().withId(3).asProject().withParent(colId) + .withTitle(QStringLiteral("Project 3")).withUid(QStringLiteral("uid-3"))); + data.createItem(GenTodo().withId(2).withParent(colId) + .withTitle(QStringLiteral("Intermediate item 2")).withUid(QStringLiteral("uid-2")) + .withParentUid(QStringLiteral("uid-3"))); + data.createItem(GenTodo().withId(1).withParent(colId) + .withTitle(QStringLiteral("Intermediate item 1")).withUid(QStringLiteral("uid-1")) + .withParentUid(QStringLiteral("uid-2"))); + data.createItem(GenTodo().withId(5).asProject().withParent(colId) + .withTitle(QStringLiteral("Project 5")).withUid(QStringLiteral("uid-5"))); + data.createItem(GenTodo().withId(4).withParent(colId) + .withTitle(QStringLiteral("Intermediate item 4")).withUid(QStringLiteral("uid-4")) + .withParentUid(QStringLiteral("uid-5"))); + + auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); + + auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); + auto storage = createCachingStorage(data, cache); + QScopedPointer queries(new Akonadi::TaskQueries(storage, + serializer, + Akonadi::MonitorInterface::Ptr(data.createMonitor()), + cache)); + auto task = serializer->createTaskFromItem(data.item(1)); + + auto result = queries->findProject(task); + + QVERIFY(result); + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("Project 3")); + + // WHEN + data.modifyItem(GenTodo(data.item(1)).withParentUid(QStringLiteral("uid-4"))); + + // THEN + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("Project 5")); + } + + void findProjectShouldReactToIntermediateParentChange() + { + // GIVEN + AkonadiFakeData data; + + // One top level collection + const Akonadi::Collection::Id colId = 42; + auto collection = GenCollection().withId(colId).withRootAsParent().withTaskContent(); + data.createCollection(collection); + + // Three tasks in the collection (two being children of the first one) + // 1->2->3 (project) where 2 changes to 1->2->4 (project) + data.createItem(GenTodo().withId(3).asProject().withParent(colId) + .withTitle(QStringLiteral("Project 3")).withUid(QStringLiteral("uid-3"))); + data.createItem(GenTodo().withId(2).withParent(colId) + .withTitle(QStringLiteral("Intermediate item 2")).withUid(QStringLiteral("uid-2")) + .withParentUid(QStringLiteral("uid-3"))); + data.createItem(GenTodo().withId(1).withParent(colId) + .withTitle(QStringLiteral("Intermediate item 1")).withUid(QStringLiteral("uid-1")) + .withParentUid(QStringLiteral("uid-2"))); + data.createItem(GenTodo().withId(4).asProject().withParent(colId) + .withTitle(QStringLiteral("Project 4")).withUid(QStringLiteral("uid-4"))); + + auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); + + auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); + auto storage = createCachingStorage(data, cache); + QScopedPointer queries(new Akonadi::TaskQueries(storage, + serializer, + Akonadi::MonitorInterface::Ptr(data.createMonitor()), + cache)); + auto task = serializer->createTaskFromItem(data.item(1)); + + auto result = queries->findProject(task); + + QVERIFY(result); + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("Project 3")); + + // WHEN + data.modifyItem(GenTodo(data.item(2)).withParentUid(QStringLiteral("uid-4"))); + + // THEN + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("Project 4")); + + // AND WHEN + data.removeItem(GenTodo(data.item(2))); + + // THEN + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 0); + } }; ZANSHIN_TEST_MAIN(AkonadiTaskQueriesTest) diff --git a/tests/units/presentation/workdaypagemodeltest.cpp b/tests/units/presentation/workdaypagemodeltest.cpp --- a/tests/units/presentation/workdaypagemodeltest.cpp +++ b/tests/units/presentation/workdaypagemodeltest.cpp @@ -90,6 +90,8 @@ taskQueriesMock(&Domain::TaskQueries::findChildren).when(childTask11).thenReturn(Domain::QueryResult::Ptr()); taskQueriesMock(&Domain::TaskQueries::findChildren).when(childTask12).thenReturn(Domain::QueryResult::Ptr()); + taskQueriesMock(&Domain::TaskQueries::findProject).when(task1).thenReturn(Domain::QueryResult::Ptr()); + Utils::MockObject taskRepositoryMock; Presentation::WorkdayPageModel workday(taskQueriesMock.getInstance(), @@ -157,6 +159,8 @@ QCOMPARE(model->data(task3Index, Qt::CheckStateRole).toBool(), task3->isDone()); QCOMPARE(model->data(taskChildTask12Index, Qt::CheckStateRole).toBool(), childTask12->isDone()); + QCOMPARE(model->data(task1Index, Qt::ToolTipRole).toString(), "Inbox"); + // WHEN taskRepositoryMock(&Domain::TaskRepository::update).when(task1).thenReturn(new FakeJob(this)); taskRepositoryMock(&Domain::TaskRepository::update).when(childTask11).thenReturn(new FakeJob(this));