diff --git a/CMakeLists.txt b/CMakeLists.txt index 0cbe4f78..b3e8a784 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,82 +1,83 @@ project(zanshin) cmake_minimum_required(VERSION 3.2) find_package(ECM REQUIRED CONFIG) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/ ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) include(FeatureSummary) include(ECMInstallIcons) include(ECMMarkAsTest) include(ECMPoQmTools) find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Core Gui Widgets Qml Test) find_package(Boost REQUIRED) macro(assert_min_ver version) set(error_msg "${CMAKE_CXX_COMPILER} ${CMAKE_CXX_COMPILER_VERSION} not supported") if("${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS "${version}") message(FATAL_ERROR "${msg}") endif() endmacro() if(APPLE) if((NOT "${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS "6.0.0.0") AND "${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS "6.0.0.6000058") # Apple Clang 6.0.0.6000057 is known to fail on some of our code using std::mem_fn # but have no issues with boost::mem_fn message("problematic Apple Clang version ${CMAKE_CXX_COMPILER_VERSION}, using boost::mem_fn") add_definitions(-DZANSHIN_USE_BOOST_MEM_FN) endif() endif() if(UNIX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") set(CMAKE_CXX_STANDARD 14) # Enable C++14, with cmake >= 3.1 set(CMAKE_CXX_EXTENSIONS OFF) # Don't enable gcc-specific extensions endif() kde_enable_exceptions() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") option(ZANSHIN_BUILD_COVERAGE "Build Zanshin with gcov support" OFF) if(ZANSHIN_BUILD_COVERAGE AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") endif() option(ZANSHIN_BUILD_ASAN "Build Zanshin with asan support" OFF) if(ZANSHIN_BUILD_ASAN) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-calls") link_libraries("asan") endif() add_definitions(-DQT_USE_FAST_CONCATENATION -DQT_USE_FAST_OPERATOR_PLUS) include_directories ( ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} ${Boost_INCLUDE_DIR} 3rdparty/kdepim/ 3rdparty/kdepim/libkdepim/ ) find_package(KF5 REQUIRED COMPONENTS AkonadiCalendar AkonadiNotes AkonadiSearch IdentityManagement KontactInterface Ldap Runner Wallet + WindowSystem ) find_package(KF5Akonadi "5.1" CONFIG REQUIRED) add_subdirectory(3rdparty) add_subdirectory(src) if(BUILD_TESTING) add_subdirectory(tests) endif() diff --git a/src/akonadi/akonadiserializer.cpp b/src/akonadi/akonadiserializer.cpp index 237f093c..8171c9c5 100644 --- a/src/akonadi/akonadiserializer.cpp +++ b/src/akonadi/akonadiserializer.cpp @@ -1,637 +1,643 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens Copyright 2014 RĂ©mi Benoit This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 "akonadiserializer.h" #include #include #include #include #include #include #include #include "utils/mem_fn.h" #include "akonadi/akonadiapplicationselectedattribute.h" #include "akonadi/akonaditimestampattribute.h" using namespace Akonadi; Serializer::Serializer() { } Serializer::~Serializer() { } bool Serializer::representsCollection(SerializerInterface::QObjectPtr object, Collection collection) { return object->property("collectionId").toLongLong() == collection.id(); } bool Serializer::representsItem(QObjectPtr object, Item item) { return object->property("itemId").toLongLong() == item.id(); } bool Serializer::representsAkonadiTag(Domain::Tag::Ptr tag, Tag akonadiTag) const { return tag->property("tagId").toLongLong() == akonadiTag.id(); } QString Serializer::objectUid(SerializerInterface::QObjectPtr object) { return object->property("todoUid").toString(); } Domain::DataSource::Ptr Serializer::createDataSourceFromCollection(Collection collection, DataSourceNameScheme naming) { if (!collection.isValid()) return Domain::DataSource::Ptr(); auto dataSource = Domain::DataSource::Ptr::create(); updateDataSourceFromCollection(dataSource, collection, naming); return dataSource; } void Serializer::updateDataSourceFromCollection(Domain::DataSource::Ptr dataSource, Collection collection, DataSourceNameScheme naming) { if (!collection.isValid()) return; QString name = collection.displayName(); if (naming == FullPath) { auto parent = collection.parentCollection(); while (parent.isValid() && parent != Akonadi::Collection::root()) { name = parent.displayName() + "/" + name; parent = parent.parentCollection(); } } dataSource->setName(name); const auto mimeTypes = collection.contentMimeTypes(); auto types = Domain::DataSource::ContentTypes(); if (mimeTypes.contains(NoteUtils::noteMimeType())) types |= Domain::DataSource::Notes; if (mimeTypes.contains(KCalCore::Todo::todoMimeType())) types |= Domain::DataSource::Tasks; dataSource->setContentTypes(types); if (collection.hasAttribute()) { auto iconName = collection.attribute()->iconName(); dataSource->setIconName(iconName); } if (!collection.hasAttribute()) { dataSource->setSelected(true); } else { auto isSelected = collection.attribute()->isSelected(); dataSource->setSelected(isSelected); } if (collection.enabled()) dataSource->setListStatus(Domain::DataSource::Bookmarked); else if (collection.referenced()) dataSource->setListStatus(Domain::DataSource::Listed); else dataSource->setListStatus(Domain::DataSource::Unlisted); dataSource->setProperty("collectionId", collection.id()); } Collection Serializer::createCollectionFromDataSource(Domain::DataSource::Ptr dataSource) { const auto id = dataSource->property("collectionId").value(); auto collection = Collection(id); collection.attribute(Akonadi::Collection::AddIfMissing); auto selectedAttribute = collection.attribute(Akonadi::Collection::AddIfMissing); selectedAttribute->setSelected(dataSource->isSelected()); switch (dataSource->listStatus()) { case Domain::DataSource::Unlisted: collection.setReferenced(false); collection.setEnabled(false); break; case Domain::DataSource::Listed: collection.setReferenced(true); collection.setEnabled(false); break; case Domain::DataSource::Bookmarked: collection.setReferenced(false); collection.setEnabled(true); break; default: qFatal("Shouldn't happen"); break; } return collection; } bool Serializer::isListedCollection(Collection collection) { return collection.enabled() || collection.referenced(); } bool Serializer::isSelectedCollection(Collection collection) { if (!isListedCollection(collection)) return false; if (!isNoteCollection(collection) && !isTaskCollection(collection)) return false; if (!collection.hasAttribute()) return true; return collection.attribute()->isSelected(); } bool Akonadi::Serializer::isNoteCollection(Akonadi::Collection collection) { return collection.contentMimeTypes().contains(NoteUtils::noteMimeType()); } bool Akonadi::Serializer::isTaskCollection(Akonadi::Collection collection) { return collection.contentMimeTypes().contains(KCalCore::Todo::todoMimeType()); } bool Serializer::isTaskItem(Item item) { if (!item.hasPayload()) return false; auto todo = item.payload(); return todo->customProperty("Zanshin", "Project").isEmpty(); } Domain::Task::Ptr Serializer::createTaskFromItem(Item item) { if (!isTaskItem(item)) return Domain::Task::Ptr(); auto task = Domain::Task::Ptr::create(); updateTaskFromItem(task, item); return task; } void Serializer::updateTaskFromItem(Domain::Task::Ptr task, Item item) { if (!isTaskItem(item)) return; auto todo = item.payload(); task->setTitle(todo->summary()); task->setText(todo->description()); task->setDone(todo->isCompleted()); task->setDoneDate(todo->completed().dateTime().toUTC()); task->setStartDate(todo->dtStart().dateTime().toUTC()); task->setDueDate(todo->dtDue().dateTime().toUTC()); task->setProperty("itemId", item.id()); task->setProperty("parentCollectionId", item.parentCollection().id()); task->setProperty("todoUid", todo->uid()); task->setProperty("relatedUid", todo->relatedTo()); + task->setRunning(todo->customProperty("Zanshin", "Running") == QLatin1String("1")); if (todo->attendeeCount() > 0) { const auto attendees = todo->attendees(); const auto delegate = std::find_if(attendees.begin(), attendees.end(), [] (const KCalCore::Attendee::Ptr &attendee) { return attendee->status() == KCalCore::Attendee::Accepted; }); if (delegate != attendees.end()) { task->setDelegate(Domain::Task::Delegate((*delegate)->name(), (*delegate)->email())); } } } bool Serializer::isTaskChild(Domain::Task::Ptr task, Akonadi::Item item) { if (!isTaskItem(item)) return false; auto todo = item.payload(); if (todo->relatedTo() == task->property("todoUid")) return true; return false; } Akonadi::Item Serializer::createItemFromTask(Domain::Task::Ptr task) { auto todo = KCalCore::Todo::Ptr::create(); todo->setSummary(task->title()); todo->setDescription(task->text()); if (task->isDone()) todo->setCompleted(KDateTime(task->doneDate())); else todo->setCompleted(false); todo->setDtStart(KDateTime(task->startDate(), KDateTime::UTC)); todo->setDtDue(KDateTime(task->dueDate(), KDateTime::UTC)); if (task->property("todoUid").isValid()) { todo->setUid(task->property("todoUid").toString()); } if (task->property("relatedUid").isValid()) { todo->setRelatedTo(task->property("relatedUid").toString()); } if (task->delegate().isValid()) { KCalCore::Attendee::Ptr attendee(new KCalCore::Attendee(task->delegate().name(), task->delegate().email(), true, KCalCore::Attendee::Accepted)); todo->addAttendee(attendee); } + if (task->isRunning()) { + todo->setCustomProperty("Zanshin", "Running", "1"); + } else { + todo->removeCustomProperty("Zanshin", "Running"); + } Akonadi::Item item; if (task->property("itemId").isValid()) { item.setId(task->property("itemId").value()); } if (task->property("parentCollectionId").isValid()) { auto parentId = task->property("parentCollectionId").value(); item.setParentCollection(Akonadi::Collection(parentId)); } item.setMimeType(KCalCore::Todo::todoMimeType()); item.setPayload(todo); return item; } QString Serializer::relatedUidFromItem(Akonadi::Item item) { if (isTaskItem(item)) { const auto todo = item.payload(); return todo->relatedTo(); } else if (isNoteItem(item)) { const auto message = item.payload(); const auto relatedHeader = message->headerByType("X-Zanshin-RelatedProjectUid"); return relatedHeader ? relatedHeader->asUnicodeString() : QString(); } else { return QString(); } } void Serializer::updateItemParent(Akonadi::Item item, Domain::Task::Ptr parent) { if (!isTaskItem(item)) return; auto todo = item.payload(); todo->setRelatedTo(parent->property("todoUid").toString()); } void Serializer::updateItemProject(Item item, Domain::Project::Ptr project) { if (isTaskItem(item)) { auto todo = item.payload(); todo->setRelatedTo(project->property("todoUid").toString()); } else if (isNoteItem(item)) { auto note = item.payload(); note->removeHeader("X-Zanshin-RelatedProjectUid"); const QByteArray parentUid = project->property("todoUid").toString().toUtf8(); if (!parentUid.isEmpty()) { auto relatedHeader = new KMime::Headers::Generic("X-Zanshin-RelatedProjectUid"); relatedHeader->from7BitString(parentUid); note->appendHeader(relatedHeader); } note->assemble(); } } void Serializer::removeItemParent(Akonadi::Item item) { if (!isTaskItem(item)) return; auto todo = item.payload(); todo->setRelatedTo(QString()); } void Serializer::promoteItemToProject(Akonadi::Item item) { if (!isTaskItem(item)) return; auto todo = item.payload(); todo->setRelatedTo(QString()); todo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); } void Serializer::clearItem(Akonadi::Item *item) { Q_ASSERT(item); if (!isTaskItem(*item)) return; // NOTE : Currently not working, when akonadistorage test will make it pass, we will use it // item->clearTags(); foreach (const Tag& tag, item->tags()) item->clearTag(tag); } Akonadi::Item::List Serializer::filterDescendantItems(const Akonadi::Item::List &potentialChildren, const Akonadi::Item &ancestorItem) { if (potentialChildren.isEmpty()) return Akonadi::Item::List(); Akonadi::Item::List itemsToProcess = potentialChildren; Q_ASSERT(ancestorItem.isValid() && ancestorItem.hasPayload()); KCalCore::Todo::Ptr todo = ancestorItem.payload(); const auto bound = std::partition(itemsToProcess.begin(), itemsToProcess.end(), [ancestorItem, todo](Akonadi::Item currentItem) { return (!currentItem.hasPayload() || currentItem == ancestorItem || currentItem.payload()->relatedTo() != todo->uid()); }); Akonadi::Item::List itemsRemoved; std::copy(itemsToProcess.begin(), bound, std::back_inserter(itemsRemoved)); itemsToProcess.erase(itemsToProcess.begin(), bound); auto result = std::accumulate(itemsToProcess.begin(), itemsToProcess.end(), Akonadi::Item::List(), [this, itemsRemoved](Akonadi::Item::List result, Akonadi::Item currentItem) { result << currentItem; return result += filterDescendantItems(itemsRemoved, currentItem); }); return result; } bool Serializer::isNoteItem(Item item) { return item.hasPayload(); } Domain::Note::Ptr Serializer::createNoteFromItem(Akonadi::Item item) { if (!isNoteItem(item)) return Domain::Note::Ptr(); Domain::Note::Ptr note = Domain::Note::Ptr::create(); updateNoteFromItem(note, item); return note; } void Serializer::updateNoteFromItem(Domain::Note::Ptr note, Item item) { if (!isNoteItem(item)) return; auto message = item.payload(); note->setTitle(message->subject(true)->asUnicodeString()); note->setText(message->mainBodyPart()->decodedText()); note->setProperty("itemId", item.id()); if (auto relatedHeader = message->headerByType("X-Zanshin-RelatedProjectUid")) { note->setProperty("relatedUid", relatedHeader->asUnicodeString()); } else { note->setProperty("relatedUid", QVariant()); } } Item Serializer::createItemFromNote(Domain::Note::Ptr note) { NoteUtils::NoteMessageWrapper builder; builder.setTitle(note->title()); builder.setText(note->text() + '\n'); // Adding an extra '\n' because KMime always removes it... KMime::Message::Ptr message = builder.message(); if (!note->property("relatedUid").toString().isEmpty()) { auto relatedHeader = new KMime::Headers::Generic("X-Zanshin-RelatedProjectUid"); relatedHeader->from7BitString(note->property("relatedUid").toString().toUtf8()); message->appendHeader(relatedHeader); } Akonadi::Item item; if (note->property("itemId").isValid()) { item.setId(note->property("itemId").value()); } item.setMimeType(Akonadi::NoteUtils::noteMimeType()); item.setPayload(message); return item; } bool Serializer::isProjectItem(Item item) { if (!item.hasPayload()) return false; return !isTaskItem(item); } Domain::Project::Ptr Serializer::createProjectFromItem(Item item) { if (!isProjectItem(item)) return Domain::Project::Ptr(); auto project = Domain::Project::Ptr::create(); updateProjectFromItem(project, item); return project; } void Serializer::updateProjectFromItem(Domain::Project::Ptr project, Item item) { if (!isProjectItem(item)) return; auto todo = item.payload(); project->setName(todo->summary()); project->setProperty("itemId", item.id()); project->setProperty("parentCollectionId", item.parentCollection().id()); project->setProperty("todoUid", todo->uid()); } Item Serializer::createItemFromProject(Domain::Project::Ptr project) { auto todo = KCalCore::Todo::Ptr::create(); todo->setSummary(project->name()); todo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); if (project->property("todoUid").isValid()) { todo->setUid(project->property("todoUid").toString()); } Akonadi::Item item; if (project->property("itemId").isValid()) { item.setId(project->property("itemId").value()); } if (project->property("parentCollectionId").isValid()) { auto parentId = project->property("parentCollectionId").value(); item.setParentCollection(Akonadi::Collection(parentId)); } item.setMimeType(KCalCore::Todo::todoMimeType()); item.setPayload(todo); return item; } bool Serializer::isProjectChild(Domain::Project::Ptr project, Item item) { const QString todoUid = project->property("todoUid").toString(); const QString relatedUid = relatedUidFromItem(item); return !todoUid.isEmpty() && !relatedUid.isEmpty() && todoUid == relatedUid; } Domain::Context::Ptr Serializer::createContextFromTag(Akonadi::Tag tag) { if (!isContext(tag)) return Domain::Context::Ptr(); auto context = Domain::Context::Ptr::create(); updateContextFromTag(context, tag); return context; } Akonadi::Tag Serializer::createTagFromContext(Domain::Context::Ptr context) { auto tag = Akonadi::Tag(); tag.setName(context->name()); tag.setType(Akonadi::SerializerInterface::contextTagType()); tag.setGid(QByteArray(context->name().toLatin1())); if (context->property("tagId").isValid()) tag.setId(context->property("tagId").value()); return tag; } void Serializer::updateContextFromTag(Domain::Context::Ptr context, Akonadi::Tag tag) { if (!isContext(tag)) return; context->setProperty("tagId", tag.id()); context->setName(tag.name()); } bool Serializer::hasContextTags(Item item) const { using namespace std::placeholders; Tag::List tags = item.tags(); return std::any_of(tags.constBegin(), tags.constEnd(), std::bind(Utils::mem_fn(&Serializer::isContext), this, _1)); } bool Serializer::hasAkonadiTags(Item item) const { using namespace std::placeholders; Tag::List tags = item.tags(); return std::any_of(tags.constBegin(), tags.constEnd(), std::bind(Utils::mem_fn(&Serializer::isAkonadiTag), this, _1)); } bool Serializer::isContext(const Akonadi::Tag &tag) const { return (tag.type() == Akonadi::SerializerInterface::contextTagType()); } bool Serializer::isAkonadiTag(const Tag &tag) const { return tag.type() == Akonadi::Tag::PLAIN; } bool Serializer::isContextTag(const Domain::Context::Ptr &context, const Akonadi::Tag &tag) const { return (context->property("tagId").value() == tag.id()); } bool Serializer::isContextChild(Domain::Context::Ptr context, Item item) const { if (!context->property("tagId").isValid()) return false; auto tagId = context->property("tagId").value(); Akonadi::Tag tag(tagId); return item.hasTag(tag); } Domain::Tag::Ptr Serializer::createTagFromAkonadiTag(Akonadi::Tag akonadiTag) { if (!isAkonadiTag(akonadiTag)) return Domain::Tag::Ptr(); auto tag = Domain::Tag::Ptr::create(); updateTagFromAkonadiTag(tag, akonadiTag); return tag; } void Serializer::updateTagFromAkonadiTag(Domain::Tag::Ptr tag, Akonadi::Tag akonadiTag) { if (!isAkonadiTag(akonadiTag)) return; tag->setProperty("tagId", akonadiTag.id()); tag->setName(akonadiTag.name()); } Akonadi::Tag Serializer::createAkonadiTagFromTag(Domain::Tag::Ptr tag) { auto akonadiTag = Akonadi::Tag(); akonadiTag.setName(tag->name()); akonadiTag.setType(Akonadi::Tag::PLAIN); akonadiTag.setGid(QByteArray(tag->name().toLatin1())); const auto tagProperty = tag->property("tagId"); if (tagProperty.isValid()) akonadiTag.setId(tagProperty.value()); return akonadiTag; } bool Serializer::isTagChild(Domain::Tag::Ptr tag, Akonadi::Item item) { if (!tag->property("tagId").isValid()) return false; auto tagId = tag->property("tagId").value(); Akonadi::Tag akonadiTag(tagId); return item.hasTag(akonadiTag); } diff --git a/src/domain/task.cpp b/src/domain/task.cpp index 6c787179..9bb102d7 100644 --- a/src/domain/task.cpp +++ b/src/domain/task.cpp @@ -1,180 +1,194 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 "task.h" #include "utils/datetime.h" using namespace Domain; Task::Task(QObject *parent) : Artifact(parent), + m_running(false), m_done(false) { } Task::~Task() { } +bool Task::isRunning() const +{ + return m_running; +} + bool Task::isDone() const { return m_done; } void Task::setDone(bool done) { if (m_done == done) return; const QDateTime doneDate = done ? Utils::DateTime::currentDateTime() : QDateTime(); m_done = done; m_doneDate = doneDate; emit doneChanged(done); emit doneDateChanged(doneDate); } void Task::setDoneDate(const QDateTime &doneDate) { if (m_doneDate == doneDate) return; m_doneDate = doneDate; emit doneDateChanged(doneDate); } QDateTime Task::startDate() const { return m_startDate; } void Task::setStartDate(const QDateTime &startDate) { if (m_startDate == startDate) return; m_startDate = startDate; emit startDateChanged(startDate); } QDateTime Task::dueDate() const { return m_dueDate; } QDateTime Task::doneDate() const { return m_doneDate; } Task::Delegate Task::delegate() const { return m_delegate; } +void Task::setRunning(bool running) +{ + if (m_running == running) + return; + m_running = running; + emit runningChanged(running); +} + void Task::setDueDate(const QDateTime &dueDate) { if (m_dueDate == dueDate) return; m_dueDate = dueDate; emit dueDateChanged(dueDate); } void Task::setDelegate(const Task::Delegate &delegate) { if (m_delegate == delegate) return; m_delegate = delegate; emit delegateChanged(delegate); } Task::Delegate::Delegate() { } Task::Delegate::Delegate(const QString &name, const QString &email) : m_name(name), m_email(email) { } Task::Delegate::Delegate(const Task::Delegate &other) : m_name(other.m_name), m_email(other.m_email) { } Task::Delegate::~Delegate() { } Task::Delegate &Task::Delegate::operator=(const Task::Delegate &other) { Delegate copy(other); std::swap(m_name, copy.m_name); std::swap(m_email, copy.m_email); return *this; } bool Task::Delegate::operator==(const Task::Delegate &other) const { return m_name == other.m_name && m_email == other.m_email; } bool Task::Delegate::isValid() const { return !m_email.isEmpty(); } QString Task::Delegate::display() const { return !isValid() ? QString() : !m_name.isEmpty() ? m_name : m_email; } QString Task::Delegate::name() const { return m_name; } void Task::Delegate::setName(const QString &name) { m_name = name; } QString Task::Delegate::email() const { return m_email; } void Task::Delegate::setEmail(const QString &email) { m_email = email; } diff --git a/src/domain/task.h b/src/domain/task.h index efc73237..0dc80404 100644 --- a/src/domain/task.h +++ b/src/domain/task.h @@ -1,106 +1,111 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 DOMAIN_TASK_H #define DOMAIN_TASK_H #include "artifact.h" #include namespace Domain { class Task : public Artifact { Q_OBJECT + Q_PROPERTY(bool running READ isRunning WRITE setRunning NOTIFY runningChanged) Q_PROPERTY(bool done READ isDone WRITE setDone NOTIFY doneChanged) Q_PROPERTY(QDateTime startDate READ startDate WRITE setStartDate NOTIFY startDateChanged) Q_PROPERTY(QDateTime dueDate READ dueDate WRITE setDueDate NOTIFY dueDateChanged) Q_PROPERTY(Domain::Task::Delegate delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) public: typedef QSharedPointer Ptr; typedef QList List; class Delegate { public: Delegate(); Delegate(const QString &name, const QString &email); Delegate(const Delegate &other); ~Delegate(); Delegate &operator=(const Delegate &other); bool operator==(const Delegate &other) const; bool isValid() const; QString display() const; QString name() const; void setName(const QString &name); QString email() const; void setEmail(const QString &email); private: QString m_name; QString m_email; }; explicit Task(QObject *parent = Q_NULLPTR); virtual ~Task(); + bool isRunning() const; bool isDone() const; QDateTime startDate() const; QDateTime dueDate() const; QDateTime doneDate() const; Delegate delegate() const; public slots: + void setRunning(bool running); void setDone(bool done); void setDoneDate(const QDateTime &doneDate); void setStartDate(const QDateTime &startDate); void setDueDate(const QDateTime &dueDate); void setDelegate(const Domain::Task::Delegate &delegate); signals: + void runningChanged(bool isRunning); void doneChanged(bool isDone); void doneDateChanged(const QDateTime &doneDate); void startDateChanged(const QDateTime &startDate); void dueDateChanged(const QDateTime &dueDate); void delegateChanged(const Domain::Task::Delegate &delegate); private: + bool m_running; bool m_done; QDateTime m_startDate; QDateTime m_dueDate; QDateTime m_doneDate; Delegate m_delegate; }; } Q_DECLARE_METATYPE(Domain::Task::Ptr) Q_DECLARE_METATYPE(Domain::Task::List) Q_DECLARE_METATYPE(Domain::Task::Delegate) #endif // DOMAIN_TASK_H diff --git a/src/presentation/CMakeLists.txt b/src/presentation/CMakeLists.txt index c84f67e7..e3bb44dd 100644 --- a/src/presentation/CMakeLists.txt +++ b/src/presentation/CMakeLists.txt @@ -1,25 +1,28 @@ set(presentation_SRCS applicationmodel.cpp artifacteditormodel.cpp artifactfilterproxymodel.cpp availablenotepagesmodel.cpp availablepagesmodelinterface.cpp availablepagessortfilterproxymodel.cpp availabletaskpagesmodel.cpp availablesourcesmodel.cpp contextpagemodel.cpp errorhandler.cpp errorhandlingmodelbase.cpp metatypes.cpp noteinboxpagemodel.cpp pagemodel.cpp projectpagemodel.cpp querytreemodelbase.cpp + runningtaskmodelinterface.cpp + runningtaskmodel.cpp tagpagemodel.cpp taskinboxpagemodel.cpp tasklistmodel.cpp + taskapplicationmodel.cpp workdaypagemodel.cpp ) add_library(presentation STATIC ${presentation_SRCS}) target_link_libraries(presentation Qt5::Core Qt5::Gui domain utils) diff --git a/src/presentation/runningtaskmodel.cpp b/src/presentation/runningtaskmodel.cpp new file mode 100644 index 00000000..1856ac90 --- /dev/null +++ b/src/presentation/runningtaskmodel.cpp @@ -0,0 +1,82 @@ +/* This file is part of Zanshin + + Copyright 2016-2017 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 "runningtaskmodel.h" + +using namespace Presentation; + +RunningTaskModel::RunningTaskModel(const Domain::TaskQueries::Ptr &taskQueries, + const Domain::TaskRepository::Ptr &taskRepository, + QObject *parent) + : RunningTaskModelInterface(parent), + m_queries(taskQueries), + m_taskRepository(taskRepository) +{ + // List all tasks to find if any one is already set to "running" + if (m_queries) { + m_taskList = m_queries->findAll(); + Q_ASSERT(m_taskList); + m_taskList->addPostInsertHandler([this](const Domain::Task::Ptr &task, int) { + if (task->isRunning()) { + setRunningTask(task); + } + }); + // if there was a addFinishedHandler, we could reset m_queries and m_taskList to nullptr there to free memory. + } +} + +RunningTaskModel::~RunningTaskModel() +{ +} + +Domain::Task::Ptr RunningTaskModel::runningTask() const +{ + return m_runningTask; +} + +void RunningTaskModel::setRunningTask(const Domain::Task::Ptr &runningTask) +{ + if (m_runningTask) { + m_runningTask->setRunning(false); + KJob *job = m_taskRepository->update(m_runningTask); + installHandler(job, tr("Cannot update task %1 to 'not running'").arg(m_runningTask->title())); + } + m_runningTask = runningTask; + if (m_runningTask) { + m_runningTask->setRunning(true); + KJob *job = m_taskRepository->update(m_runningTask); + installHandler(job, tr("Cannot update task %1 to 'running'").arg(m_runningTask->title())); + } + emit runningTaskChanged(m_runningTask); +} + +void RunningTaskModel::stopTask() +{ + setRunningTask(nullptr); +} + +void RunningTaskModel::doneTask() +{ + m_runningTask->setDone(true); + stopTask(); +} diff --git a/src/presentation/runningtaskmodel.h b/src/presentation/runningtaskmodel.h new file mode 100644 index 00000000..d169bee1 --- /dev/null +++ b/src/presentation/runningtaskmodel.h @@ -0,0 +1,62 @@ +/* This file is part of Zanshin + + Copyright 2016-2017 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 PRESENTATION_RUNNINGTASKMODEL_H +#define PRESENTATION_RUNNINGTASKMODEL_H + +#include "domain/taskqueries.h" +#include "domain/taskrepository.h" + +#include "runningtaskmodelinterface.h" + +namespace Presentation { + +class RunningTaskModel : public RunningTaskModelInterface +{ + Q_OBJECT +public: + typedef QSharedPointer Ptr; + + explicit RunningTaskModel(const Domain::TaskQueries::Ptr &taskQueries, + const Domain::TaskRepository::Ptr &taskRepository, + QObject *parent = nullptr); + ~RunningTaskModel(); + + Domain::Task::Ptr runningTask() const Q_DECL_OVERRIDE; + void setRunningTask(const Domain::Task::Ptr &runningTask) Q_DECL_OVERRIDE; + +public slots: + void stopTask() Q_DECL_OVERRIDE; + void doneTask() Q_DECL_OVERRIDE; + +private: + Domain::Task::Ptr m_runningTask; + + Domain::QueryResult::Ptr m_taskList; + Domain::TaskQueries::Ptr m_queries; + Domain::TaskRepository::Ptr m_taskRepository; +}; + +} + +#endif // PRESENTATION_RUNNINGTASKMODEL_H diff --git a/src/presentation/runningtaskmodelinterface.cpp b/src/presentation/runningtaskmodelinterface.cpp new file mode 100644 index 00000000..d5fe6bed --- /dev/null +++ b/src/presentation/runningtaskmodelinterface.cpp @@ -0,0 +1,12 @@ +#include "runningtaskmodelinterface.h" + +using namespace Presentation; + +RunningTaskModelInterface::RunningTaskModelInterface(QObject *parent) + : QObject(parent) +{ +} + +RunningTaskModelInterface::~RunningTaskModelInterface() +{ +} diff --git a/src/presentation/runningtaskmodelinterface.h b/src/presentation/runningtaskmodelinterface.h new file mode 100644 index 00000000..daf3e8d0 --- /dev/null +++ b/src/presentation/runningtaskmodelinterface.h @@ -0,0 +1,58 @@ +/* This file is part of Zanshin + + Copyright 2017 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 PRESENTATION_RUNNINGTASKMODELINTERFACE_H +#define PRESENTATION_RUNNINGTASKMODELINTERFACE_H + +#include + +#include "domain/task.h" + +#include "errorhandlingmodelbase.h" + +namespace Presentation { + +class RunningTaskModelInterface : public QObject, public ErrorHandlingModelBase +{ + Q_OBJECT + Q_PROPERTY(Domain::Task::Ptr runningTask READ runningTask WRITE setRunningTask NOTIFY runningTaskChanged) +public: + typedef QSharedPointer Ptr; + + explicit RunningTaskModelInterface(QObject *parent = nullptr); + ~RunningTaskModelInterface(); + + virtual Domain::Task::Ptr runningTask() const = 0; + virtual void setRunningTask(const Domain::Task::Ptr &runningTask) = 0; + +signals: + void runningTaskChanged(const Domain::Task::Ptr &task); + +public slots: + virtual void stopTask() = 0; + virtual void doneTask() = 0; +}; + +} + +#endif // PRESENTATION_RUNNINGTASKMODEL_H diff --git a/src/presentation/taskapplicationmodel.cpp b/src/presentation/taskapplicationmodel.cpp new file mode 100644 index 00000000..9a90d0e3 --- /dev/null +++ b/src/presentation/taskapplicationmodel.cpp @@ -0,0 +1,47 @@ +/* This file is part of Zanshin + + Copyright 2016-2017 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 "runningtaskmodel.h" +#include "taskapplicationmodel.h" +#include "utils/dependencymanager.h" + +using namespace Presentation; + +TaskApplicationModel::TaskApplicationModel(QObject *parent) + : ApplicationModel(parent) +{ +} + +TaskApplicationModel::~TaskApplicationModel() +{ +} + +RunningTaskModelInterface *TaskApplicationModel::runningTaskModel() +{ + if (!m_runningTaskModel) { + auto model = Utils::DependencyManager::globalInstance().create(); + m_runningTaskModel = model; + m_runningTaskModel->setErrorHandler(errorHandler()); + } + return m_runningTaskModel.data(); +} diff --git a/src/presentation/taskapplicationmodel.h b/src/presentation/taskapplicationmodel.h new file mode 100644 index 00000000..b71103fb --- /dev/null +++ b/src/presentation/taskapplicationmodel.h @@ -0,0 +1,50 @@ +/* This file is part of Zanshin + + Copyright 2016-2017 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 TASKAPPLICATIONMODEL_H +#define TASKAPPLICATIONMODEL_H + +#include "applicationmodel.h" +#include "runningtaskmodelinterface.h" + +namespace Presentation { + +class TaskApplicationModel : public ApplicationModel +{ + Q_OBJECT + Q_PROPERTY(RunningTaskModelInterface* runningTaskModel READ runningTaskModel) +public: + typedef QSharedPointer Ptr; + + explicit TaskApplicationModel(QObject *parent = Q_NULLPTR); + ~TaskApplicationModel(); + + Presentation::RunningTaskModelInterface *runningTaskModel(); + +private: + RunningTaskModelInterface::Ptr m_runningTaskModel; +}; + +} + +#endif // TASKAPPLICATIONMODEL_H diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index c573a7e8..4ff711aa 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -1,32 +1,35 @@ set(widgets_SRCS applicationcomponents.cpp availablepagesview.cpp availablesourcesview.cpp datasourcedelegate.cpp editorview.cpp filterwidget.cpp itemdelegate.cpp messagebox.cpp messageboxinterface.cpp newprojectdialog.cpp newprojectdialoginterface.cpp pageview.cpp pageviewerrorhandler.cpp + runningtaskwidget.cpp quickselectdialog.cpp quickselectdialoginterface.cpp scripteditor.cpp + taskapplicationcomponents.cpp ) qt5_wrap_ui(widgets_SRCS editorview.ui filterwidget.ui newprojectdialog.ui ) add_library(widgets STATIC ${widgets_SRCS}) target_link_libraries(widgets Qt5::Qml Qt5::Widgets presentation zanshinkdepimstatic + KF5::WindowSystem ) diff --git a/src/widgets/applicationcomponents.cpp b/src/widgets/applicationcomponents.cpp index 3c01ff7d..97c9d919 100644 --- a/src/widgets/applicationcomponents.cpp +++ b/src/widgets/applicationcomponents.cpp @@ -1,273 +1,278 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 "applicationcomponents.h" #include #include #include #include #include #include #include #include "availablepagesview.h" #include "availablesourcesview.h" #include "editorview.h" #include "pageview.h" #include "pageviewerrorhandler.h" #include "quickselectdialog.h" using namespace Widgets; ApplicationComponents::ApplicationComponents(QWidget *parent) : QObject(parent), + m_pageView(Q_NULLPTR), m_parent(parent), m_availableSourcesView(Q_NULLPTR), m_availablePagesView(Q_NULLPTR), - m_pageView(Q_NULLPTR), m_editorView(Q_NULLPTR), m_errorHandler(new PageViewErrorHandler) { m_quickSelectDialogFactory = [] (QWidget *parent) { return QuickSelectDialogPtr(new QuickSelectDialog(parent)); }; auto moveItemAction = new QAction(this); moveItemAction->setObjectName(QStringLiteral("moveItemAction")); moveItemAction->setText(tr("Move item")); moveItemAction->setShortcut(Qt::Key_M); connect(moveItemAction, &QAction::triggered, this, &ApplicationComponents::onMoveItemsRequested); m_actions.insert(QStringLiteral("page_view_move"), moveItemAction); } ApplicationComponents::~ApplicationComponents() { setModel({}); } QHash ApplicationComponents::globalActions() const { auto actions = QHash(); actions.unite(availableSourcesView()->globalActions()); actions.unite(availablePagesView()->globalActions()); actions.unite(pageView()->globalActions()); actions.unite(m_actions); return actions; } QObjectPtr ApplicationComponents::model() const { return m_model; } AvailableSourcesView *ApplicationComponents::availableSourcesView() const { if (!m_availableSourcesView) { auto availableSourcesView = new AvailableSourcesView(m_parent); if (m_model) { availableSourcesView->setModel(m_model->property("availableSources").value()); } ApplicationComponents *self = const_cast(this); self->m_availableSourcesView = availableSourcesView; } return m_availableSourcesView; } AvailablePagesView *ApplicationComponents::availablePagesView() const { if (!m_availablePagesView) { auto availablePagesView = new AvailablePagesView(m_parent); if (m_model) { availablePagesView->setModel(m_model->property("availablePages").value()); auto availableSources = m_model->property("availableSources").value(); if (availableSources) availablePagesView->setProjectSourcesModel(availableSources->property("sourceListModel").value()); } ApplicationComponents *self = const_cast(this); self->m_availablePagesView = availablePagesView; connect(self->m_availablePagesView, &AvailablePagesView::currentPageChanged, self, &ApplicationComponents::onCurrentPageChanged); } return m_availablePagesView; } PageView *ApplicationComponents::pageView() const { if (!m_pageView) { auto pageView = new PageView(m_parent); if (m_model) { pageView->setModel(m_model->property("currentPage").value()); connect(m_model.data(), SIGNAL(currentPageChanged(QObject*)), pageView, SLOT(setModel(QObject*))); } ApplicationComponents *self = const_cast(this); self->m_pageView = pageView; self->m_errorHandler->setPageView(pageView); connect(self->m_pageView, &PageView::currentArtifactChanged, self, &ApplicationComponents::onCurrentArtifactChanged); } return m_pageView; } EditorView *ApplicationComponents::editorView() const { if (!m_editorView) { auto editorView = new EditorView(m_parent); if (m_model) { editorView->setModel(m_model->property("editor").value()); } auto self = const_cast(this); self->m_editorView = editorView; } return m_editorView; } ApplicationComponents::QuickSelectDialogFactory ApplicationComponents::quickSelectDialogFactory() const { return m_quickSelectDialogFactory; } +QWidget *ApplicationComponents::parentWidget() const +{ + return m_parent; +} + void ApplicationComponents::setModel(const QObjectPtr &model) { if (m_model == model) return; if (m_model) { if (m_pageView) disconnect(m_model.data(), 0, m_pageView, 0); m_model->setProperty("errorHandler", 0); } // Delay deletion of the old model until we're out of scope auto tmp = m_model; Q_UNUSED(tmp); m_model = model; if (m_model) m_model->setProperty("errorHandler", QVariant::fromValue(errorHandler())); if (m_availableSourcesView) { m_availableSourcesView->setModel(m_model ? m_model->property("availableSources").value() : Q_NULLPTR); } if (m_availablePagesView) { m_availablePagesView->setModel(m_model ? m_model->property("availablePages").value() : Q_NULLPTR); m_availablePagesView->setProjectSourcesModel(m_model ? m_model->property("dataSourcesModel").value() : Q_NULLPTR); } if (m_pageView) { m_pageView->setModel(m_model ? m_model->property("currentPage").value() : Q_NULLPTR); if (m_model) { connect(m_model.data(), SIGNAL(currentPageChanged(QObject*)), m_pageView, SLOT(setModel(QObject*))); } } if (m_editorView) { m_editorView->setModel(m_model ? m_model->property("editor").value() : Q_NULLPTR); } } void ApplicationComponents::setQuickSelectDialogFactory(const QuickSelectDialogFactory &factory) { m_quickSelectDialogFactory = factory; } void ApplicationComponents::onCurrentPageChanged(QObject *page) { if (!m_model) return; m_model->setProperty("currentPage", QVariant::fromValue(page)); QObject *editorModel = m_model->property("editor").value(); if (editorModel) editorModel->setProperty("artifact", QVariant::fromValue(Domain::Artifact::Ptr())); } void ApplicationComponents::onCurrentArtifactChanged(const Domain::Artifact::Ptr &artifact) { if (!m_model) return; auto editorModel = m_model->property("editor").value(); if (editorModel) editorModel->setProperty("artifact", QVariant::fromValue(artifact)); } void ApplicationComponents::onMoveItemsRequested() { if (!m_model) return; if (m_pageView->selectedIndexes().size() == 0) return; auto pageListModel = m_availablePagesView->model()->property("pageListModel").value(); Q_ASSERT(pageListModel); QuickSelectDialogInterface::Ptr dlg = m_quickSelectDialogFactory(m_pageView); dlg->setModel(pageListModel); if (dlg->exec() == QDialog::Accepted) moveItems(dlg->selectedIndex(), m_pageView->selectedIndexes()); } Presentation::ErrorHandler *ApplicationComponents::errorHandler() const { return m_errorHandler.data(); } void ApplicationComponents::moveItems(const QModelIndex &destination, const QModelIndexList &droppedItems) { Q_ASSERT(destination.isValid()); Q_ASSERT(!droppedItems.isEmpty()); auto centralListModel = droppedItems.first().model(); auto availablePagesModel = const_cast(destination.model()); // drag const auto data = std::unique_ptr(centralListModel->mimeData(droppedItems)); // drop availablePagesModel->dropMimeData(data.get(), Qt::MoveAction, -1, -1, destination); } diff --git a/src/widgets/applicationcomponents.h b/src/widgets/applicationcomponents.h index 33323ab9..3dae9975 100644 --- a/src/widgets/applicationcomponents.h +++ b/src/widgets/applicationcomponents.h @@ -1,105 +1,110 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 WIDGETS_APPLICATIONCOMPONENTS_H #define WIDGETS_APPLICATIONCOMPONENTS_H #include #include #include #include #include #include "domain/artifact.h" #include "presentation/metatypes.h" class QAction; class QWidget; namespace Presentation { class ErrorHandler; } namespace Widgets { class AvailablePagesView; class AvailableSourcesView; class EditorView; class PageView; class PageViewErrorHandler; class QuickSelectDialogInterface; class ApplicationComponents : public QObject { Q_OBJECT public: typedef QSharedPointer QuickSelectDialogPtr; typedef std::function QuickSelectDialogFactory; explicit ApplicationComponents(QWidget *parent = Q_NULLPTR); ~ApplicationComponents(); QHash globalActions() const; QObjectPtr model() const; AvailableSourcesView *availableSourcesView() const; AvailablePagesView *availablePagesView() const; - PageView *pageView() const; + virtual PageView *pageView() const; EditorView *editorView() const; QuickSelectDialogFactory quickSelectDialogFactory() const; +protected: + QWidget *parentWidget() const; + public slots: - void setModel(const QObjectPtr &model); + virtual void setModel(const QObjectPtr &model); void setQuickSelectDialogFactory(const QuickSelectDialogFactory &factory); +protected: + PageView *m_pageView; + private slots: void onCurrentPageChanged(QObject *page); void onCurrentArtifactChanged(const Domain::Artifact::Ptr &artifact); void onMoveItemsRequested(); private: Presentation::ErrorHandler *errorHandler() const; void moveItems(const QModelIndex &destination, const QModelIndexList &droppedItems); QHash m_actions; QObjectPtr m_model; QWidget *m_parent; QPointer m_availableSourcesView; QPointer m_availablePagesView; - PageView *m_pageView; QPointer m_editorView; QScopedPointer m_errorHandler; QuickSelectDialogFactory m_quickSelectDialogFactory; }; } #endif // WIDGETS_APPLICATIONCOMPONENTS_H diff --git a/src/widgets/pageview.cpp b/src/widgets/pageview.cpp index 34ee513e..376a3fec 100644 --- a/src/widgets/pageview.cpp +++ b/src/widgets/pageview.cpp @@ -1,398 +1,459 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 "pageview.h" #include #include #include #include #include #include #include #include #include #include #include "filterwidget.h" #include "itemdelegate.h" #include "messagebox.h" #include #include "presentation/artifactfilterproxymodel.h" #include "presentation/metatypes.h" #include "presentation/querytreemodelbase.h" +#include "presentation/runningtaskmodelinterface.h" namespace Widgets { class PageTreeView : public QTreeView { Q_OBJECT public: using QTreeView::QTreeView; protected: void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE { if (event->key() == Qt::Key_Escape && state() != EditingState) { selectionModel()->clear(); } QTreeView::keyPressEvent(event); } void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE { header()->resizeSection(0, event->size().width()); QTreeView::resizeEvent(event); } }; } class PassivePopup : public QFrame { Q_OBJECT public: explicit PassivePopup(QWidget *parent = Q_NULLPTR) : QFrame(parent), m_hideTimer(new QTimer(this)), m_label(new QLabel(this)) { setWindowFlags(Qt::Tool | Qt::X11BypassWindowManagerHint | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint); setFrameStyle(QFrame::Box | QFrame::Plain); setLineWidth(2); setAttribute(Qt::WA_DeleteOnClose); setLayout(new QVBoxLayout); layout()->addWidget(m_label); connect(m_hideTimer, &QTimer::timeout, this, &QWidget::hide); } void setVisible(bool visible) Q_DECL_OVERRIDE { if (visible) { m_hideTimer->start(2000); } QFrame::setVisible(visible); } void setText(const QString &text) { m_label->setText(text); } private: QTimer *m_hideTimer; QLabel *m_label; }; using namespace Widgets; PageView::PageView(QWidget *parent) : QWidget(parent), m_cancelAction(new QAction(this)), m_model(Q_NULLPTR), m_messageWidget(new KMessageWidget(this)), m_filterWidget(new FilterWidget(this)), m_centralView(new PageTreeView(this)), m_quickAddEdit(new QLineEdit(this)) { m_messageWidget->setObjectName(QStringLiteral("messageWidget")); m_messageWidget->setCloseButtonVisible(true); m_messageWidget->setMessageType(KMessageWidget::Error); m_messageWidget->setWordWrap(true); m_messageWidget->hide(); m_filterWidget->setObjectName(QStringLiteral("filterWidget")); m_filterWidget->hide(); m_centralView->setObjectName(QStringLiteral("centralView")); m_centralView->header()->hide(); m_centralView->setAlternatingRowColors(true); m_centralView->setItemDelegate(new ItemDelegate(this)); m_centralView->setDragDropMode(QTreeView::DragDrop); m_centralView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_centralView->setModel(m_filterWidget->proxyModel()); m_centralView->installEventFilter(this); m_centralView->setItemsExpandable(false); m_centralView->setRootIsDecorated(false); connect(m_centralView->model(), &QAbstractItemModel::rowsInserted, m_centralView, &QTreeView::expandAll); connect(m_centralView->model(), &QAbstractItemModel::layoutChanged, m_centralView, &QTreeView::expandAll); connect(m_centralView->model(), &QAbstractItemModel::modelReset, m_centralView, &QTreeView::expandAll); m_centralView->setStyleSheet(QStringLiteral("QTreeView::branch { border-image: url(none.png); }")); m_quickAddEdit->setObjectName(QStringLiteral("quickAddEdit")); m_quickAddEdit->setPlaceholderText(tr("Type and press enter to add an item")); connect(m_quickAddEdit, &QLineEdit::returnPressed, this, &PageView::onReturnPressed); auto layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 3); layout->addWidget(m_messageWidget); layout->addWidget(m_filterWidget); layout->addWidget(m_centralView); layout->addWidget(m_quickAddEdit); setLayout(layout); m_messageBoxInterface = MessageBox::Ptr::create(); auto addItemAction = new QAction(this); addItemAction->setObjectName(QStringLiteral("addItemAction")); addItemAction->setText(tr("New item")); addItemAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); addItemAction->setShortcut(Qt::CTRL | Qt::Key_N); connect(addItemAction, &QAction::triggered, this, &PageView::onAddItemRequested); m_cancelAction->setObjectName(QStringLiteral("cancelAddItemAction")); m_cancelAction->setShortcut(Qt::Key_Escape); addAction(m_cancelAction); connect(m_cancelAction, &QAction::triggered, m_centralView, static_cast(&QWidget::setFocus)); auto removeItemAction = new QAction(this); removeItemAction->setObjectName(QStringLiteral("removeItemAction")); removeItemAction->setText(tr("Remove item")); removeItemAction->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); removeItemAction->setShortcut(Qt::Key_Delete); connect(removeItemAction, &QAction::triggered, this, &PageView::onRemoveItemRequested); addAction(removeItemAction); auto promoteItemAction = new QAction(this); promoteItemAction->setObjectName(QStringLiteral("promoteItemAction")); promoteItemAction->setText(tr("Promote item as project")); promoteItemAction->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P); connect(promoteItemAction, &QAction::triggered, this, &PageView::onPromoteItemRequested); auto filterViewAction = new QAction(this); filterViewAction->setObjectName(QStringLiteral("filterViewAction")); filterViewAction->setText(tr("Filter...")); filterViewAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-find"))); filterViewAction->setShortcut(Qt::CTRL | Qt::Key_F); filterViewAction->setCheckable(true); connect(filterViewAction, &QAction::triggered, this, &PageView::onFilterToggled); + m_runTaskAction = new QAction(this); + m_runTaskAction->setObjectName(QStringLiteral("runTaskAction")); + m_runTaskAction->setShortcut(Qt::CTRL | Qt::Key_Space); + m_runTaskAction->setText(tr("Start now")); + m_runTaskAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); + connect(m_runTaskAction, &QAction::triggered, this, &PageView::onRunTaskTriggered); + updateRunTaskAction(); + m_actions.insert(QStringLiteral("page_view_add"), addItemAction); m_actions.insert(QStringLiteral("page_view_remove"), removeItemAction); m_actions.insert(QStringLiteral("page_view_promote"), promoteItemAction); m_actions.insert(QStringLiteral("page_view_filter"), filterViewAction); + m_actions.insert(QStringLiteral("page_run_task"), m_runTaskAction); } QHash PageView::globalActions() const { return m_actions; } QObject *PageView::model() const { return m_model; } +Presentation::RunningTaskModelInterface *PageView::runningTaskModel() const +{ + return m_runningTaskModel; +} + void PageView::setModel(QObject *model) { if (model == m_model) return; if (m_centralView->selectionModel()) { disconnect(m_centralView->selectionModel(), Q_NULLPTR, this, Q_NULLPTR); } m_filterWidget->proxyModel()->setSourceModel(Q_NULLPTR); m_model = model; setEnabled(m_model); + updateRunTaskAction(); + if (!m_model) return; QVariant modelProperty = m_model->property("centralListModel"); if (modelProperty.canConvert()) m_filterWidget->proxyModel()->setSourceModel(modelProperty.value()); connect(m_centralView->selectionModel(), &QItemSelectionModel::currentChanged, this, &PageView::onCurrentChanged); } MessageBoxInterface::Ptr PageView::messageBoxInterface() const { return m_messageBoxInterface; } QModelIndexList PageView::selectedIndexes() const { using namespace std::placeholders; const auto selection = m_centralView->selectionModel()->selectedIndexes(); auto sourceIndices = QModelIndexList(); std::transform(selection.constBegin(), selection.constEnd(), std::back_inserter(sourceIndices ), std::bind(&QSortFilterProxyModel::mapToSource, m_filterWidget->proxyModel(), _1)); return sourceIndices; } +void PageView::setRunningTaskModel(Presentation::RunningTaskModelInterface *model) +{ + m_runningTaskModel = model; + connect(m_runningTaskModel, SIGNAL(runningTaskChanged(Domain::Task::Ptr)), + this, SLOT(onRunningTaskChanged(Domain::Task::Ptr))); +} + void PageView::setMessageBoxInterface(const MessageBoxInterface::Ptr &interface) { m_messageBoxInterface = interface; } void PageView::displayErrorMessage(const QString &message) { m_messageWidget->setText(message); m_messageWidget->animatedShow(); } void PageView::onReturnPressed() { if (m_quickAddEdit->text().isEmpty()) return; auto parentIndex = QModelIndex(); if (m_centralView->selectionModel()->selectedIndexes().size() == 1) parentIndex = m_centralView->selectionModel()->selectedIndexes().first(); QMetaObject::invokeMethod(m_model, "addItem", Q_ARG(QString, m_quickAddEdit->text()), Q_ARG(QModelIndex, parentIndex)); m_quickAddEdit->clear(); } void PageView::onAddItemRequested() { if (m_quickAddEdit->hasFocus()) return; const auto editTopLeft = m_quickAddEdit->geometry().topLeft(); const auto pos = mapToGlobal(editTopLeft); auto popup = new PassivePopup(m_quickAddEdit); popup->setText(tr("Type and press enter to add an item")); popup->show(); popup->move(pos - QPoint(0, popup->height())); m_quickAddEdit->selectAll(); m_quickAddEdit->setFocus(); } void PageView::onRemoveItemRequested() { const QModelIndexList ¤tIndexes = m_centralView->selectionModel()->selectedIndexes(); if (currentIndexes.isEmpty()) return; QString text; if (currentIndexes.size() > 1) { bool hasDescendants = false; foreach (const QModelIndex ¤tIndex, currentIndexes) { if (!currentIndex.isValid()) continue; if (currentIndex.model()->rowCount(currentIndex) > 0) { hasDescendants = true; break; } } if (hasDescendants) text = tr("Do you really want to delete the selected items and their children?"); else text = tr("Do you really want to delete the selected items?"); } else { const QModelIndex ¤tIndex = currentIndexes.first(); if (!currentIndex.isValid()) return; if (currentIndex.model()->rowCount(currentIndex) > 0) text = tr("Do you really want to delete the selected task and all its children?"); } if (!text.isEmpty()) { QMessageBox::Button button = m_messageBoxInterface->askConfirmation(this, tr("Delete Tasks"), text); bool canRemove = (button == QMessageBox::Yes); if (!canRemove) return; } foreach (const QModelIndex ¤tIndex, currentIndexes) { if (!currentIndex.isValid()) continue; QMetaObject::invokeMethod(m_model, "removeItem", Q_ARG(QModelIndex, currentIndex)); } } void PageView::onPromoteItemRequested() { QModelIndex currentIndex = m_centralView->currentIndex(); if (!currentIndex.isValid()) return; QMetaObject::invokeMethod(m_model, "promoteItem", Q_ARG(QModelIndex, currentIndex)); } void PageView::onFilterToggled(bool show) { m_filterWidget->setVisible(show); if (show) m_filterWidget->setFocus(); else m_filterWidget->clear(); } +void PageView::updateRunTaskAction() +{ + const auto artifact = currentArtifact(); + const auto task = artifact.objectCast(); + m_runTaskAction->setEnabled(task); +} + +void PageView::onRunTaskTriggered() +{ + auto task = currentArtifact().objectCast(); + Q_ASSERT(task); // the action is supposed to be disabled otherwise + if (task->startDate().isNull()) + task->setStartDate(QDateTime::currentDateTime()); + m_runningTaskModel->setProperty("runningTask", QVariant::fromValue(task)); +} + +void PageView::onRunningTaskChanged(const Domain::Task::Ptr &task) +{ + if (!task) { + QWidget *toplevel = window(); + toplevel->raise(); + toplevel->activateWindow(); + } +} + void PageView::onCurrentChanged(const QModelIndex ¤t) { + updateRunTaskAction(); + auto data = current.data(Presentation::QueryTreeModelBase::ObjectRole); if (!data.isValid()) return; - auto artifact = data.value(); + auto artifact = currentArtifact(); if (!artifact) return; emit currentArtifactChanged(artifact); } bool PageView::eventFilter(QObject *object, QEvent *event) { Q_ASSERT(object == m_centralView); switch(event->type()) { case QEvent::FocusIn: m_cancelAction->setEnabled(false); break; case QEvent::FocusOut: m_cancelAction->setEnabled(true); break; default: break; } return false; } +Domain::Artifact::Ptr PageView::currentArtifact() const +{ + const auto current = m_centralView->selectionModel()->currentIndex(); + const auto data = current.data(Presentation::QueryTreeModelBase::ObjectRole); + if (!data.isValid()) + return Domain::Artifact::Ptr(Q_NULLPTR); + + return data.value(); +} + #include "pageview.moc" diff --git a/src/widgets/pageview.h b/src/widgets/pageview.h index 1dbf212b..8aa7bb67 100644 --- a/src/widgets/pageview.h +++ b/src/widgets/pageview.h @@ -1,93 +1,106 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 WIDGETS_PAGEVIEW_H #define WIDGETS_PAGEVIEW_H #include #include #include #include #include #include "domain/artifact.h" +#include "domain/task.h" #include "messageboxinterface.h" class QLineEdit; class QModelIndex; class QMessageBox; class KMessageWidget; +namespace Presentation { +class RunningTaskModelInterface; +} + namespace Widgets { class FilterWidget; class PageTreeView; class PageView : public QWidget { Q_OBJECT public: explicit PageView(QWidget *parent = Q_NULLPTR); QHash globalActions() const; QObject *model() const; + Presentation::RunningTaskModelInterface *runningTaskModel() const; MessageBoxInterface::Ptr messageBoxInterface() const; QModelIndexList selectedIndexes() const; public slots: void setModel(QObject *model); + void setRunningTaskModel(Presentation::RunningTaskModelInterface *model); void setMessageBoxInterface(const MessageBoxInterface::Ptr &interface); void displayErrorMessage(const QString &message); signals: void currentArtifactChanged(const Domain::Artifact::Ptr &artifact); private slots: void onReturnPressed(); void onAddItemRequested(); void onRemoveItemRequested(); void onPromoteItemRequested(); void onFilterToggled(bool show); void onCurrentChanged(const QModelIndex ¤t); + void onRunTaskTriggered(); + void onRunningTaskChanged(const Domain::Task::Ptr &task); private: bool eventFilter(QObject *object, QEvent *event) Q_DECL_OVERRIDE; + void updateRunTaskAction(); + Domain::Artifact::Ptr currentArtifact() const; QHash m_actions; QAction *m_cancelAction; + QAction *m_runTaskAction; QObject *m_model; KMessageWidget *m_messageWidget; FilterWidget *m_filterWidget; PageTreeView *m_centralView; QLineEdit *m_quickAddEdit; MessageBoxInterface::Ptr m_messageBoxInterface; + Presentation::RunningTaskModelInterface *m_runningTaskModel; }; } #endif // WIDGETS_PAGEVIEW_H diff --git a/src/widgets/runningtaskwidget.cpp b/src/widgets/runningtaskwidget.cpp new file mode 100644 index 00000000..13a0eb02 --- /dev/null +++ b/src/widgets/runningtaskwidget.cpp @@ -0,0 +1,138 @@ +/* This file is part of Zanshin + + Copyright 2016 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 "runningtaskwidget.h" +#include "runningtaskmodelinterface.h" +#include +#include +#include +#include +#include +#include + +using namespace Widgets; + +RunningTaskWidget::RunningTaskWidget(QWidget *parent) + : QWidget(parent), + m_layout(new QHBoxLayout(this)), + m_titleLabel(new QLabel(this)), + m_stopButton(new QPushButton(this)), + m_doneButton(new QPushButton(this)), + m_collapsed(false) +{ + // BypassWindowManagerHint allows to prevent the window from showing up in Alt-Tab + // This means no way to focus it with the keyboard, though, obviously. + + setWindowFlags(Qt::Window | Qt::BypassWindowManagerHint | Qt::FramelessWindowHint); + KWindowSystem::setOnAllDesktops(winId(), true); + KWindowSystem::setState(winId(), NET::KeepAbove | NET::SkipTaskbar | NET::SkipPager); + + setWindowTitle(tr("Zanshin Running Task Banner")); + + // Current idea for a good background color: + // the selection color, i.e. usually blue. Arguable ;) + QPalette pal; + pal.setBrush(QPalette::Background, pal.brush(QPalette::Highlight)); + setPalette(pal); + setAutoFillBackground(true); + + m_stopButton->setObjectName(QStringLiteral("stopButton")); + m_stopButton->setText(tr("Stop")); + connect(m_stopButton, &QAbstractButton::clicked, this, &RunningTaskWidget::onTaskRunStopped); + + m_doneButton->setObjectName(QStringLiteral("doneButton")); + m_doneButton->setText(tr("Done")); + connect(m_doneButton, &QAbstractButton::clicked, this, &RunningTaskWidget::onTaskRunDone); + + m_layout->setContentsMargins(0, 0, 0, 0); + m_layout->addWidget(m_stopButton); + m_layout->addWidget(m_titleLabel, 1, Qt::AlignCenter); + m_layout->addWidget(m_doneButton); + + setCollapsed(true); +} + +void RunningTaskWidget::setModel(Presentation::RunningTaskModelInterface *model) +{ + Q_ASSERT(model); + m_model = model; + connect(m_model, &Presentation::RunningTaskModelInterface::runningTaskChanged, + this, &RunningTaskWidget::onRunningTaskChanged); +} + +void RunningTaskWidget::setCollapsed(bool b) +{ + if (m_collapsed == b) { + return; + } + m_collapsed = b; + m_stopButton->setVisible(!b); + m_titleLabel->setVisible(!b); + m_doneButton->setVisible(!b); + m_layout->activate(); + resize(); +} + +void RunningTaskWidget::enterEvent(QEvent *) +{ + setCollapsed(false); +} + +void RunningTaskWidget::leaveEvent(QEvent *) +{ + setCollapsed(true); +} + +void RunningTaskWidget::onRunningTaskChanged(const Domain::Task::Ptr &task) +{ + if (task) { + m_titleLabel->setText(task->title()); + resize(); + show(); + } else { + hide(); + } +} + +void RunningTaskWidget::onTaskRunStopped() +{ + QMetaObject::invokeMethod(m_model, "stopTask"); +} + +void RunningTaskWidget::onTaskRunDone() +{ + QMetaObject::invokeMethod(m_model, "doneTask"); +} + +void RunningTaskWidget::resize() +{ + const auto screenGeometry = qApp->desktop()->availableGeometry(this); + const int screenWidth = screenGeometry.width(); + const int height = m_collapsed ? 5 : sizeHint().height(); + setGeometry(QRect(screenGeometry.left(), screenGeometry.top(), screenWidth, height)); +} + +Presentation::RunningTaskModelInterface *RunningTaskWidget::model() const +{ + return m_model; +} diff --git a/src/widgets/runningtaskwidget.h b/src/widgets/runningtaskwidget.h new file mode 100644 index 00000000..e0e98877 --- /dev/null +++ b/src/widgets/runningtaskwidget.h @@ -0,0 +1,76 @@ +/* This file is part of Zanshin + + Copyright 2016 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 RUNNINGTASKWIDGET_H +#define RUNNINGTASKWIDGET_H + +#include +#include "domain/task.h" + +class QLabel; +class QHBoxLayout; +class QPushButton; + +namespace Presentation { +class RunningTaskModelInterface; +} + +namespace Widgets { + +class RunningTaskWidget : public QWidget +{ + Q_OBJECT +public: + explicit RunningTaskWidget(QWidget *parent = Q_NULLPTR); + + void setModel(Presentation::RunningTaskModelInterface *model); + + Presentation::RunningTaskModelInterface *model() const; + +private slots: + // connected to the model + void onRunningTaskChanged(const Domain::Task::Ptr &task); + // connected to the push buttons + void onTaskRunStopped(); + void onTaskRunDone(); + + void setCollapsed(bool b); + +protected: + void enterEvent(QEvent *ev) Q_DECL_OVERRIDE; + void leaveEvent(QEvent *ev) Q_DECL_OVERRIDE; + +private: + void resize(); + + Presentation::RunningTaskModelInterface *m_model; + QHBoxLayout *m_layout; + QLabel *m_titleLabel; + QPushButton *m_stopButton; + QPushButton *m_doneButton; + bool m_collapsed; +}; + +} + +#endif // RUNNINGTASKWIDGET_H diff --git a/src/widgets/taskapplicationcomponents.cpp b/src/widgets/taskapplicationcomponents.cpp new file mode 100644 index 00000000..faf92777 --- /dev/null +++ b/src/widgets/taskapplicationcomponents.cpp @@ -0,0 +1,66 @@ +/* This file is part of Zanshin + + Copyright 2016-2017 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 "taskapplicationcomponents.h" + +#include "pageview.h" +#include "runningtaskwidget.h" +#include "runningtaskmodelinterface.h" + +using namespace Widgets; +using namespace Presentation; + +TaskApplicationComponents::TaskApplicationComponents(QWidget *parent) + : ApplicationComponents(parent), + m_runningTaskWidget(new RunningTaskWidget(parentWidget())) +{ +} + +TaskApplicationComponents::~TaskApplicationComponents() +{ +} + +void TaskApplicationComponents::setModel(const QObjectPtr &model) +{ + ApplicationComponents::setModel(model); + RunningTaskModelInterface *runningTaskModel = model ? model->property("runningTaskModel").value() + : nullptr; + m_runningTaskWidget->setModel(runningTaskModel); + if (m_pageView) { + m_pageView->setRunningTaskModel(runningTaskModel); + } +} + +PageView *TaskApplicationComponents::pageView() const +{ + auto pageView = ApplicationComponents::pageView(); + pageView->setRunningTaskModel(model() ? model()->property("runningTaskModel").value() + : nullptr); + return pageView; +} + +// Only used by the unittest +RunningTaskWidget *TaskApplicationComponents::runningTaskWidget() const +{ + return m_runningTaskWidget; +} diff --git a/src/widgets/taskapplicationcomponents.h b/src/widgets/taskapplicationcomponents.h new file mode 100644 index 00000000..0531922d --- /dev/null +++ b/src/widgets/taskapplicationcomponents.h @@ -0,0 +1,51 @@ +/* This file is part of Zanshin + + Copyright 2016-2017 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 WIDGETS_TASKAPPLICATIONCOMPONENTS_H +#define WIDGETS_TASKAPPLICATIONCOMPONENTS_H + +#include "applicationcomponents.h" + +namespace Widgets { + +class RunningTaskWidget; + +class TaskApplicationComponents : public ApplicationComponents +{ +public: + explicit TaskApplicationComponents(QWidget *parent = Q_NULLPTR); + ~TaskApplicationComponents(); + + virtual void setModel(const QObjectPtr &model) Q_DECL_OVERRIDE; + + virtual PageView *pageView() const; + + RunningTaskWidget *runningTaskWidget() const; + +private: + RunningTaskWidget *m_runningTaskWidget; +}; + +} + +#endif // WIDGETS_TASKAPPLICATIONCOMPONENTS_H diff --git a/src/zanshin/app/dependencies.cpp b/src/zanshin/app/dependencies.cpp index 0e1075db..1ca0aa8c 100644 --- a/src/zanshin/app/dependencies.cpp +++ b/src/zanshin/app/dependencies.cpp @@ -1,120 +1,125 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 "dependencies.h" #include "akonadi/akonadicontextqueries.h" #include "akonadi/akonadicontextrepository.h" #include "akonadi/akonadidatasourcequeries.h" #include "akonadi/akonadidatasourcerepository.h" #include "akonadi/akonadiprojectqueries.h" #include "akonadi/akonadiprojectrepository.h" #include "akonadi/akonaditaskqueries.h" #include "akonadi/akonaditaskrepository.h" #include "akonadi/akonadimessaging.h" #include "akonadi/akonadimonitorimpl.h" #include "akonadi/akonadiserializer.h" #include "akonadi/akonadistorage.h" #include "presentation/artifacteditormodel.h" #include "presentation/availablesourcesmodel.h" #include "presentation/availabletaskpagesmodel.h" +#include "presentation/runningtaskmodel.h" #include "utils/dependencymanager.h" void App::initializeDependencies() { auto &deps = Utils::DependencyManager::globalInstance(); deps.add(); deps.add(); deps.add(); deps.add(); deps.add(); deps.add(); deps.add([] (Utils::DependencyManager *deps) { return new Akonadi::DataSourceQueries(Akonadi::StorageInterface::Tasks, deps->create(), deps->create(), deps->create()); }); deps.add(); deps.add(); deps.add(); deps.add(); deps.add(); deps.add([] (Utils::DependencyManager *deps) { auto model = new Presentation::ArtifactEditorModel; auto repository = deps->create(); model->setSaveFunction([repository] (const Domain::Artifact::Ptr &artifact) { auto task = artifact.objectCast(); Q_ASSERT(task); return repository->update(task); }); model->setDelegateFunction([repository] (const Domain::Task::Ptr &task, const Domain::Task::Delegate &delegate) { return repository->delegate(task, delegate); }); return model; }); deps.add(); deps.add(); + + deps.add(); } diff --git a/src/zanshin/app/main.cpp b/src/zanshin/app/main.cpp index 400a1daf..fbe5f56b 100644 --- a/src/zanshin/app/main.cpp +++ b/src/zanshin/app/main.cpp @@ -1,115 +1,115 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 #include #include #include #include #include -#include "widgets/applicationcomponents.h" +#include "widgets/taskapplicationcomponents.h" #include "widgets/availablepagesview.h" #include "widgets/availablesourcesview.h" #include "widgets/editorview.h" #include "widgets/pageview.h" -#include "presentation/applicationmodel.h" +#include "presentation/taskapplicationmodel.h" #include "aboutdata.h" #include "dependencies.h" #include #include #include #include int main(int argc, char **argv) { QApplication app(argc, argv); App::initializeDependencies(); auto aboutData = App::getAboutData(); QCommandLineParser parser; KAboutData::setApplicationData(aboutData); parser.addVersionOption(); parser.addHelpOption(); aboutData.setupCommandLine(&parser); parser.process(app); aboutData.processCommandLine(&parser); auto widget = new QWidget; - auto components = new Widgets::ApplicationComponents(widget); - components->setModel(Presentation::ApplicationModel::Ptr::create()); + auto components = new Widgets::TaskApplicationComponents(widget); + components->setModel(Presentation::TaskApplicationModel::Ptr::create()); auto layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(components->pageView()); widget->setLayout(layout); auto sourcesDock = new QDockWidget(QObject::tr("Sources")); sourcesDock->setObjectName(QStringLiteral("sourcesDock")); sourcesDock->setWidget(components->availableSourcesView()); auto pagesDock = new QDockWidget(QObject::tr("Pages")); pagesDock->setObjectName(QStringLiteral("pagesDock")); pagesDock->setWidget(components->availablePagesView()); auto editorDock = new QDockWidget(QObject::tr("Editor")); editorDock->setObjectName(QStringLiteral("editorDock")); editorDock->setWidget(components->editorView()); auto window = new KXmlGuiWindow; window->setCentralWidget(widget); window->addDockWidget(Qt::RightDockWidgetArea, editorDock); window->addDockWidget(Qt::LeftDockWidgetArea, pagesDock); window->addDockWidget(Qt::LeftDockWidgetArea, sourcesDock); auto actions = components->globalActions(); actions.insert(QStringLiteral("dock_sources"), sourcesDock->toggleViewAction()); actions.insert(QStringLiteral("dock_pages"), pagesDock->toggleViewAction()); actions.insert(QStringLiteral("dock_editor"), editorDock->toggleViewAction()); auto ac = window->actionCollection(); ac->addAction(KStandardAction::Quit, window, SLOT(close())); for (auto it = actions.constBegin(); it != actions.constEnd(); ++it) { auto shortcut = it.value()->shortcut(); if (!shortcut.isEmpty()) { ac->setDefaultShortcut(it.value(), shortcut); } ac->addAction(it.key(), it.value()); } window->setupGUI(QSize(1024, 600), KXmlGuiWindow::ToolBar | KXmlGuiWindow::Keys | KXmlGuiWindow::Save | KXmlGuiWindow::Create); window->show(); return app.exec(); } diff --git a/src/zanshin/app/zanshinui.rc b/src/zanshin/app/zanshinui.rc index 299dc9d4..53143314 100644 --- a/src/zanshin/app/zanshinui.rc +++ b/src/zanshin/app/zanshinui.rc @@ -1,41 +1,43 @@ - + &Go &Panels Main Toolbar + + diff --git a/src/zanshin/kontact/part.cpp b/src/zanshin/kontact/part.cpp index 9b284f90..0296852a 100644 --- a/src/zanshin/kontact/part.cpp +++ b/src/zanshin/kontact/part.cpp @@ -1,92 +1,92 @@ /* This file is part of Zanshin Todo. Copyright 2011 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 "part.h" #include #include #include #include #include #include #include "../app/aboutdata.h" #include "../app/dependencies.h" -#include "presentation/applicationmodel.h" +#include "presentation/taskapplicationmodel.h" -#include "widgets/applicationcomponents.h" +#include "widgets/taskapplicationcomponents.h" #include "widgets/availablepagesview.h" #include "widgets/availablesourcesview.h" #include "widgets/editorview.h" #include "widgets/pageview.h" #include "utils/dependencymanager.h" K_PLUGIN_FACTORY(PartFactory, registerPlugin();) Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList &) : KParts::ReadOnlyPart(parent) { App::initializeDependencies(); setComponentName(QStringLiteral("zanshin"), QStringLiteral("zanshin")); auto splitter = new QSplitter(parentWidget); auto sidebar = new QSplitter(Qt::Vertical, parentWidget); - auto components = new Widgets::ApplicationComponents(parentWidget); - components->setModel(Presentation::ApplicationModel::Ptr::create()); + auto components = new Widgets::TaskApplicationComponents(parentWidget); + components->setModel(Presentation::TaskApplicationModel::Ptr::create()); sidebar->addWidget(components->availablePagesView()); sidebar->addWidget(components->availableSourcesView()); splitter->addWidget(sidebar); splitter->addWidget(components->pageView()); splitter->addWidget(components->editorView()); setWidget(splitter); auto actions = components->globalActions(); auto ac = actionCollection(); for (auto it = actions.constBegin(); it != actions.constEnd(); ++it) { auto shortcut = it.value()->shortcut(); if (!shortcut.isEmpty()) { ac->setDefaultShortcut(it.value(), shortcut); } ac->addAction(it.key(), it.value()); } setXMLFile(QStringLiteral("zanshin_part.rc"), true); } Part::~Part() { } bool Part::openFile() { return false; } #include "part.moc" diff --git a/tests/units/akonadi/akonadiserializertest.cpp b/tests/units/akonadi/akonadiserializertest.cpp index 7d7ea5a0..50226809 100644 --- a/tests/units/akonadi/akonadiserializertest.cpp +++ b/tests/units/akonadi/akonadiserializertest.cpp @@ -1,2467 +1,2493 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens Copyright 2014 RĂ©mi Benoit This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 #include "akonadi/akonadiserializer.h" #include "akonadi/akonadiapplicationselectedattribute.h" #include "akonadi/akonaditimestampattribute.h" #include #include #include #include #include #include #include Q_DECLARE_METATYPE(Akonadi::Item*) class AkonadiSerializerTest : public QObject { Q_OBJECT private slots: void shouldKnowWhenAnObjectRepresentsACollection() { // GIVEN Akonadi::Serializer serializer; auto object = Akonadi::Serializer::QObjectPtr::create(); Akonadi::Collection collection(42); // WHEN // Nothing yet // THEN QVERIFY(!serializer.representsCollection(object, collection)); // WHEN object->setProperty("collectionId", 42); // THEN QVERIFY(serializer.representsCollection(object, collection)); // WHEN object->setProperty("collectionId", 43); // THEN QVERIFY(!serializer.representsCollection(object, collection)); } void shouldKnowWhenAnObjectRepresentsAnItem() { // GIVEN Akonadi::Serializer serializer; auto object = Akonadi::Serializer::QObjectPtr::create(); Akonadi::Item item(42); // WHEN // Nothing yet // THEN QVERIFY(!serializer.representsItem(object, item)); // WHEN object->setProperty("itemId", 42); // THEN QVERIFY(serializer.representsItem(object, item)); // WHEN object->setProperty("itemId", 43); // THEN QVERIFY(!serializer.representsItem(object, item)); } void shouldKnowWhenAnAkonadiTagRepresentsATag() { // GIVEN Akonadi::Serializer serializer; Akonadi::Tag akondiTag(42); auto tag = Domain::Tag::Ptr::create(); // WHEN // Nothing yet // THEN QVERIFY(!serializer.representsAkonadiTag(tag, akondiTag)); // WHEN tag->setProperty("tagId", 42); // THEN QVERIFY(serializer.representsAkonadiTag(tag, akondiTag)); // WHEN tag->setProperty("tagId", 43); // THEN QVERIFY(!serializer.representsAkonadiTag(tag, akondiTag)); } void shouldKnowObjectUid() { // GIVEN Akonadi::Serializer serializer; auto object = Akonadi::Serializer::QObjectPtr::create(); // WHEN object->setProperty("todoUid", "my-uid"); // THEN QCOMPARE(serializer.objectUid(object), QStringLiteral("my-uid")); } void shouldCreateDataSourceFromCollection_data() { QTest::addColumn("name"); QTest::addColumn("iconName"); QTest::addColumn("mimeTypes"); QTest::addColumn("hasSelectedAttribute"); QTest::addColumn("isSelected"); QTest::addColumn("isReferenced"); QTest::addColumn("isEnabled"); const auto noteMimeTypes = QStringList() << QStringLiteral("text/x-vnd.akonadi.note"); const auto taskMimeTypes = QStringList() << QStringLiteral("application/x-vnd.akonadi.calendar.todo"); const auto bogusMimeTypes = QStringList() << QStringLiteral("foo/bar"); const auto allMimeTypes = noteMimeTypes + taskMimeTypes + bogusMimeTypes; QTest::newRow("nominal case") << "name" << "icon" << allMimeTypes << true << false << false << true; QTest::newRow("only notes") << "name" << "icon" << noteMimeTypes << true << false << false << true; QTest::newRow("only tasks") << "name" << "icon" << taskMimeTypes << true << false << false << true; QTest::newRow("only bogus") << "name" << "icon" << bogusMimeTypes << true << false << false << true; QTest::newRow("no selected attribute") << "name" << "icon" << allMimeTypes << false << false << false << true; QTest::newRow("selected attribute (false)") << "name" << "icon" << allMimeTypes << true << false << false << true; QTest::newRow("selected attribute (true)") << "name" << "icon" << allMimeTypes << true << true << false << true; QTest::newRow("enabled and referenced") << "name" << "icon" << allMimeTypes << true << false << true << true; QTest::newRow("enabled and !referenced") << "name" << "icon" << allMimeTypes << true << false << true << false; QTest::newRow("!enabled and referenced") << "name" << "icon" << allMimeTypes << true << false << false << true; QTest::newRow("!enabled and !referenced") << "name" << "icon" << allMimeTypes << true << false << false << false; QTest::newRow("empty case") << QString() << QString() << QStringList() << false << false << false << true; } void shouldCreateDataSourceFromCollection() { // GIVEN // Data... QFETCH(QString, name); QFETCH(QString, iconName); QFETCH(QStringList, mimeTypes); QFETCH(bool, hasSelectedAttribute); QFETCH(bool, isSelected); QFETCH(bool, isReferenced); QFETCH(bool, isEnabled); Domain::DataSource::ContentTypes expectedContentTypes; if (mimeTypes.contains(QStringLiteral("text/x-vnd.akonadi.note"))) { expectedContentTypes |= Domain::DataSource::Notes; } if (mimeTypes.contains(QStringLiteral("application/x-vnd.akonadi.calendar.todo"))) { expectedContentTypes |= Domain::DataSource::Tasks; } // ... stored in a collection Akonadi::Collection collection(42); collection.setContentMimeTypes(mimeTypes); collection.setName(name); collection.setReferenced(isReferenced); collection.setEnabled(isEnabled); auto displayAttribute = new Akonadi::EntityDisplayAttribute; displayAttribute->setIconName(iconName); collection.addAttribute(displayAttribute); if (hasSelectedAttribute) { auto selectedAttribute = new Akonadi::ApplicationSelectedAttribute; selectedAttribute->setSelected(isSelected); collection.addAttribute(selectedAttribute); } // WHEN Akonadi::Serializer serializer; auto dataSource = serializer.createDataSourceFromCollection(collection, Akonadi::SerializerInterface::BaseName); // THEN QCOMPARE(dataSource->name(), name); QCOMPARE(dataSource->iconName(), iconName); QCOMPARE(dataSource->contentTypes(), expectedContentTypes); QCOMPARE(dataSource->isSelected(), !hasSelectedAttribute || isSelected); QCOMPARE(dataSource->property("collectionId").value(), collection.id()); QCOMPARE((dataSource->listStatus() & Domain::DataSource::Listed) != 0, isReferenced || isEnabled); QCOMPARE((dataSource->listStatus() == Domain::DataSource::Bookmarked), isEnabled); } void shouldCreateNullDataSourceFromInvalidCollection() { // GIVEN Akonadi::Collection collection; // WHEN Akonadi::Serializer serializer; auto dataSource = serializer.createDataSourceFromCollection(collection, Akonadi::SerializerInterface::BaseName); // THEN QVERIFY(dataSource.isNull()); } void shouldUpdateDataSourceFromCollection_data() { QTest::addColumn("updatedName"); QTest::newRow("no change") << "name"; QTest::newRow("changed") << "new name"; } void shouldUpdateDataSourceFromCollection() { // GIVEN // A collection... Akonadi::Collection originalCollection(42); originalCollection.setName(QStringLiteral("name")); // ... deserialized as a data source Akonadi::Serializer serializer; auto dataSource = serializer.createDataSourceFromCollection(originalCollection, Akonadi::SerializerInterface::BaseName); // WHEN // Data... QFETCH(QString, updatedName); // ... in a new collection Akonadi::Collection updatedCollection(42); updatedCollection.setName(updatedName); serializer.updateDataSourceFromCollection(dataSource, updatedCollection, Akonadi::SerializerInterface::BaseName); // THEN QCOMPARE(dataSource->name(), updatedName); } void shouldNotUpdateDataSourceFromInvalidCollection() { // GIVEN // Data... const QString name = QStringLiteral("name"); // ... stored in a collection... Akonadi::Collection originalCollection(42); originalCollection.setName(name); // ... deserialized as a data source Akonadi::Serializer serializer; auto dataSource = serializer.createDataSourceFromCollection(originalCollection, Akonadi::SerializerInterface::BaseName); // WHEN Akonadi::Collection invalidCollection; invalidCollection.setName(QStringLiteral("foo")); serializer.updateDataSourceFromCollection(dataSource, invalidCollection, Akonadi::SerializerInterface::BaseName); // THEN QCOMPARE(dataSource->name(), name); } void shouldNameDataSourceFromCollectionPathIfRequested() { // GIVEN // Data... const QString name = QStringLiteral("name"); const QString parentName = QStringLiteral("parent"); // ... stored in a collection with a parent Akonadi::Collection collection(42); collection.setName(name); Akonadi::Collection parentCollection(41); parentCollection.setName(QStringLiteral("Foo")); auto attribute = new Akonadi::EntityDisplayAttribute; attribute->setDisplayName(parentName); parentCollection.addAttribute(attribute); collection.setParentCollection(parentCollection); // WHEN Akonadi::Serializer serializer; auto dataSource1 = serializer.createDataSourceFromCollection(collection, Akonadi::SerializerInterface::FullPath); auto dataSource2 = serializer.createDataSourceFromCollection(collection, Akonadi::SerializerInterface::BaseName); // Give it another try with the root parentCollection.setParentCollection(Akonadi::Collection::root()); collection.setParentCollection(parentCollection); auto dataSource3 = serializer.createDataSourceFromCollection(collection, Akonadi::SerializerInterface::FullPath); auto dataSource4 = serializer.createDataSourceFromCollection(collection, Akonadi::SerializerInterface::BaseName); // THEN QCOMPARE(dataSource1->name(), QString(parentName + "/" + name)); QCOMPARE(dataSource2->name(), name); QCOMPARE(dataSource3->name(), QString(parentName + "/" + name)); QCOMPARE(dataSource4->name(), name); } void shouldCreateCollectionFromDataSource_data() { QTest::addColumn("name"); QTest::addColumn("iconName"); QTest::addColumn("contentTypes"); QTest::addColumn("isSelected"); QTest::addColumn("listStatus"); const auto noType = Domain::DataSource::ContentTypes(Domain::DataSource::NoContent); const auto taskType = Domain::DataSource::ContentTypes(Domain::DataSource::Tasks); const auto noteType = Domain::DataSource::ContentTypes(Domain::DataSource::Notes); const auto allTypes = taskType | noteType; const auto unlisted = Domain::DataSource::Unlisted; const auto listed = Domain::DataSource::Listed; const auto bookmarked = Domain::DataSource::Bookmarked; QTest::newRow("nominal case") << "name" << "icon-name" << allTypes << true << unlisted; QTest::newRow("only notes") << "name" << "icon-name" << noteType << true << unlisted; QTest::newRow("only tasks") << "name" << "icon-name" << taskType << true << unlisted; QTest::newRow("only nothing ;)") << "name" << "icon-name" << noType << true << unlisted; QTest::newRow("not selected") << "name" << "icon-name" << allTypes << false << unlisted; QTest::newRow("selected") << "name" << "icon-name" << allTypes << true << unlisted; QTest::newRow("unlisted") << "name" << "icon-name" << allTypes << true << unlisted; QTest::newRow("listed") << "name" << "icon-name" << allTypes << true << listed; QTest::newRow("bookmarked") << "name" << "icon-name" << allTypes << true << bookmarked; QTest::newRow("empty case") << QString() << QString() << noType << true << unlisted; } void shouldCreateCollectionFromDataSource() { // GIVEN const auto timestamp = QDateTime::currentMSecsSinceEpoch(); // Data... QFETCH(QString, name); QFETCH(QString, iconName); QFETCH(Domain::DataSource::ContentTypes, contentTypes); QFETCH(bool, isSelected); QFETCH(Domain::DataSource::ListStatus, listStatus); QStringList mimeTypes; if (contentTypes & Domain::DataSource::Tasks) mimeTypes << QStringLiteral("application/x-vnd.akonadi.calendar.todo"); if (contentTypes & Domain::DataSource::Notes) mimeTypes << QStringLiteral("text/x-vnd.akonadi.note"); // ... stored in a data source auto source = Domain::DataSource::Ptr::create(); source->setName(name); source->setIconName(iconName); source->setContentTypes(contentTypes); source->setListStatus(listStatus); source->setSelected(isSelected); source->setProperty("collectionId", 42); // WHEN Akonadi::Serializer serializer; auto collection = serializer.createCollectionFromDataSource(source); // THEN QCOMPARE(collection.id(), source->property("collectionId").value()); QVERIFY(collection.hasAttribute()); QCOMPARE(collection.attribute()->isSelected(), isSelected); QVERIFY(collection.hasAttribute()); QVERIFY(collection.attribute()->timestamp() >= timestamp); switch (listStatus) { case Domain::DataSource::Unlisted: QVERIFY(!collection.referenced()); QVERIFY(!collection.enabled()); break; case Domain::DataSource::Listed: QVERIFY(collection.referenced()); QVERIFY(!collection.enabled()); break; case Domain::DataSource::Bookmarked: QVERIFY(collection.enabled()); break; default: qFatal("Shouldn't happen"); break; } } void shouldVerifyIfCollectionIsListed_data() { QTest::addColumn("isEnabled"); QTest::addColumn("isReferenced"); QTest::addColumn("expectedListed"); QTest::newRow("enabled and referenced") << true << true << true; QTest::newRow("enabled and !referenced") << true << false << true; QTest::newRow("!enabled and referenced") << false << true << true; QTest::newRow("!enabled and !referenced") << false << false << false; } void shouldVerifyIfCollectionIsListed() { // GIVEN QFETCH(bool, isEnabled); QFETCH(bool, isReferenced); // ... stored in a collection Akonadi::Collection collection(42); collection.setReferenced(isReferenced); collection.setEnabled(isEnabled); // WHEN Akonadi::Serializer serializer; // THEN QFETCH(bool, expectedListed); QCOMPARE(serializer.isListedCollection(collection), expectedListed); } void shouldVerifyIfCollectionIsSelected_data() { QTest::addColumn("mimeTypes"); QTest::addColumn("hasSelectedAttribute"); QTest::addColumn("isSelected"); QTest::addColumn("isReferenced"); QTest::addColumn("isEnabled"); QTest::addColumn("expectedSelected"); const auto noteMimeTypes = QStringList() << QStringLiteral("text/x-vnd.akonadi.note"); const auto taskMimeTypes = QStringList() << QStringLiteral("application/x-vnd.akonadi.calendar.todo"); const auto bogusMimeTypes = QStringList() << QStringLiteral("foo/bar"); const auto allMimeTypes = noteMimeTypes + taskMimeTypes + bogusMimeTypes; QTest::newRow("nominal case") << allMimeTypes << true << false << false << true << false; QTest::newRow("only notes") << noteMimeTypes << true << false << false << true << false; QTest::newRow("only tasks") << taskMimeTypes << true << false << false << true << false; QTest::newRow("only bogus") << bogusMimeTypes << true << false << false << true << false; QTest::newRow("selected, only notes") << noteMimeTypes << true << true << false << true << true; QTest::newRow("selected, only tasks") << taskMimeTypes << true << true << false << true << true; QTest::newRow("selected, only bogus") << bogusMimeTypes << true << true << false << true << false; QTest::newRow("no selected attribute") << allMimeTypes << false << false << false << true << true; QTest::newRow("selected attribute (false)") << allMimeTypes << true << false << false << true << false; QTest::newRow("selected attribute (true)") << allMimeTypes << true << true << false << true << true; QTest::newRow("enabled and referenced") << allMimeTypes << true << false << true << true << false; QTest::newRow("enabled and !referenced") << allMimeTypes << true << false << true << false << false; QTest::newRow("!enabled and referenced") << allMimeTypes << true << false << false << true << false; QTest::newRow("!enabled and !referenced") << allMimeTypes << true << false << false << false << false; QTest::newRow("selected, enabled and referenced") << allMimeTypes << true << true << true << true << true; QTest::newRow("selected, enabled and !referenced") << allMimeTypes << true << true << true << false << true; QTest::newRow("selected, !enabled and referenced") << allMimeTypes << true << true << false << true << true; QTest::newRow("selected, !enabled and !referenced") << allMimeTypes << true << true << false << false << false; QTest::newRow("empty case") << QStringList() << false << false << false << true << false; } void shouldVerifyIfCollectionIsSelected() { // GIVEN QFETCH(QStringList, mimeTypes); QFETCH(bool, hasSelectedAttribute); QFETCH(bool, isSelected); QFETCH(bool, isReferenced); QFETCH(bool, isEnabled); Domain::DataSource::ContentTypes expectedContentTypes; if (mimeTypes.contains(QStringLiteral("text/x-vnd.akonadi.note"))) { expectedContentTypes |= Domain::DataSource::Notes; } if (mimeTypes.contains(QStringLiteral("application/x-vnd.akonadi.calendar.todo"))) { expectedContentTypes |= Domain::DataSource::Tasks; } // ... stored in a collection Akonadi::Collection collection(42); collection.setContentMimeTypes(mimeTypes); collection.setReferenced(isReferenced); collection.setEnabled(isEnabled); if (hasSelectedAttribute) { auto selectedAttribute = new Akonadi::ApplicationSelectedAttribute; selectedAttribute->setSelected(isSelected); collection.addAttribute(selectedAttribute); } // WHEN Akonadi::Serializer serializer; // THEN QFETCH(bool, expectedSelected); QCOMPARE(serializer.isSelectedCollection(collection), expectedSelected); } void shouldVerifyCollectionContents_data() { QTest::addColumn("mimeType"); QTest::addColumn("expectedNotes"); QTest::addColumn("expectedTasks"); QTest::newRow("task collection") << "application/x-vnd.akonadi.calendar.todo" << false << true; QTest::newRow("note collection") << "text/x-vnd.akonadi.note" << true << false; } void shouldVerifyCollectionContents() { // GIVEN // Data... QFETCH(QString, mimeType); // ... stored in a collection Akonadi::Collection collection(42); collection.setContentMimeTypes(QStringList() << mimeType); // WHEN Akonadi::Serializer serializer; QFETCH(bool, expectedNotes); QFETCH(bool, expectedTasks); // THEN QCOMPARE(serializer.isNoteCollection(collection), expectedNotes); QCOMPARE(serializer.isTaskCollection(collection), expectedTasks); } void shouldCreateTaskFromItem_data() { QTest::addColumn("summary"); QTest::addColumn("content"); QTest::addColumn("isDone"); QTest::addColumn("doneDate"); QTest::addColumn("startDate"); QTest::addColumn("dueDate"); QTest::addColumn("delegateName"); QTest::addColumn("delegateEmail"); QTest::newRow("nominal case") << "summary" << "content" << false << QDateTime() << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) << "John Doe" << "j@d.com"; QTest::newRow("done case") << "summary" << "content" << true << QDateTime(QDate(2013, 11, 30)) << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) << "John Doe" << "j@d.com"; QTest::newRow("done without doneDate case") << "summary" << "content" << true << QDateTime() << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) << "John Doe" << "j@d.com"; QTest::newRow("empty case") << QString() << QString() << false << QDateTime() << QDateTime() << QDateTime() << QString() << QString(); } void shouldCreateTaskFromItem() { // GIVEN // Data... QFETCH(QString, summary); QFETCH(QString, content); QFETCH(bool, isDone); QFETCH(QDateTime, doneDate); QFETCH(QDateTime, startDate); QFETCH(QDateTime, dueDate); QFETCH(QString, delegateName); QFETCH(QString, delegateEmail); // Switch to UTC doneDate.setTimeSpec(Qt::UTC); startDate.setTimeSpec(Qt::UTC); dueDate.setTimeSpec(Qt::UTC); // ... stored in a todo... KCalCore::Todo::Ptr todo(new KCalCore::Todo); todo->setSummary(summary); todo->setDescription(content); if (isDone) todo->setCompleted(KDateTime(doneDate)); else todo->setCompleted(isDone); todo->setDtStart(KDateTime(startDate, KDateTime::UTC)); todo->setDtDue(KDateTime(dueDate, KDateTime::UTC)); todo->setRelatedTo(QStringLiteral("my-uid")); if (!delegateName.isEmpty() || !delegateEmail.isEmpty()) { KCalCore::Attendee::Ptr attendee(new KCalCore::Attendee(delegateName, delegateEmail, true, KCalCore::Attendee::Accepted)); todo->addAttendee(attendee); } // ... as payload of an item Akonadi::Item item; item.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); item.setPayload(todo); // which has a parent collection Akonadi::Collection collection(43); item.setParentCollection(collection); // WHEN Akonadi::Serializer serializer; Domain::Task::Ptr task = serializer.createTaskFromItem(item); auto artifact = serializer.createArtifactFromItem(item).dynamicCast(); // THEN QCOMPARE(task->title(), summary); QCOMPARE(task->text(), content); QCOMPARE(task->isDone(), isDone); QCOMPARE(task->doneDate(), doneDate); QCOMPARE(task->startDate(), startDate); QCOMPARE(task->dueDate(), dueDate); QCOMPARE(task->property("todoUid").toString(), todo->uid()); QCOMPARE(task->property("relatedUid").toString(), todo->relatedTo()); QCOMPARE(task->property("itemId").toLongLong(), item.id()); QCOMPARE(task->property("parentCollectionId").toLongLong(), collection.id()); QCOMPARE(task->delegate().name(), delegateName); QCOMPARE(task->delegate().email(), delegateEmail); QVERIFY(!artifact.isNull()); QCOMPARE(artifact->title(), summary); QCOMPARE(artifact->text(), content); QCOMPARE(artifact->isDone(), isDone); QCOMPARE(artifact->doneDate(), doneDate); QCOMPARE(artifact->startDate(), startDate); QCOMPARE(artifact->dueDate(), dueDate); QCOMPARE(artifact->property("todoUid").toString(), todo->uid()); QCOMPARE(artifact->property("relatedUid").toString(), todo->relatedTo()); QCOMPARE(artifact->property("itemId").toLongLong(), item.id()); QCOMPARE(artifact->property("parentCollectionId").toLongLong(), collection.id()); QCOMPARE(artifact->delegate().name(), delegateName); QCOMPARE(artifact->delegate().email(), delegateEmail); } void shouldCreateNullTaskFromInvalidItem() { // GIVEN Akonadi::Item item; // WHEN Akonadi::Serializer serializer; Domain::Task::Ptr task = serializer.createTaskFromItem(item); auto artifact = serializer.createArtifactFromItem(item); // THEN QVERIFY(task.isNull()); QVERIFY(artifact.isNull()); } void shouldCreateNullTaskFromProjectItem() { // GIVEN // A todo with the project flag KCalCore::Todo::Ptr todo(new KCalCore::Todo); todo->setSummary(QStringLiteral("foo")); todo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); // ... as payload of an item Akonadi::Item item; item.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); item.setPayload(todo); // WHEN Akonadi::Serializer serializer; Domain::Task::Ptr task = serializer.createTaskFromItem(item); auto artifact = serializer.createArtifactFromItem(item); // THEN QVERIFY(task.isNull()); QVERIFY(artifact.isNull()); } void shouldUpdateTaskFromItem_data() { QTest::addColumn("updatedSummary"); QTest::addColumn("updatedContent"); QTest::addColumn("updatedDone"); QTest::addColumn("updatedDoneDate"); QTest::addColumn("updatedStartDate"); QTest::addColumn("updatedDueDate"); QTest::addColumn("updatedRelated"); QTest::addColumn("updatedDelegateName"); QTest::addColumn("updatedDelegateEmail"); + QTest::addColumn("updatedRunning"); - QTest::newRow("no change") << "summary" << "content" << false << QDateTime() << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) << "my-uid" << "John Doe" << "j@d.com"; - QTest::newRow("changed") << "new summary" << "new content" << true << QDateTime(QDate(2013, 11, 28)) << QDateTime(QDate(2013, 11, 25)) << QDateTime(QDate(2014, 03, 02)) << "my-new-uid" << "John Smith" << "j@s.com"; + QTest::newRow("no change") << "summary" << "content" << false << QDateTime() << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) << "my-uid" << "John Doe" << "j@d.com" << false; + QTest::newRow("changed") << "new summary" << "new content" << true << QDateTime(QDate(2013, 11, 28)) << QDateTime(QDate(2013, 11, 25)) << QDateTime(QDate(2014, 03, 02)) << "my-new-uid" << "John Smith" << "j@s.com" << false; + QTest::newRow("set_to_running") << "summary" << "content" << false << QDateTime() << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) << "my-uid" << "John Doe" << "j@d.com" << true; } void shouldUpdateTaskFromItem() { // GIVEN // A todo... KCalCore::Todo::Ptr originalTodo(new KCalCore::Todo); originalTodo->setSummary(QStringLiteral("summary")); originalTodo->setDescription(QStringLiteral("content")); originalTodo->setCompleted(false); originalTodo->setDtStart(KDateTime(QDate(2013, 11, 24), KDateTime::UTC)); originalTodo->setDtDue(KDateTime(QDate(2014, 03, 01), KDateTime::UTC)); originalTodo->setRelatedTo(QStringLiteral("my-uid")); KCalCore::Attendee::Ptr originalAttendee(new KCalCore::Attendee(QStringLiteral("John Doe"), QStringLiteral("j@d.com"), true, KCalCore::Attendee::Accepted)); originalTodo->addAttendee(originalAttendee); // ... as payload of an item... Akonadi::Item originalItem; originalItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); originalItem.setPayload(originalTodo); // ... which has a parent collection... Akonadi::Collection originalCollection(43); originalItem.setParentCollection(originalCollection); // ... deserialized as a task Akonadi::Serializer serializer; auto task = serializer.createTaskFromItem(originalItem); auto artifact = serializer.createArtifactFromItem(originalItem); // WHEN // Data... QFETCH(QString, updatedSummary); QFETCH(QString, updatedContent); QFETCH(bool, updatedDone); QFETCH(QDateTime, updatedDoneDate); QFETCH(QDateTime, updatedStartDate); QFETCH(QDateTime, updatedDueDate); QFETCH(QString, updatedRelated); QFETCH(QString, updatedDelegateName); QFETCH(QString, updatedDelegateEmail); + QFETCH(bool, updatedRunning); // Switch to UTC updatedDoneDate.setTimeSpec(Qt::UTC); updatedStartDate.setTimeSpec(Qt::UTC); updatedDueDate.setTimeSpec(Qt::UTC); // ... in a new todo... KCalCore::Todo::Ptr updatedTodo(new KCalCore::Todo); updatedTodo->setSummary(updatedSummary); updatedTodo->setDescription(updatedContent); if (updatedDone) updatedTodo->setCompleted(KDateTime(updatedDoneDate)); else updatedTodo->setCompleted(updatedDone); updatedTodo->setDtStart(KDateTime(updatedStartDate, KDateTime::UTC)); updatedTodo->setDtDue(KDateTime(updatedDueDate, KDateTime::UTC)); updatedTodo->setRelatedTo(updatedRelated); if (!updatedDelegateName.isEmpty() || !updatedDelegateEmail.isEmpty()) { KCalCore::Attendee::Ptr updatedAttendee(new KCalCore::Attendee(updatedDelegateName, updatedDelegateEmail, true, KCalCore::Attendee::Accepted)); updatedTodo->addAttendee(updatedAttendee); } + if (updatedRunning) { + updatedTodo->setCustomProperty("Zanshin", "Running", "1"); + } else { + updatedTodo->removeCustomProperty("Zanshin", "Running"); + } // ... as payload of a new item Akonadi::Item updatedItem; updatedItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); updatedItem.setPayload(updatedTodo); // ... which has a new parent collection Akonadi::Collection updatedCollection(45); updatedItem.setParentCollection(updatedCollection); serializer.updateTaskFromItem(task, updatedItem); serializer.updateArtifactFromItem(artifact, updatedItem); // THEN QCOMPARE(task->title(), updatedSummary); QCOMPARE(task->text(), updatedContent); QCOMPARE(task->isDone(), updatedDone); QCOMPARE(task->doneDate(), updatedDoneDate.toUTC()); QCOMPARE(task->startDate(), updatedStartDate.toUTC()); QCOMPARE(task->dueDate(), updatedDueDate.toUTC()); QCOMPARE(task->property("todoUid").toString(), updatedTodo->uid()); QCOMPARE(task->property("relatedUid").toString(), updatedTodo->relatedTo()); QCOMPARE(task->property("itemId").toLongLong(), updatedItem.id()); QCOMPARE(task->property("parentCollectionId").toLongLong(), updatedCollection.id()); QCOMPARE(task->delegate().name(), updatedDelegateName); QCOMPARE(task->delegate().email(), updatedDelegateEmail); + QCOMPARE(task->isRunning(), updatedRunning); task = artifact.dynamicCast(); QCOMPARE(task->title(), updatedSummary); QCOMPARE(task->text(), updatedContent); QCOMPARE(task->isDone(), updatedDone); QCOMPARE(task->doneDate(), updatedDoneDate.toUTC()); QCOMPARE(task->startDate(), updatedStartDate.toUTC()); QCOMPARE(task->dueDate(), updatedDueDate.toUTC()); QCOMPARE(task->property("todoUid").toString(), updatedTodo->uid()); QCOMPARE(task->property("relatedUid").toString(), updatedTodo->relatedTo()); QCOMPARE(task->property("itemId").toLongLong(), updatedItem.id()); QCOMPARE(task->property("parentCollectionId").toLongLong(), updatedCollection.id()); QCOMPARE(task->delegate().name(), updatedDelegateName); QCOMPARE(task->delegate().email(), updatedDelegateEmail); + QCOMPARE(task->isRunning(), updatedRunning); } void shouldNotUpdateTaskFromInvalidItem() { // GIVEN // Data... const QString summary = QStringLiteral("summary"); const QString content = QStringLiteral("content"); const bool isDone = true; const QDateTime doneDate(QDate(2013, 11, 30), QTime(0, 0), Qt::UTC); const QDateTime startDate(QDate(2013, 11, 24), QTime(0, 0), Qt::UTC); const QDateTime dueDate(QDate(2014, 03, 01), QTime(0, 0), Qt::UTC); // ... stored in a todo... KCalCore::Todo::Ptr originalTodo(new KCalCore::Todo); originalTodo->setSummary(summary); originalTodo->setDescription(content); if (originalTodo) originalTodo->setCompleted(KDateTime(doneDate)); else originalTodo->setCompleted(isDone); originalTodo->setDtStart(KDateTime(startDate, KDateTime::UTC)); originalTodo->setDtDue(KDateTime(dueDate, KDateTime::UTC)); // ... as payload of an item... Akonadi::Item originalItem; originalItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); originalItem.setPayload(originalTodo); // ... deserialized as a task Akonadi::Serializer serializer; auto task = serializer.createTaskFromItem(originalItem); auto artifact = serializer.createArtifactFromItem(originalItem); // WHEN Akonadi::Item invalidItem; serializer.updateTaskFromItem(task, invalidItem); serializer.updateArtifactFromItem(artifact, invalidItem); // THEN QCOMPARE(task->title(), summary); QCOMPARE(task->text(), content); QCOMPARE(task->isDone(), isDone); QCOMPARE(task->doneDate(), doneDate); QCOMPARE(task->startDate(), startDate); QCOMPARE(task->dueDate(), dueDate); QCOMPARE(task->property("itemId").toLongLong(), originalItem.id()); task = artifact.dynamicCast(); QCOMPARE(task->title(), summary); QCOMPARE(task->text(), content); QCOMPARE(task->isDone(), isDone); QCOMPARE(task->doneDate(), doneDate); QCOMPARE(task->startDate(), startDate); QCOMPARE(task->dueDate(), dueDate); QCOMPARE(task->property("itemId").toLongLong(), originalItem.id()); } void shouldNotUpdateTaskFromProjectItem() { // GIVEN // Data... const QString summary = QStringLiteral("summary"); const QString content = QStringLiteral("content"); const bool isDone = true; const QDateTime doneDate(QDate(2013, 11, 30), QTime(0, 0), Qt::UTC); const QDateTime startDate(QDate(2013, 11, 24), QTime(0, 0), Qt::UTC); const QDateTime dueDate(QDate(2014, 03, 01), QTime(0, 0), Qt::UTC); // ... stored in a todo... KCalCore::Todo::Ptr originalTodo(new KCalCore::Todo); originalTodo->setSummary(summary); originalTodo->setDescription(content); if (originalTodo) originalTodo->setCompleted(KDateTime(doneDate)); else originalTodo->setCompleted(isDone); originalTodo->setDtStart(KDateTime(startDate, KDateTime::UTC)); originalTodo->setDtDue(KDateTime(dueDate, KDateTime::UTC)); // ... as payload of an item... Akonadi::Item originalItem; originalItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); originalItem.setPayload(originalTodo); // ... deserialized as a task Akonadi::Serializer serializer; auto task = serializer.createTaskFromItem(originalItem); auto artifact = serializer.createArtifactFromItem(originalItem); // WHEN // A todo with the project flag KCalCore::Todo::Ptr projectTodo(new KCalCore::Todo); projectTodo->setSummary(QStringLiteral("foo")); projectTodo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); // ... as payload of an item Akonadi::Item projectItem; projectItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); projectItem.setPayload(projectTodo); serializer.updateTaskFromItem(task, projectItem); serializer.updateArtifactFromItem(artifact, projectItem); // THEN QCOMPARE(task->title(), summary); QCOMPARE(task->text(), content); QCOMPARE(task->isDone(), isDone); QCOMPARE(task->doneDate(), doneDate); QCOMPARE(task->startDate(), startDate); QCOMPARE(task->dueDate(), dueDate); QCOMPARE(task->property("itemId").toLongLong(), originalItem.id()); task = artifact.dynamicCast(); QCOMPARE(task->title(), summary); QCOMPARE(task->text(), content); QCOMPARE(task->isDone(), isDone); QCOMPARE(task->doneDate(), doneDate); QCOMPARE(task->startDate(), startDate); QCOMPARE(task->dueDate(), dueDate); QCOMPARE(task->property("itemId").toLongLong(), originalItem.id()); } void shouldCreateItemFromTask_data() { QTest::addColumn("summary"); QTest::addColumn("content"); QTest::addColumn("isDone"); QTest::addColumn("doneDate"); QTest::addColumn("startDate"); QTest::addColumn("dueDate"); QTest::addColumn("itemId"); QTest::addColumn("parentCollectionId"); QTest::addColumn("todoUid"); QTest::addColumn("delegate"); + QTest::addColumn("running"); QTest::newRow("nominal case (no id)") << "summary" << "content" << false << QDateTime() << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) << qint64(-1) << qint64(-1) << QString() - << Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("j@d.com")); + << Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("j@d.com")) + << false; QTest::newRow("done case (no id)") << "summary" << "content" << true << QDateTime(QDate(2013, 11, 30)) << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) << qint64(-1) << qint64(-1) << QString() - << Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("j@d.com")); + << Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("j@d.com")) + << false; QTest::newRow("empty case (no id)") << QString() << QString() << false << QDateTime() << QDateTime() << QDateTime() << qint64(-1) << qint64(-1) << QString() - << Domain::Task::Delegate(); + << Domain::Task::Delegate() + << false; QTest::newRow("nominal_with_time_info_noid") << "summary" << "content" << true << QDateTime(QDate(2015, 3, 1), QTime(1, 2, 3), Qt::UTC) << QDateTime(QDate(2013, 11, 24), QTime(0, 1, 2), Qt::UTC) << QDateTime(QDate(2016, 3, 1), QTime(4, 5, 6), Qt::UTC) << qint64(-1) << qint64(-1) << QString() - << Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("j@d.com")); + << Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("j@d.com")) + << false; QTest::newRow("nominal case (with id)") << "summary" << "content" << false << QDateTime() << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) << qint64(42) << qint64(43) << "my-uid" - << Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("j@d.com")); + << Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("j@d.com")) + << false; QTest::newRow("done case (with id)") << "summary" << "content" << true << QDateTime(QDate(2013, 11, 30)) << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) << qint64(42) << qint64(43) << "my-uid" - << Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("j@d.com")); + << Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("j@d.com")) + << false; QTest::newRow("empty case (with id)") << QString() << QString() << false << QDateTime() << QDateTime() << QDateTime() << qint64(42) << qint64(43) << "my-uid" - << Domain::Task::Delegate(); + << Domain::Task::Delegate() + << false; + QTest::newRow("nominal case (running)") << "running" << QString() << false << QDateTime() + << QDateTime(QDate(2013, 11, 24)) << QDateTime(QDate(2014, 03, 01)) + << qint64(-1) << qint64(-1) << QString() + << Domain::Task::Delegate() + << true; } void shouldCreateItemFromTask() { // GIVEN // Data... QFETCH(QString, summary); QFETCH(QString, content); QFETCH(bool, isDone); QFETCH(QDateTime, doneDate); QFETCH(QDateTime, startDate); QFETCH(QDateTime, dueDate); QFETCH(qint64, itemId); QFETCH(qint64, parentCollectionId); QFETCH(QString, todoUid); QFETCH(Domain::Task::Delegate, delegate); + QFETCH(bool, running); // Switch to UTC doneDate.setTimeSpec(Qt::UTC); startDate.setTimeSpec(Qt::UTC); dueDate.setTimeSpec(Qt::UTC); // ... stored in a task auto task = Domain::Task::Ptr::create(); task->setTitle(summary); task->setText(content); task->setDone(isDone); task->setDoneDate(doneDate); task->setStartDate(startDate); task->setDueDate(dueDate); task->setDelegate(delegate); + task->setRunning(running); if (itemId > 0) task->setProperty("itemId", itemId); if (parentCollectionId > 0) task->setProperty("parentCollectionId", parentCollectionId); if (!todoUid.isEmpty()) task->setProperty("todoUid", todoUid); task->setProperty("relatedUid", "parent-uid"); // WHEN Akonadi::Serializer serializer; auto item = serializer.createItemFromTask(task); // THEN QCOMPARE(item.mimeType(), KCalCore::Todo::todoMimeType()); QCOMPARE(item.isValid(), itemId > 0); if (itemId > 0) { QCOMPARE(item.id(), itemId); } QCOMPARE(item.parentCollection().isValid(), parentCollectionId > 0); if (parentCollectionId > 0) { QCOMPARE(item.parentCollection().id(), parentCollectionId); } auto todo = item.payload(); QCOMPARE(todo->summary(), summary); QCOMPARE(todo->description(), content); QCOMPARE(todo->isCompleted(), isDone); QCOMPARE(todo->completed().dateTime().toUTC(), doneDate); QCOMPARE(todo->dtStart().dateTime().toUTC(), startDate); QCOMPARE(todo->dtDue().dateTime().toUTC(), dueDate); if (todo->dtStart().isValid()) { QCOMPARE(int(todo->dtStart().timeType()), int(KDateTime::UTC)); } QCOMPARE(todo->dtStart().isDateOnly(), todo->allDay()); if (delegate.isValid()) { auto attendee = todo->attendeeByMail(delegate.email()); QVERIFY(attendee); QCOMPARE(attendee->name(), delegate.name()); QCOMPARE(attendee->email(), delegate.email()); } if (!todoUid.isEmpty()) { QCOMPARE(todo->uid(), todoUid); } QCOMPARE(todo->relatedTo(), QStringLiteral("parent-uid")); + QCOMPARE(todo->customProperty("Zanshin", "Running"), running ? QStringLiteral("1") : QString()); } void shouldVerifyIfAnItemIsATaskChild_data() { QTest::addColumn("task"); QTest::addColumn("item"); QTest::addColumn("isParent"); // Create task const QString summary = QStringLiteral("summary"); const QString content = QStringLiteral("content"); const bool isDone = true; const QDateTime doneDate(QDate(2013, 11, 30), QTime(0, 0), Qt::UTC); const QDateTime startDate(QDate(2013, 11, 24), QTime(0, 0), Qt::UTC); const QDateTime dueDate(QDate(2014, 03, 01), QTime(0, 0), Qt::UTC); // ... create a task Domain::Task::Ptr task(new Domain::Task); task->setTitle(summary); task->setText(content); task->setDone(isDone); task->setDoneDate(doneDate); task->setStartDate(startDate); task->setDueDate(dueDate); task->setProperty("todoUid", "1"); // Create Child item KCalCore::Todo::Ptr childTodo(new KCalCore::Todo); childTodo->setSummary(summary); childTodo->setDescription(content); if (isDone) childTodo->setCompleted(KDateTime(doneDate)); else childTodo->setCompleted(isDone); childTodo->setDtStart(KDateTime(startDate, KDateTime::UTC)); childTodo->setDtDue(KDateTime(dueDate, KDateTime::UTC)); Akonadi::Item childItem; childItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); childItem.setPayload(childTodo); QTest::newRow("without parent") << task << childItem << false; // Create Child Item with parent KCalCore::Todo::Ptr childTodo2(new KCalCore::Todo); childTodo2->setSummary(summary); childTodo2->setDescription(content); if (isDone) childTodo2->setCompleted(KDateTime(doneDate)); else childTodo2->setCompleted(isDone); childTodo2->setDtStart(KDateTime(startDate, KDateTime::UTC)); childTodo2->setDtDue(KDateTime(dueDate, KDateTime::UTC)); childTodo2->setRelatedTo(QStringLiteral("1")); Akonadi::Item childItem2; childItem2.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); childItem2.setPayload(childTodo2); QTest::newRow("with parent") << task << childItem2 << true; Domain::Task::Ptr invalidTask(new Domain::Task); QTest::newRow("with invalid task") << invalidTask << childItem << false; Akonadi::Item invalidItem; QTest::newRow("with invalid item") << task << invalidItem << false; } void shouldVerifyIfAnItemIsATaskChild() { // GIVEN QFETCH(Domain::Task::Ptr, task); QFETCH(Akonadi::Item, item); QFETCH(bool, isParent); // WHEN Akonadi::Serializer serializer; bool value = serializer.isTaskChild(task, item); // THEN QCOMPARE(value, isParent); } void shouldRetrieveRelatedUidFromItem_data() { QTest::addColumn("item"); QTest::addColumn("expectedUid"); Akonadi::Item item1; KCalCore::Todo::Ptr todo1(new KCalCore::Todo); item1.setPayload(todo1); Akonadi::Item item2; KCalCore::Todo::Ptr todo2(new KCalCore::Todo); todo2->setRelatedTo(QStringLiteral("1")); item2.setPayload(todo2); Akonadi::Item item3; KMime::Message::Ptr message1(new KMime::Message); message1->subject(true)->fromUnicodeString(QStringLiteral("foo"), "utf-8"); message1->mainBodyPart()->fromUnicodeString(QStringLiteral("bar")); item3.setMimeType(Akonadi::NoteUtils::noteMimeType()); item3.setPayload(message1); Akonadi::Item item4; KMime::Message::Ptr message2(new KMime::Message); message2->subject(true)->fromUnicodeString(QStringLiteral("foo"), "utf-8"); message2->mainBodyPart()->fromUnicodeString(QStringLiteral("bar")); auto relatedHeader1 = new KMime::Headers::Generic("X-Zanshin-RelatedProjectUid"); relatedHeader1->from7BitString("1"); message2->appendHeader(relatedHeader1); item4.setMimeType(Akonadi::NoteUtils::noteMimeType()); item4.setPayload(message2); Akonadi::Item item5; KMime::Message::Ptr message3(new KMime::Message); message3->subject(true)->fromUnicodeString(QStringLiteral("foo"), "utf-8"); message3->mainBodyPart()->fromUnicodeString(QStringLiteral("bar")); auto relatedHeader2 = new KMime::Headers::Generic("X-Zanshin-RelatedProjectUid"); message3->appendHeader(relatedHeader2); item5.setMimeType(Akonadi::NoteUtils::noteMimeType()); item5.setPayload(message3); QTest::newRow("task without related") << item1 << QString(); QTest::newRow("task with related") << item2 << "1"; QTest::newRow("note without related") << item3 << QString(); QTest::newRow("note with related") << item4 << "1"; QTest::newRow("note with empty related") << item5 << QString(); } void shouldRetrieveRelatedUidFromItem() { // GIVEN QFETCH(Akonadi::Item, item); QFETCH(QString, expectedUid); // WHEN Akonadi::Serializer serializer; QString uid = serializer.relatedUidFromItem(item); // THEN QCOMPARE(uid, expectedUid); } void shouldCreateNoteFromItem_data() { QTest::addColumn("title"); QTest::addColumn("text"); QTest::addColumn("relatedUid"); QTest::newRow("nominal case (no related)") << "A note title" << "A note content.\nWith two lines." << QString(); QTest::newRow("nominal case (with related)") << "A note title" << "A note content.\nWith two lines." << "parent-uid"; QTest::newRow("trailing new lines") << "A note title" << "Empty lines at the end.\n\n\n" << QString(); QTest::newRow("empty case") << QString() << QString() << QString(); } void shouldCreateNoteFromItem() { // GIVEN // Data... QFETCH(QString, title); QFETCH(QString, text); QFETCH(QString, relatedUid); // ... stored in a message... KMime::Message::Ptr message(new KMime::Message); message->subject(true)->fromUnicodeString(title, "utf-8"); message->mainBodyPart()->fromUnicodeString(text); if (!relatedUid.isEmpty()) { auto relatedHeader = new KMime::Headers::Generic("X-Zanshin-RelatedProjectUid"); relatedHeader->from7BitString(relatedUid.toUtf8()); message->appendHeader(relatedHeader); } // ... as payload of an item. Akonadi::Item item; item.setMimeType(Akonadi::NoteUtils::noteMimeType()); item.setPayload(message); // WHEN Akonadi::Serializer serializer; Domain::Note::Ptr note = serializer.createNoteFromItem(item); auto artifact = serializer.createArtifactFromItem(item).dynamicCast(); // THEN const auto expectedText = text.endsWith('\n') ? (text.chop(1), text) : text; QCOMPARE(note->title(), title); QCOMPARE(note->text(), expectedText); QCOMPARE(note->property("itemId").toLongLong(), item.id()); QCOMPARE(note->property("relatedUid").toString(), relatedUid); QVERIFY(!artifact.isNull()); QCOMPARE(artifact->title(), title); QCOMPARE(artifact->text(), expectedText); QCOMPARE(artifact->property("itemId").toLongLong(), item.id()); QCOMPARE(artifact->property("relatedUid").toString(), relatedUid); } void shouldCreateNullNoteFromInvalidItem() { // GIVEN Akonadi::Item item; // WHEN Akonadi::Serializer serializer; Domain::Note::Ptr note = serializer.createNoteFromItem(item); auto artifact = serializer.createArtifactFromItem(item); // THEN QVERIFY(note.isNull()); QVERIFY(artifact.isNull()); } void shouldUpdateNoteFromItem_data() { QTest::addColumn("updatedTitle"); QTest::addColumn("updatedText"); QTest::addColumn("updatedRelatedUid"); QTest::newRow("no change") << "title" << "content" << "parent-uid"; QTest::newRow("data changed (with related)") << "A new title" << "A new content" << "new-parent-uid"; QTest::newRow("data changed (with no related)") << "A new title" << "A new content" << QString(); } void shouldUpdateNoteFromItem() { // GIVEN // A message... KMime::Message::Ptr message(new KMime::Message); message->subject(true)->fromUnicodeString(QStringLiteral("title"), "utf-8"); message->mainBodyPart()->fromUnicodeString(QStringLiteral("text")); auto relatedHeader = new KMime::Headers::Generic("X-Zanshin-RelatedProjectUid"); relatedHeader->from7BitString("parent-uid"); message->appendHeader(relatedHeader); //... as the payload of an item... Akonadi::Item item; item.setMimeType(Akonadi::NoteUtils::noteMimeType()); item.setPayload(message); //... deserialized as a note Akonadi::Serializer serializer; auto note = serializer.createNoteFromItem(item); auto artifact = serializer.createNoteFromItem(item); // WHEN // Data... QFETCH(QString, updatedTitle); QFETCH(QString, updatedText); QFETCH(QString, updatedRelatedUid); //... stored in a new message... KMime::Message::Ptr updatedMessage(new KMime::Message); updatedMessage->subject(true)->fromUnicodeString(updatedTitle, "utf-8"); updatedMessage->mainBodyPart()->fromUnicodeString(updatedText); if (!updatedRelatedUid.isEmpty()) { relatedHeader = new KMime::Headers::Generic("X-Zanshin-RelatedProjectUid"); relatedHeader->from7BitString(updatedRelatedUid.toUtf8()); updatedMessage->appendHeader(relatedHeader); } //... as the payload of a new item... Akonadi::Item updatedItem; updatedItem.setMimeType(Akonadi::NoteUtils::noteMimeType()); updatedItem.setPayload(updatedMessage); serializer.updateNoteFromItem(note, updatedItem); serializer.updateArtifactFromItem(artifact, updatedItem); // THEN QCOMPARE(note->title(), updatedTitle); QCOMPARE(note->text(), updatedText); QCOMPARE(note->property("itemId").toLongLong(), updatedItem.id()); QCOMPARE(note->property("relatedUid").toString(), updatedRelatedUid); note = artifact.dynamicCast(); QCOMPARE(note->title(), updatedTitle); QCOMPARE(note->text(), updatedText); QCOMPARE(note->property("itemId").toLongLong(), updatedItem.id()); QCOMPARE(note->property("relatedUid").toString(), updatedRelatedUid); } void shouldNotUpdateNoteFromInvalidItem() { // GIVEN // Data... QString title = QStringLiteral("A title"); QString text = QStringLiteral("A note content"); // ... stored in a message... KMime::Message::Ptr message(new KMime::Message); message->subject(true)->fromUnicodeString(title, "utf-8"); message->mainBodyPart()->fromUnicodeString(text); //... as the payload of an item... Akonadi::Item item; item.setMimeType(Akonadi::NoteUtils::noteMimeType()); item.setPayload(message); //... deserialized as a note Akonadi::Serializer serializer; auto note = serializer.createNoteFromItem(item); auto artifact = serializer.createArtifactFromItem(item); // WHEN Akonadi::Item invalidItem; serializer.updateNoteFromItem(note, invalidItem); serializer.updateArtifactFromItem(artifact, invalidItem); //THEN QCOMPARE(note->title(), title); QCOMPARE(note->text(), text); QCOMPARE(note->property("itemId").toLongLong(), item.id()); note = artifact.dynamicCast(); QCOMPARE(note->title(), title); QCOMPARE(note->text(), text); QCOMPARE(note->property("itemId").toLongLong(), item.id()); } void shouldCreateItemFromNote_data() { QTest::addColumn("title"); QTest::addColumn("content"); QTest::addColumn("expectedTitle"); QTest::addColumn("expectedContent"); QTest::addColumn("itemId"); QTest::addColumn("relatedUid"); QTest::newRow("nominal case (no id)") << "title" << "content" << "title" << "content" << qint64(-1) << QString(); QTest::newRow("empty case (no id)") << QString() << QString() << "New Note" << QString() << qint64(-1) << QString(); QTest::newRow("nominal case (with id)") << "title" << "content" << "title" << "content" << qint64(42) << "parent-uid"; QTest::newRow("empty case (with id)") << QString() << QString() << "New Note" << QString() << qint64(42) << "parent-uid"; QTest::newRow("empty line at the end") << "title" << "content\n\n\n" << "title" << "content\n\n\n" << qint64(-1) << QString(); } void shouldCreateItemFromNote() { // GIVEN // Data... QFETCH(QString, title); QFETCH(QString, content); QFETCH(qint64, itemId); QFETCH(QString, relatedUid); // ... stored in a note auto note = Domain::Note::Ptr::create(); note->setTitle(title); note->setText(content); if (itemId > 0) note->setProperty("itemId", itemId); if (!relatedUid.isEmpty()) note->setProperty("relatedUid", relatedUid); // WHEN Akonadi::Serializer serializer; auto item = serializer.createItemFromNote(note); // THEN QCOMPARE(item.mimeType(), Akonadi::NoteUtils::noteMimeType()); QCOMPARE(item.isValid(), itemId > 0); if (itemId > 0) { QCOMPARE(item.id(), itemId); } QFETCH(QString, expectedTitle); QFETCH(QString, expectedContent); auto message = item.payload(); QCOMPARE(message->subject(false)->asUnicodeString(), expectedTitle); QCOMPARE(message->mainBodyPart()->decodedText(), expectedContent); if (relatedUid.isEmpty()) { QVERIFY(!message->headerByType("X-Zanshin-RelatedProjectUid")); } else { QVERIFY(message->headerByType("X-Zanshin-RelatedProjectUid")); QCOMPARE(message->headerByType("X-Zanshin-RelatedProjectUid")->asUnicodeString(), relatedUid); } } void shouldCreateProjectFromItem_data() { QTest::addColumn("summary"); QTest::newRow("nominal case") << "summary"; QTest::newRow("empty case") << QString(); } void shouldCreateProjectFromItem() { // GIVEN // Data... QFETCH(QString, summary); // ... stored in a todo... KCalCore::Todo::Ptr todo(new KCalCore::Todo); todo->setSummary(summary); todo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); QVERIFY(!todo->uid().isEmpty()); // ... as payload of an item Akonadi::Item item(42); item.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); item.setPayload(todo); // which has a prent collection Akonadi::Collection collection(43); item.setParentCollection(collection); // WHEN Akonadi::Serializer serializer; Domain::Project::Ptr project = serializer.createProjectFromItem(item); // THEN QCOMPARE(project->name(), summary); QCOMPARE(project->property("itemId").toLongLong(), item.id()); QCOMPARE(project->property("parentCollectionId").toLongLong(), collection.id()); QCOMPARE(project->property("todoUid").toString(), todo->uid()); } void shouldCreateNullProjectFromInvalidItem() { // GIVEN Akonadi::Item item; // WHEN Akonadi::Serializer serializer; Domain::Project::Ptr project = serializer.createProjectFromItem(item); // THEN QVERIFY(project.isNull()); } void shouldCreateNullProjectFromTaskItem() { // GIVEN // A todo without the project flag KCalCore::Todo::Ptr todo(new KCalCore::Todo); todo->setSummary(QStringLiteral("foo")); // ... as payload of an item Akonadi::Item item; item.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); item.setPayload(todo); // WHEN Akonadi::Serializer serializer; Domain::Project::Ptr project = serializer.createProjectFromItem(item); // THEN QVERIFY(project.isNull()); } void shouldUpdateProjectFromItem_data() { QTest::addColumn("updatedSummary"); QTest::newRow("no change") << "summary"; QTest::newRow("changed") << "new summary"; } void shouldUpdateProjectFromItem() { // GIVEN // A todo... KCalCore::Todo::Ptr originalTodo(new KCalCore::Todo); originalTodo->setSummary(QStringLiteral("summary")); originalTodo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); // ... as payload of an item... Akonadi::Item originalItem(42); originalItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); originalItem.setPayload(originalTodo); // ... which has a parent collection... Akonadi::Collection originalCollection(43); originalItem.setParentCollection(originalCollection); // ... deserialized as a project Akonadi::Serializer serializer; auto project = serializer.createProjectFromItem(originalItem); // WHEN // Data... QFETCH(QString, updatedSummary); // ... in a new todo... KCalCore::Todo::Ptr updatedTodo(new KCalCore::Todo); updatedTodo->setSummary(updatedSummary); updatedTodo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); QVERIFY(!updatedTodo->uid().isEmpty()); // ... as payload of a new item Akonadi::Item updatedItem(44); updatedItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); updatedItem.setPayload(updatedTodo); // ... which has a new parent collection Akonadi::Collection updatedCollection(45); updatedItem.setParentCollection(updatedCollection); serializer.updateProjectFromItem(project, updatedItem); // THEN QCOMPARE(project->name(), updatedSummary); QCOMPARE(project->property("itemId").toLongLong(), updatedItem.id()); QCOMPARE(project->property("parentCollectionId").toLongLong(), updatedCollection.id()); QCOMPARE(project->property("todoUid").toString(), updatedTodo->uid()); } void shouldNotUpdateProjectFromInvalidItem() { // GIVEN // Data... const QString summary = QStringLiteral("summary"); // ... stored in a todo... KCalCore::Todo::Ptr originalTodo(new KCalCore::Todo); originalTodo->setSummary(summary); originalTodo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); // ... as payload of an item... Akonadi::Item originalItem; originalItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); originalItem.setPayload(originalTodo); // ... deserialized as a project Akonadi::Serializer serializer; auto project = serializer.createProjectFromItem(originalItem); // WHEN Akonadi::Item invalidItem; serializer.updateProjectFromItem(project, invalidItem); // THEN QCOMPARE(project->name(), summary); } void shouldNotUpdateProjectFromTaskItem() { // GIVEN // Data... const QString summary = QStringLiteral("summary"); // ... stored in a todo... KCalCore::Todo::Ptr originalTodo(new KCalCore::Todo); originalTodo->setSummary(summary); originalTodo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); // ... as payload of an item... Akonadi::Item originalItem; originalItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); originalItem.setPayload(originalTodo); // ... deserialized as a project Akonadi::Serializer serializer; auto project = serializer.createProjectFromItem(originalItem); // WHEN // A todo without the project flag KCalCore::Todo::Ptr projectTodo(new KCalCore::Todo); projectTodo->setSummary(QStringLiteral("foo")); // ... as payload of an item Akonadi::Item projectItem; projectItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); projectItem.setPayload(projectTodo); serializer.updateProjectFromItem(project, projectItem); // THEN QCOMPARE(project->name(), summary); } void shouldCreateItemFromProject_data() { QTest::addColumn("summary"); QTest::addColumn("itemId"); QTest::addColumn("parentCollectionId"); QTest::newRow("nominal case (no id)") << "summary" << qint64(-1) << qint64(-1); QTest::newRow("empty case (no id)") << QString() << qint64(-1) << qint64(-1); QTest::newRow("nominal case (with id)") << "summary" << qint64(42) << qint64(43); QTest::newRow("empty case (with id)") << QString() << qint64(42) << qint64(43); } void shouldCreateItemFromProject() { // GIVEN // Data... QFETCH(QString, summary); QFETCH(qint64, itemId); QFETCH(qint64, parentCollectionId); const QString todoUid = QStringLiteral("test-uid"); // ... stored in a project auto project = Domain::Project::Ptr::create(); project->setName(summary); project->setProperty("todoUid", todoUid); if (itemId > 0) project->setProperty("itemId", itemId); if (parentCollectionId > 0) project->setProperty("parentCollectionId", parentCollectionId); // WHEN Akonadi::Serializer serializer; auto item = serializer.createItemFromProject(project); // THEN QCOMPARE(item.mimeType(), KCalCore::Todo::todoMimeType()); QCOMPARE(item.isValid(), itemId > 0); if (itemId > 0) { QCOMPARE(item.id(), itemId); } QCOMPARE(item.parentCollection().isValid(), parentCollectionId > 0); if (parentCollectionId > 0) { QCOMPARE(item.parentCollection().id(), parentCollectionId); } auto todo = item.payload(); QCOMPARE(todo->summary(), summary); QCOMPARE(todo->uid(), todoUid); QVERIFY(!todo->customProperty("Zanshin", "Project").isEmpty()); } void shouldVerifyIfAnItemIsAProjectChild_data() { QTest::addColumn("project"); QTest::addColumn("item"); QTest::addColumn("isParent"); // Create project auto project = Domain::Project::Ptr::create(); project->setName(QStringLiteral("project")); project->setProperty("todoUid", "1"); // Create unrelated todo auto unrelatedTodo = KCalCore::Todo::Ptr::create(); unrelatedTodo->setSummary(QStringLiteral("summary")); Akonadi::Item unrelatedTodoItem; unrelatedTodoItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); unrelatedTodoItem.setPayload(unrelatedTodo); QTest::newRow("unrelated todo") << project << unrelatedTodoItem << false; // Create child todo auto childTodo = KCalCore::Todo::Ptr::create(); childTodo->setSummary(QStringLiteral("summary")); childTodo->setRelatedTo(QStringLiteral("1")); Akonadi::Item childTodoItem; childTodoItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.todo")); childTodoItem.setPayload(childTodo); QTest::newRow("child todo") << project << childTodoItem << true; // Create unrelated note KMime::Message::Ptr unrelatedNote(new KMime::Message); unrelatedNote->subject(true)->fromUnicodeString(QStringLiteral("subject"), "utf-8"); Akonadi::Item unrelatedNoteItem; unrelatedNoteItem.setMimeType(Akonadi::NoteUtils::noteMimeType()); unrelatedNoteItem.setPayload(unrelatedNote); QTest::newRow("unrelated note") << project << unrelatedNoteItem << false; // Create child note KMime::Message::Ptr childNote(new KMime::Message); childNote->subject(true)->fromUnicodeString(QStringLiteral("subject"), "utf-8"); auto relatedHeader = new KMime::Headers::Generic("X-Zanshin-RelatedProjectUid"); relatedHeader->from7BitString("1"); childNote->appendHeader(relatedHeader); Akonadi::Item childNoteItem; childNoteItem.setMimeType(Akonadi::NoteUtils::noteMimeType()); childNoteItem.setPayload(childNote); QTest::newRow("child todo") << project << childNoteItem << true; auto invalidProject = Domain::Project::Ptr::create(); QTest::newRow("invalid project") << invalidProject << unrelatedNoteItem << false; Akonadi::Item invalidItem; QTest::newRow("invalid item") << project << invalidItem << false; } void shouldVerifyIfAnItemIsAProjectChild() { // GIVEN QFETCH(Domain::Project::Ptr, project); QFETCH(Akonadi::Item, item); QFETCH(bool, isParent); // WHEN Akonadi::Serializer serializer; bool value = serializer.isProjectChild(project, item); // THEN QCOMPARE(value, isParent); } void shouldUpdateItemParent_data() { QTest::addColumn("item"); QTest::addColumn("parent"); QTest::addColumn("expectedRelatedToUid"); Akonadi::Item item1; KCalCore::Todo::Ptr todo1(new KCalCore::Todo); item1.setPayload(todo1); Domain::Task::Ptr parent(new Domain::Task); parent->setProperty("todoUid", "1"); QTest::newRow("nominal case") << item1 << parent << "1"; Akonadi::Item item2; QTest::newRow("update item without payload") << item2 << parent << QString(); Domain::Task::Ptr parent2(new Domain::Task); QTest::newRow("update item with a empty parent uid") << item1 << parent2 << QString(); } void shouldUpdateItemParent() { // GIVEN QFETCH(Akonadi::Item, item); QFETCH(Domain::Task::Ptr, parent); QFETCH(QString, expectedRelatedToUid); // WHEN Akonadi::Serializer serializer; serializer.updateItemParent(item, parent); // THEN if (item.hasPayload()) { auto todo = item.payload(); QString relatedUid = todo->relatedTo(); QCOMPARE(relatedUid, expectedRelatedToUid); } } void shouldUpdateItemProject_data() { QTest::addColumn("item"); QTest::addColumn("parent"); QTest::addColumn("expectedRelatedToUid"); Akonadi::Item todoItem; KCalCore::Todo::Ptr todo(new KCalCore::Todo); todoItem.setPayload(todo); auto parent = Domain::Project::Ptr::create(); parent->setProperty("todoUid", "1"); QTest::newRow("nominal todo case") << todoItem << parent << "1"; auto invalidParent = Domain::Project::Ptr::create(); QTest::newRow("update todo item with a empty parent uid") << todoItem << invalidParent << QString(); Akonadi::Item noteItem; KMime::Message::Ptr note(new KMime::Message); noteItem.setPayload(note); QTest::newRow("nominal note case") << noteItem << parent << "1"; QTest::newRow("update note item with a empty parent uid") << noteItem << invalidParent << QString(); Akonadi::Item invalidItem; QTest::newRow("update item without payload") << invalidItem << parent << QString(); } void shouldUpdateItemProject() { // GIVEN QFETCH(Akonadi::Item, item); QFETCH(Domain::Project::Ptr, parent); QFETCH(QString, expectedRelatedToUid); // WHEN Akonadi::Serializer serializer; serializer.updateItemProject(item, parent); // THEN if (item.hasPayload()) { auto todo = item.payload(); const QString relatedUid = todo->relatedTo(); QCOMPARE(relatedUid, expectedRelatedToUid); } else if (item.hasPayload()) { auto note = item.payload(); const auto relatedHeader = note->headerByType("X-Zanshin-RelatedProjectUid"); const QString relatedUid = relatedHeader ? relatedHeader->asUnicodeString() : QString(); QCOMPARE(relatedUid, expectedRelatedToUid); if (!expectedRelatedToUid.isEmpty()) QVERIFY(note->encodedContent().contains(QStringLiteral("X-Zanshin-RelatedProjectUid: %1").arg(expectedRelatedToUid).toUtf8())); else QVERIFY(!note->encodedContent().contains("X-Zanshin-RelatedProjectUid:")); } } void shouldFilterChildrenItem_data() { QTest::addColumn("item"); QTest::addColumn("items"); QTest::addColumn("size"); Akonadi::Item item(12); KCalCore::Todo::Ptr todo(new KCalCore::Todo); todo->setUid(QStringLiteral("1")); item.setPayload(todo); Akonadi::Item::List items; QTest::newRow("empty list") << item << items << 0; Akonadi::Item item2(13); KCalCore::Todo::Ptr todo2(new KCalCore::Todo); item2.setPayload(todo2); Akonadi::Item::List items2; items2 << item2; QTest::newRow("list without child") << item << items2 << 0; Akonadi::Item item3(14); KCalCore::Todo::Ptr todo3(new KCalCore::Todo); todo3->setUid(QStringLiteral("3")); todo3->setRelatedTo(QStringLiteral("1")); item3.setPayload(todo3); Akonadi::Item::List items3; items3 << item2 << item3; QTest::newRow("list with child") << item << items3 << 1; Akonadi::Item item4(15); KCalCore::Todo::Ptr todo4(new KCalCore::Todo); todo4->setRelatedTo(QStringLiteral("3")); item4.setPayload(todo4); Akonadi::Item::List items4; items4 << item2 << item3 << item4; QTest::newRow("list with child with a child") << item << items4 << 2; Akonadi::Item::List items5; items5 << item << item2 << item3 << item4; QTest::newRow("list with filter in list") << item << items5 << 2; } void shouldFilterChildrenItem() { // GIVEN QFETCH(Akonadi::Item, item); QFETCH(Akonadi::Item::List, items); QFETCH(int, size); // WHEN Akonadi::Serializer serializer; Akonadi::Item::List list = serializer.filterDescendantItems(items, item); // THEN QCOMPARE(list.size(), size); } void shouldRemoveItemParent_data() { QTest::addColumn("item"); Akonadi::Item item(15); KCalCore::Todo::Ptr todo(new KCalCore::Todo); todo->setRelatedTo(QStringLiteral("3")); item.setPayload(todo); QTest::newRow("nominal case") << item; Akonadi::Item item2(16); QTest::newRow("parent invalid") << item2; } void shouldRemoveItemParent() { // GIVEN QFETCH(Akonadi::Item, item); // WHEN Akonadi::Serializer serializer; serializer.removeItemParent(item); // THEN if (item.hasPayload()) QCOMPARE(item.payload()->relatedTo(), QString()); } void shouldPromoteItemToProject_data() { QTest::addColumn("item"); auto item = Akonadi::Item(15); auto todo = KCalCore::Todo::Ptr::create(); todo->setRelatedTo(QStringLiteral("3")); item.setPayload(todo); QTest::newRow("nominal case") << item; QTest::newRow("invalid item") << Akonadi::Item(16); } void shouldPromoteItemToProject() { // GIVEN QFETCH(Akonadi::Item, item); // WHEN Akonadi::Serializer serializer; serializer.promoteItemToProject(item); // THEN if (item.hasPayload()) { auto todo = item.payload(); QCOMPARE(todo->relatedTo(), QString()); QVERIFY(!todo->customProperty("Zanshin", "Project").isEmpty()); } } void shouldClearItem_data() { QTest::addColumn("item"); Akonadi::Item *itemWithContent = new Akonadi::Item(15); KCalCore::Todo::Ptr todo(new KCalCore::Todo); // context Akonadi::Tag context(QStringLiteral("42")); context.setType( Akonadi::SerializerInterface::contextTagType() ); // tag Akonadi::Tag tag(QStringLiteral("43")); tag.setType( Akonadi::Tag::PLAIN ); Akonadi::Tag::List tagsList = Akonadi::Tag::List() << tag << context; itemWithContent->setTags(tagsList); itemWithContent->setPayload(todo); QTest::newRow("nominal case") << itemWithContent; Akonadi::Item *item2 = new Akonadi::Item(16); QTest::newRow("parent invalid") << item2; } void shouldClearItem() { // GIVEN QFETCH(Akonadi::Item*, item); // WHEN Akonadi::Serializer serializer; serializer.clearItem(item); // THEN QCOMPARE(item->tags().size(), 0); delete item; } void shouldCreateContextFromTag_data() { QTest::addColumn("type"); QTest::addColumn("name"); QTest::addColumn("tagId"); const QByteArray rightTagType = Akonadi::Serializer::contextTagType() ; QTest::newRow("nominal case") << rightTagType << "Context42" << Akonadi::Tag::Id(42); QTest::newRow("empty name case") << rightTagType << "" << Akonadi::Tag::Id(43); } void shouldCreateContextFromTag() { // GIVEN // Data... QFETCH(QByteArray, type); QFETCH(QString, name); QFETCH(Akonadi::Tag::Id, tagId); // ... stored as an Akonadi Tag Akonadi::Tag tag(name); tag.setType(type); tag.setId(tagId); // WHEN Akonadi::Serializer serializer; Domain::Context::Ptr context = serializer.createContextFromTag(tag); // THEN QCOMPARE(context->name(), tag.name()); QCOMPARE(context->property("tagId").toLongLong(), tag.id()); } void shouldNotCreateContextFromWrongTagType() { // GIVEN // Data stored as an Akonadi Tag Akonadi::Tag tag(QStringLiteral("context42")); tag.setType(QByteArray("wrongTagType")); // WHEN Akonadi::Serializer serializer; Domain::Context::Ptr context = serializer.createContextFromTag(tag); // THEN QVERIFY(!context); } void shouldUpdateContextFromTag_data() { shouldCreateContextFromTag_data(); } void shouldUpdateContextFromTag() { // GIVEN // Data... QFETCH(QByteArray, type); QFETCH(QString, name); QFETCH(Akonadi::Tag::Id, tagId); // ... stored as an Akonadi Tag Akonadi::Tag tag(name); tag.setType(type); tag.setId(tagId); // WHEN Akonadi::Serializer serializer; Domain::Context::Ptr context(new Domain::Context); serializer.updateContextFromTag(context, tag); // THEN QCOMPARE(context->name(), tag.name()); QCOMPARE(context->property("tagId").toLongLong(), tag.id()); } void shouldNotUpdateContextFromWrongTagType() { // GIVEN Akonadi::Tag originalTag(QStringLiteral("Context42")); originalTag.setType(Akonadi::Serializer::contextTagType()); originalTag.setId(42); Akonadi::Serializer serializer; Domain::Context::Ptr context = serializer.createContextFromTag(originalTag); // WHEN Akonadi::Tag wrongTag(QStringLiteral("WrongContext42")); wrongTag.setType(QByteArray("wrongTypeTag")); serializer.updateContextFromTag(context, wrongTag); // THEN QCOMPARE(context->name(), originalTag.name()); QCOMPARE(context->property("tagId").toLongLong(), originalTag.id()); } void shouldVerifyIfAnItemIsAContextChild_data() { QTest::addColumn("context"); QTest::addColumn("item"); QTest::addColumn("isChild"); // Create a context auto context = Domain::Context::Ptr::create(); context->setProperty("tagId", qint64(43)); Akonadi::Tag tag(Akonadi::Tag::Id(43)); Akonadi::Item unrelatedItem; QTest::newRow("Unrelated item") << context << unrelatedItem << false; Akonadi::Item relatedItem; relatedItem.setTag(tag); QTest::newRow("Related item") << context << relatedItem << true; auto invalidContext = Domain::Context::Ptr::create(); QTest::newRow("Invalid context") << invalidContext << relatedItem << false; Akonadi::Item invalidItem; QTest::newRow("Invalid Item") << context << invalidItem << false; } void shouldVerifyIfAnItemIsAContextChild() { // GIVEN QFETCH(Domain::Context::Ptr, context); QFETCH(Akonadi::Item, item); QFETCH(bool, isChild); // WHEN Akonadi::Serializer serializer; bool value = serializer.isContextChild(context, item); // THEN QCOMPARE(value, isChild); } void shouldCheckIfAnItemHasContextsOrTags_data() { QTest::addColumn("item"); QTest::addColumn("contextsExpected"); QTest::addColumn("tagsExpected"); Akonadi::Tag unrelatedTag(QStringLiteral("Foo")); unrelatedTag.setType("unrelated"); Akonadi::Tag contextTag(QStringLiteral("Bar")); contextTag.setType(Akonadi::Serializer::contextTagType()); Akonadi::Tag akonadiTag(QStringLiteral("Baz")); akonadiTag.setType(Akonadi::Tag::PLAIN); Akonadi::Item item; QTest::newRow("no tags") << item << false << false; item.setTags({ unrelatedTag }); QTest::newRow("unrelated tags") << item << false << false; item.setTags({ unrelatedTag, contextTag }); QTest::newRow("has contexts") << item << true << false; item.setTags({ unrelatedTag, akonadiTag }); QTest::newRow("has tags") << item << false << true; item.setTags({ unrelatedTag, contextTag, akonadiTag }); QTest::newRow("has both") << item << true << true; } void shouldCheckIfAnItemHasContextsOrTags() { // GIVEN QFETCH(Akonadi::Item, item); QFETCH(bool, contextsExpected); QFETCH(bool, tagsExpected); Akonadi::Serializer serializer; // WHEN const bool hasContexts = serializer.hasContextTags(item); const bool hasTags = serializer.hasAkonadiTags(item); // THEN QCOMPARE(hasContexts, contextsExpected); QCOMPARE(hasTags, tagsExpected); } void shouldCreateTagFromContext_data() { QTest::addColumn("name"); QTest::addColumn("tagId"); QTest::addColumn("tagGid"); QString nameInternet = QStringLiteral("Internet"); QTest::newRow("nominal case") << QString(nameInternet) << qint64(42) << nameInternet.toLatin1(); QTest::newRow("null name case") << QString() << qint64(42) << QByteArray(); QTest::newRow("null tagId case") << QString(nameInternet)<< qint64(-1) << nameInternet.toLatin1(); QTest::newRow("totally null context case") << QString() << qint64(-1) << QByteArray(); } void shouldCreateTagFromContext() { // GIVEN QFETCH(QString, name); QFETCH(qint64, tagId); QFETCH(QByteArray, tagGid); // WHEN auto context = Domain::Context::Ptr::create(); context->setProperty("tagId", tagId); context->setName(name); Akonadi::Serializer serializer; Akonadi::Tag tag = serializer.createTagFromContext(context); // THEN QCOMPARE(tag.name(), name); QCOMPARE(tag.isValid(), tagId > 0); if (tagId > 0) { QCOMPARE(tag.id(), tagId); QCOMPARE(tag.gid(), tagGid); QCOMPARE(tag.type(), Akonadi::SerializerInterface::contextTagType()); } } void shouldCreateTagFromAkonadiTag_data() { QTest::addColumn("name"); QTest::addColumn("tagId"); QTest::addColumn("type"); QString tagName = QStringLiteral("Optional"); QByteArray plainType = Akonadi::Tag::PLAIN; QTest::newRow("nominal case") << tagName << qint64(42) << plainType; QTest::newRow("null name case") << QString() << qint64(42) << plainType; QTest::newRow("null tagId case") << tagName << qint64(-1) << plainType; QTest::newRow("totally null tag case") << QString() << qint64(-1) << plainType; } void shouldCreateTagFromAkonadiTag() { // GIVEN QFETCH(QString, name); QFETCH(qint64, tagId); QFETCH(QByteArray, type); auto akonadiTag = Akonadi::Tag(); akonadiTag.setName(name); akonadiTag.setId(tagId); akonadiTag.setType(type); // WHEN Akonadi::Serializer serializer; Domain::Tag::Ptr resultTag = serializer.createTagFromAkonadiTag(akonadiTag); // THEN QCOMPARE(resultTag->name(), akonadiTag.name()); QCOMPARE(resultTag->property("tagId").toLongLong(), akonadiTag.id()); } void shouldUpdateTagFromAkonadiTag_data() { shouldCreateTagFromAkonadiTag_data(); } void shouldUpdateTagFromAkonadiTag() { // GIVEN QFETCH(QString, name); QFETCH(qint64, tagId); QFETCH(QByteArray, type); // ... stored as an Akonadi Tag Akonadi::Tag akonadiTag(name); akonadiTag.setId(tagId); akonadiTag.setType(type); // WHEN Akonadi::Serializer serializer; auto tag = Domain::Tag::Ptr::create(); tag->setName(QStringLiteral("tag42")); serializer.updateTagFromAkonadiTag(tag, akonadiTag); // THEN QCOMPARE(tag->name(), akonadiTag.name()); QCOMPARE(tag->property("tagId").toLongLong(), akonadiTag.id()); } void shouldCreateAkonadiTagFromTag_data() { // GIVEN QTest::addColumn("name"); QTest::addColumn("tagId"); QTest::addColumn("tagGid"); const QByteArray namePhilo = "Philosophy"; QTest::newRow("nominal case") << QString(namePhilo) << qint64(42) << namePhilo; QTest::newRow("null name case") << QString() << qint64(42) << QByteArray(); QTest::newRow("null tagId case") << QString(namePhilo) << qint64(-1) << namePhilo; QTest::newRow("totally null tag case") << QString() << qint64(-1) << QByteArray(); } void shouldCreateAkonadiTagFromTag() { // GIVEN QFETCH(QString, name); QFETCH(qint64, tagId); QFETCH(QByteArray, tagGid); // WHEN auto tag = Domain::Tag::Ptr::create(); tag->setProperty("tagId", tagId); tag->setName(name); Akonadi::Serializer serializer; Akonadi::Tag akonadiTag = serializer.createAkonadiTagFromTag(tag); // THEN QCOMPARE(akonadiTag.name(), name); QCOMPARE(akonadiTag.isValid(), tagId > 0); if (tagId > 0) { QCOMPARE(akonadiTag.id(), tagId); QCOMPARE(akonadiTag.gid(), tagGid); QCOMPARE(akonadiTag.type(), QByteArray(Akonadi::Tag::PLAIN)); } } void shouldVerifyIfAnItemIsATagChild_data() { QTest::addColumn("tag"); QTest::addColumn("item"); QTest::addColumn("isChild"); // Create a Tag auto tag = Domain::Tag::Ptr::create(); tag->setProperty("tagId", qint64(43)); Akonadi::Tag akonadiTag(Akonadi::Tag::Id(43)); Akonadi::Item unrelatedItem; QTest::newRow("Unrelated item") << tag << unrelatedItem << false; Akonadi::Item relatedItem; relatedItem.setTag(akonadiTag); QTest::newRow("Related item") << tag << relatedItem << true; auto invalidTag = Domain::Tag::Ptr::create(); QTest::newRow("Invalid Tag") << invalidTag << relatedItem << false; Akonadi::Item invalidItem; QTest::newRow("Invalid Item") << tag << invalidItem << false; QTest::newRow("both invalid") << invalidTag << invalidItem << false; } void shouldVerifyIfAnItemIsATagChild() { // GIVEN QFETCH(Domain::Tag::Ptr, tag); QFETCH(Akonadi::Item, item); QFETCH(bool, isChild); // WHEN Akonadi::Serializer serializer; bool value = serializer.isTagChild(tag, item); // THEN QCOMPARE(value, isChild); } // Investigation into how to differenciate all-day events from events with time, // using QDateTime only. Doesn't seem to be possible. void KDateTimeShouldStillBeNeeded() // although I wish it wasn't... { // GIVEN a QDateTime without time information QDateTime dateOnly(QDate(2016, 6, 12), QTime(-1, -1, -1)); // THEN we can't detect that there was no time information, i.e. all day event QVERIFY(dateOnly.time().isValid()); // I wish this was "!" QVERIFY(!dateOnly.time().isNull()); // got converted to midnight localtime by QDateTime // This doesn't help, QDateTime converts "null time" to midnight. dateOnly.setTime(QTime()); QVERIFY(dateOnly.time().isValid()); // same as above QVERIFY(!dateOnly.time().isNull()); // same as above // GIVEN a QDateTime at midnight QDateTime atMidnight(QDate(2016, 6, 12), QTime(0, 0, 0)); // THEN we can detect that a time information was present QVERIFY(atMidnight.time().isValid()); QVERIFY(!atMidnight.time().isNull()); // GIVEN a KDateTime without time information KDateTime kdOnly(QDate(2016, 6, 12)); // THEN we can detect that there was no time information, i.e. all day event QVERIFY(kdOnly.isDateOnly()); } }; ZANSHIN_TEST_MAIN(AkonadiSerializerTest) #include "akonadiserializertest.moc" diff --git a/tests/units/presentation/CMakeLists.txt b/tests/units/presentation/CMakeLists.txt index 3c47c0d3..e1f740af 100644 --- a/tests/units/presentation/CMakeLists.txt +++ b/tests/units/presentation/CMakeLists.txt @@ -1,21 +1,23 @@ zanshin_auto_tests( applicationmodeltest artifacteditormodeltest artifactfilterproxymodeltest availablenotepagesmodeltest availablepagessortfilterproxymodeltest availablesourcesmodeltest availabletaskpagesmodeltest errorhandlertest errorhandlingmodelbasetest metatypestest noteinboxpagemodeltest pagemodeltest projectpagemodeltest querytreemodeltest + runningtaskmodeltest tagpagemodeltest + taskapplicationmodeltest taskinboxpagemodeltest tasklistmodeltest contextpagemodeltest workdaypagemodeltest ) diff --git a/tests/units/presentation/runningtaskmodeltest.cpp b/tests/units/presentation/runningtaskmodeltest.cpp new file mode 100644 index 00000000..d1953f54 --- /dev/null +++ b/tests/units/presentation/runningtaskmodeltest.cpp @@ -0,0 +1,170 @@ +/* This file is part of Zanshin + + Copyright 2016-2017 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 + +#include "utils/mockobject.h" +#include "utils/jobhandler.h" + +#include "presentation/runningtaskmodel.h" + +#include "testhelpers.h" + +using namespace mockitopp; +using namespace mockitopp::matcher; + +class TestDependencies +{ +public: + TestDependencies() + { + m_taskProvider = Domain::QueryResultProvider::Ptr::create(); + auto taskResult = Domain::QueryResult::create(m_taskProvider); + + m_taskQueriesMock(&Domain::TaskQueries::findAll).when().thenReturn(taskResult); + m_taskQueriesMockInstance = m_taskQueriesMock.getInstance(); + + Utils::MockObject taskRepositoryMock; + taskRepositoryMock(&Domain::TaskRepository::update).when(any()) + .thenReturn(0); + m_taskRepositoryMockInstance = taskRepositoryMock.getInstance(); + } + Utils::MockObject m_taskQueriesMock; + + Domain::TaskQueries::Ptr m_taskQueriesMockInstance; + Domain::TaskRepository::Ptr m_taskRepositoryMockInstance; + Domain::QueryResultProvider::Ptr m_taskProvider; +}; + +class RunningTaskModelTest : public QObject +{ + Q_OBJECT +private slots: + void shouldDoInitialListing() + { + // GIVEN + TestDependencies deps; + // Three tasks, one being marked as running + auto firstTask = Domain::Task::Ptr::create(); + firstTask->setTitle(QStringLiteral("rootTask")); + auto initialTask = Domain::Task::Ptr::create(); + initialTask->setTitle(QStringLiteral("initialTask")); + initialTask->setRunning(true); + auto otherTask = Domain::Task::Ptr::create(); + otherTask->setTitle(QStringLiteral("otherTask")); + + // WHEN + Presentation::RunningTaskModel model(deps.m_taskQueriesMockInstance, deps.m_taskRepositoryMockInstance); + QVERIFY(!model.runningTask()); + deps.m_taskProvider->append(firstTask); + deps.m_taskProvider->append(initialTask); + deps.m_taskProvider->append(otherTask); + + // THEN + QCOMPARE(model.runningTask(), initialTask); + } + + void shouldStartTask() + { + // GIVEN + TestDependencies deps; + Presentation::RunningTaskModel model(deps.m_taskQueriesMockInstance, deps.m_taskRepositoryMockInstance); + Domain::Task::Ptr task = Domain::Task::Ptr::create(); + QSignalSpy spy(&model, &Presentation::RunningTaskModel::runningTaskChanged); + + // WHEN + model.setRunningTask(task); + + // THEN + QCOMPARE(model.runningTask(), task); + QVERIFY(task->isRunning()); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(0).value(), task); + } + + void shouldHandleStopTask() + { + // GIVEN + TestDependencies deps; + Presentation::RunningTaskModel model(deps.m_taskQueriesMockInstance, deps.m_taskRepositoryMockInstance); + Domain::Task::Ptr task = Domain::Task::Ptr::create(); + model.setRunningTask(task); + QSignalSpy spy(&model, &Presentation::RunningTaskModel::runningTaskChanged); + + // WHEN + model.stopTask(); + + // THEN + QCOMPARE(model.runningTask(), Domain::Task::Ptr(nullptr)); + QVERIFY(!task->isRunning()); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(0).value(), Domain::Task::Ptr(nullptr)); + } + + void shouldHandleDoneTask() + { + // GIVEN + TestDependencies deps; + Presentation::RunningTaskModel model(deps.m_taskQueriesMockInstance, deps.m_taskRepositoryMockInstance); + Domain::Task::Ptr task = Domain::Task::Ptr::create(); + model.setRunningTask(task); + QSignalSpy spy(&model, &Presentation::RunningTaskModel::runningTaskChanged); + + // WHEN + model.doneTask(); + + // THEN + QCOMPARE(model.runningTask(), Domain::Task::Ptr(nullptr)); + QVERIFY(!task->isRunning()); + QVERIFY(task->isDone()); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(0).value(), Domain::Task::Ptr(nullptr)); + } + + void shouldHandleSwitchingToAnotherTask() + { + // GIVEN + TestDependencies deps; + Presentation::RunningTaskModel model(deps.m_taskQueriesMockInstance, deps.m_taskRepositoryMockInstance); + Domain::Task::Ptr task = Domain::Task::Ptr::create(); + model.setRunningTask(task); + Domain::Task::Ptr task2 = Domain::Task::Ptr::create(); + QSignalSpy spy(&model, &Presentation::RunningTaskModel::runningTaskChanged); + + // WHEN + model.setRunningTask(task2); + + // THEN + QCOMPARE(model.runningTask(), task2); + QVERIFY(!task->isRunning()); + QVERIFY(task2->isRunning()); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(0).value(), task2); + } + +private: +}; + +ZANSHIN_TEST_MAIN(RunningTaskModelTest) + +#include "runningtaskmodeltest.moc" diff --git a/tests/units/presentation/taskapplicationmodeltest.cpp b/tests/units/presentation/taskapplicationmodeltest.cpp new file mode 100644 index 00000000..87434b12 --- /dev/null +++ b/tests/units/presentation/taskapplicationmodeltest.cpp @@ -0,0 +1,70 @@ +/* This file is part of Zanshin + + Copyright 2014 Kevin Ottens + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 + +#include "utils/dependencymanager.h" +#include "utils/jobhandler.h" +#include "utils/mockobject.h" + +#include "presentation/taskapplicationmodel.h" +#include "presentation/runningtaskmodel.h" + +#include "testlib/fakejob.h" + +using namespace mockitopp; +using namespace mockitopp::matcher; + + +class TaskApplicationModelTest : public QObject +{ + Q_OBJECT +public: + explicit TaskApplicationModelTest(QObject *parent = Q_NULLPTR) + : QObject(parent) + { + Utils::DependencyManager::globalInstance().add( + [] (Utils::DependencyManager *) { + return new Presentation::RunningTaskModel(Domain::TaskQueries::Ptr(), + Domain::TaskRepository::Ptr()); + }); + } + +private slots: + void shouldProvideRunningTaskModel() + { + // GIVEN + Presentation::TaskApplicationModel app; + + // WHEN + QObject *model = app.runningTaskModel(); + + // THEN + QVERIFY(qobject_cast(model)); + } + +}; + +ZANSHIN_TEST_MAIN(TaskApplicationModelTest) + +#include "taskapplicationmodeltest.moc" diff --git a/tests/units/widgets/CMakeLists.txt b/tests/units/widgets/CMakeLists.txt index 43408cbf..291bd3de 100644 --- a/tests/units/widgets/CMakeLists.txt +++ b/tests/units/widgets/CMakeLists.txt @@ -1,17 +1,19 @@ zanshin_auto_tests( applicationcomponentstest availablepagesviewtest availablesourcesviewtest datasourcedelegatetest editorviewtest filterwidgettest newprojectdialogtest pageviewerrorhandlertest pageviewtest quickselectdialogtest + runningtaskwidgettest scripteditortest + taskapplicationcomponentstest ) # These tests need a window that takes focus set_tests_properties(tests-units-widgets-editorviewtest PROPERTIES RUN_SERIAL TRUE) set_tests_properties(tests-units-widgets-pageviewtest PROPERTIES RUN_SERIAL TRUE) diff --git a/tests/units/widgets/pageviewtest.cpp b/tests/units/widgets/pageviewtest.cpp index ba600fbe..111ac97d 100644 --- a/tests/units/widgets/pageviewtest.cpp +++ b/tests/units/widgets/pageviewtest.cpp @@ -1,678 +1,746 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 #include #include #include #include #include #include #include #include #include +#include #include #include "domain/task.h" #include "presentation/artifactfilterproxymodel.h" #include "presentation/metatypes.h" +#include "presentation/querytreemodelbase.h" #include "widgets/filterwidget.h" #include "widgets/itemdelegate.h" #include "widgets/pageview.h" #include "messageboxstub.h" class PageModelStub : public QObject { Q_OBJECT Q_PROPERTY(QAbstractItemModel* centralListModel READ centralListModel) public: QAbstractItemModel *centralListModel() { return &itemModel; } - void addStubItem(const QString &title, QStandardItem *parentItem = Q_NULLPTR) + QStandardItem *addStubItem(const QString &title, QStandardItem *parentItem = Q_NULLPTR) { QStandardItem *item = new QStandardItem; item->setData(title, Qt::DisplayRole); if (!parentItem) itemModel.appendRow(item); else parentItem->appendRow(item); taskNames << title; + return item; + } + + Domain::Task::Ptr addTaskItem(const QString &title, QStandardItem *parentItem = Q_NULLPTR) + { + auto item = addStubItem(title, parentItem); + auto task = Domain::Task::Ptr::create(); + task->setTitle(title); + item->setData(QVariant::fromValue(task), Presentation::QueryTreeModelBase::ObjectRole); + return task; } void addStubItems(const QStringList &list) { foreach (const QString &title, list) { addStubItem(title); } } public slots: void addItem(const QString &name, const QModelIndex &parentIndex) { taskNames << name; parentIndices << parentIndex; } void removeItem(const QModelIndex &index) { removedIndices << index; } void promoteItem(const QModelIndex &index) { promotedIndices << index; } public: QStringList taskNames; QList parentIndices; QList removedIndices; QList promotedIndices; QStandardItemModel itemModel; }; +class RunningTaskModelStub : public Presentation::RunningTaskModelInterface +{ + Q_OBJECT +public: + Domain::Task::Ptr runningTask() const Q_DECL_OVERRIDE { return m_runningTask; } + void setRunningTask(const Domain::Task::Ptr &task) Q_DECL_OVERRIDE { m_runningTask = task; } +public slots: + void stopTask() Q_DECL_OVERRIDE {} + void doneTask() Q_DECL_OVERRIDE {} +private: + Domain::Task::Ptr m_runningTask; +}; + class PageViewTest : public QObject { Q_OBJECT private slots: void shouldHaveDefaultState() { Widgets::PageView page; QCOMPARE(page.contentsMargins(), QMargins(0, 0, 0, 0)); QCOMPARE(page.layout()->contentsMargins(), QMargins(0, 0, 0, 3)); auto messageWidget = page.findChild(QStringLiteral("messageWidget")); QVERIFY(messageWidget); QVERIFY(!messageWidget->isVisibleTo(&page)); QVERIFY(!messageWidget->isCloseButtonVisible()); QVERIFY(messageWidget->wordWrap()); QVERIFY(messageWidget->text().isEmpty()); QVERIFY(messageWidget->icon().isNull()); QCOMPARE(messageWidget->messageType(), KMessageWidget::Error); QVERIFY(!messageWidget->isShowAnimationRunning()); QVERIFY(!messageWidget->isHideAnimationRunning()); auto centralView = page.findChild(QStringLiteral("centralView")); QVERIFY(centralView); QVERIFY(centralView->isVisibleTo(&page)); QVERIFY(!centralView->header()->isVisibleTo(&page)); QVERIFY(qobject_cast(centralView->itemDelegate())); QVERIFY(centralView->alternatingRowColors()); QCOMPARE(centralView->dragDropMode(), QTreeView::DragDrop); auto filter = page.findChild(QStringLiteral("filterWidget")); QVERIFY(filter); QVERIFY(!filter->isVisibleTo(&page)); QVERIFY(filter->proxyModel()); QCOMPARE(filter->proxyModel(), centralView->model()); QLineEdit *quickAddEdit = page.findChild(QStringLiteral("quickAddEdit")); QVERIFY(quickAddEdit); QVERIFY(quickAddEdit->isVisibleTo(&page)); QVERIFY(quickAddEdit->text().isEmpty()); QCOMPARE(quickAddEdit->placeholderText(), tr("Type and press enter to add an item")); auto addAction = page.findChild(QStringLiteral("addItemAction")); QVERIFY(addAction); auto cancelAddAction = page.findChild(QStringLiteral("cancelAddItemAction")); QVERIFY(cancelAddAction); auto removeAction = page.findChild(QStringLiteral("removeItemAction")); QVERIFY(removeAction); auto promoteAction = page.findChild(QStringLiteral("promoteItemAction")); QVERIFY(promoteAction); auto filterAction = page.findChild(QStringLiteral("filterViewAction")); QVERIFY(filterAction); QVERIFY(filterAction->isCheckable()); QVERIFY(!filterAction->isChecked()); + auto runTaskAction = page.findChild(QStringLiteral("runTaskAction")); + QVERIFY(runTaskAction); + QVERIFY(!runTaskAction->isEnabled()); auto actions = page.globalActions(); QCOMPARE(actions.value(QStringLiteral("page_view_add")), addAction); QCOMPARE(actions.value(QStringLiteral("page_view_remove")), removeAction); QCOMPARE(actions.value(QStringLiteral("page_view_promote")), promoteAction); QCOMPARE(actions.value(QStringLiteral("page_view_filter")), filterAction); + QCOMPARE(actions.value(QStringLiteral("page_run_task")), runTaskAction); } void shouldDisplayListFromPageModel() { // GIVEN QStandardItemModel model; QObject stubPageModel; stubPageModel.setProperty("centralListModel", QVariant::fromValue(static_cast(&model))); Widgets::PageView page; auto centralView = page.findChild(QStringLiteral("centralView")); QVERIFY(centralView); auto proxyModel = qobject_cast(centralView->model()); QVERIFY(proxyModel); QVERIFY(!proxyModel->sourceModel()); // WHEN page.setModel(&stubPageModel); // THEN QCOMPARE(page.model(), &stubPageModel); QVERIFY(page.isEnabled()); QCOMPARE(proxyModel->sourceModel(), &model); } void shouldNotCrashWithNullModel() { // GIVEN QStandardItemModel model; QObject stubPageModel; stubPageModel.setProperty("centralListModel", QVariant::fromValue(static_cast(&model))); Widgets::PageView page; page.setModel(&stubPageModel); auto centralView = page.findChild(QStringLiteral("centralView")); QVERIFY(centralView); auto proxyModel = qobject_cast(centralView->model()); QVERIFY(proxyModel); QCOMPARE(proxyModel->sourceModel(), &model); // WHEN page.setModel(Q_NULLPTR); // THEN QVERIFY(!page.model()); QVERIFY(!page.isEnabled()); QVERIFY(!proxyModel->sourceModel()); } void shouldManageFocusThroughActions() { // GIVEN Widgets::PageView page; auto centralView = page.findChild(QStringLiteral("centralView")); auto quickAddEdit = page.findChild(QStringLiteral("quickAddEdit")); auto filter = page.findChild(QStringLiteral("filterWidget")); auto filterEdit = filter->findChild(); QVERIFY(filterEdit); page.show(); QTest::qWaitForWindowShown(&page); centralView->setFocus(); QVERIFY(centralView->hasFocus()); QVERIFY(!quickAddEdit->hasFocus()); QVERIFY(!filter->isVisibleTo(&page)); QVERIFY(!filterEdit->hasFocus()); auto addAction = page.findChild(QStringLiteral("addItemAction")); auto cancelAddAction = page.findChild(QStringLiteral("cancelAddItemAction")); auto filterAction = page.findChild(QStringLiteral("filterViewAction")); // WHEN addAction->trigger(); // THEN QVERIFY(!centralView->hasFocus()); QVERIFY(quickAddEdit->hasFocus()); QVERIFY(!filter->isVisibleTo(&page)); QVERIFY(!filterEdit->hasFocus()); // WHEN cancelAddAction->trigger(); // THEN QVERIFY(centralView->hasFocus()); QVERIFY(!quickAddEdit->hasFocus()); QVERIFY(!filter->isVisibleTo(&page)); QVERIFY(!filterEdit->hasFocus()); // WHEN filterAction->trigger(); // THEN QVERIFY(!centralView->hasFocus()); QVERIFY(!quickAddEdit->hasFocus()); QVERIFY(filter->isVisibleTo(&page)); QVERIFY(filterEdit->hasFocus()); // WHEN cancelAddAction->trigger(); // THEN QVERIFY(centralView->hasFocus()); QVERIFY(!quickAddEdit->hasFocus()); QVERIFY(filter->isVisibleTo(&page)); QVERIFY(!filterEdit->hasFocus()); } void shouldManageFilterVisibilityThroughAction() { // GIVEN Widgets::PageView page; auto centralView = page.findChild(QStringLiteral("centralView")); auto filter = page.findChild(QStringLiteral("filterWidget")); auto filterEdit = filter->findChild(); QVERIFY(filterEdit); page.show(); QTest::qWaitForWindowShown(&page); centralView->setFocus(); QVERIFY(centralView->hasFocus()); QVERIFY(!filter->isVisibleTo(&page)); QVERIFY(!filterEdit->hasFocus()); auto filterAction = page.findChild(QStringLiteral("filterViewAction")); // WHEN filterAction->trigger(); // THEN QVERIFY(!centralView->hasFocus()); QVERIFY(filter->isVisibleTo(&page)); QVERIFY(filterEdit->hasFocus()); // WHEN filterEdit->setText("Foo"); // THEN QCOMPARE(filterEdit->text(), QString("Foo")); // WHEN filterAction->trigger(); // THEN QVERIFY(centralView->hasFocus()); QVERIFY(!filter->isVisibleTo(&page)); QVERIFY(!filterEdit->hasFocus()); QVERIFY(filterEdit->text().isEmpty()); } void shouldCreateTasksWithNoParentWhenHittingReturnWithoutSelectedIndex() { // GIVEN PageModelStub stubPageModel; Widgets::PageView page; page.setModel(&stubPageModel); auto quickAddEdit = page.findChild(QStringLiteral("quickAddEdit")); // WHEN QTest::keyClick(quickAddEdit, Qt::Key_Return); // Does nothing (edit empty) QTest::keyClicks(quickAddEdit, QStringLiteral("Foo")); QTest::keyClick(quickAddEdit, Qt::Key_Return); QTest::keyClick(quickAddEdit, Qt::Key_Return); // Does nothing (edit empty) QTest::keyClicks(quickAddEdit, QStringLiteral("Bar")); QTest::keyClick(quickAddEdit, Qt::Key_Return); QTest::keyClick(quickAddEdit, Qt::Key_Return); // Does nothing (edit empty) // THEN QCOMPARE(stubPageModel.taskNames, QStringList() << QStringLiteral("Foo") << QStringLiteral("Bar")); QCOMPARE(stubPageModel.parentIndices.size(), 2); QCOMPARE(stubPageModel.parentIndices.first(), QPersistentModelIndex()); QCOMPARE(stubPageModel.parentIndices.last(), QPersistentModelIndex()); } void shouldCreateTasksWithNoParentWhenHittingReturnWithSeveralSelectedIndices() { // GIVEN PageModelStub stubPageModel; Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); stubPageModel.addStubItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C")); QPersistentModelIndex index0 = stubPageModel.itemModel.index(0, 0); QPersistentModelIndex index1 = stubPageModel.itemModel.index(1, 0); Widgets::PageView page; page.setModel(&stubPageModel); auto centralView = page.findChild(QStringLiteral("centralView")); centralView->selectionModel()->select(index0, QItemSelectionModel::ClearAndSelect); centralView->selectionModel()->select(index1, QItemSelectionModel::Select); auto quickAddEdit = page.findChild(QStringLiteral("quickAddEdit")); // WHEN QTest::keyClicks(quickAddEdit, QStringLiteral("Foo")); QTest::keyClick(quickAddEdit, Qt::Key_Return); // THEN QCOMPARE(stubPageModel.taskNames, QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C") << QStringLiteral("Foo")); QCOMPARE(stubPageModel.parentIndices.size(), 1); QCOMPARE(stubPageModel.parentIndices.first(), QPersistentModelIndex()); } void shouldCreateTasksWithParentWhenHittingReturnWithOneSelectedIndex() { // GIVEN PageModelStub stubPageModel; Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); stubPageModel.addStubItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C")); QPersistentModelIndex index = stubPageModel.itemModel.index(1, 0); Widgets::PageView page; page.setModel(&stubPageModel); auto centralView = page.findChild(QStringLiteral("centralView")); centralView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect); auto quickAddEdit = page.findChild(QStringLiteral("quickAddEdit")); // WHEN QTest::keyClicks(quickAddEdit, QStringLiteral("Foo")); QTest::keyClick(quickAddEdit, Qt::Key_Return); // THEN QCOMPARE(stubPageModel.taskNames, QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C") << QStringLiteral("Foo")); QCOMPARE(stubPageModel.parentIndices.size(), 1); QCOMPARE(stubPageModel.parentIndices.first(), index); } void shouldDeleteItemWhenHittingTheDeleteKey() { // GIVEN PageModelStub stubPageModel; Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); stubPageModel.addStubItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C")); QPersistentModelIndex index = stubPageModel.itemModel.index(1, 0); Widgets::PageView page; page.setModel(&stubPageModel); QTreeView *centralView = page.findChild(QStringLiteral("centralView")); centralView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); centralView->setFocus(); // Needed for shortcuts to work page.show(); QTest::qWaitForWindowShown(&page); QTest::qWait(100); // WHEN QTest::keyPress(centralView, Qt::Key_Delete); // THEN QCOMPARE(stubPageModel.removedIndices.size(), 1); QCOMPARE(stubPageModel.removedIndices.first(), index); } void shouldNoteTryToDeleteIfThereIsNoSelection() { // GIVEN PageModelStub stubPageModel; Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); stubPageModel.addStubItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C")); Widgets::PageView page; page.setModel(&stubPageModel); QTreeView *centralView = page.findChild(QStringLiteral("centralView")); centralView->clearSelection(); page.findChild(QStringLiteral("quickAddEdit"))->setFocus(); // Needed for shortcuts to work page.show(); QTest::qWaitForWindowShown(&page); QTest::qWait(100); // WHEN QTest::keyPress(centralView, Qt::Key_Delete); // THEN QVERIFY(stubPageModel.removedIndices.isEmpty()); } void shouldDisplayNotificationWhenHittingTheDeleteKeyOnAnItemWithChildren() { // GIVEN PageModelStub stubPageModel; Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); stubPageModel.addStubItems(QStringList() << QStringLiteral("A") << QStringLiteral("B")); QStandardItem *parentIndex = stubPageModel.itemModel.item(1, 0); stubPageModel.addStubItem(QStringLiteral("C"), parentIndex); QPersistentModelIndex index = stubPageModel.itemModel.index(1, 0); Widgets::PageView page; page.setModel(&stubPageModel); auto msgbox = MessageBoxStub::Ptr::create(); page.setMessageBoxInterface(msgbox); QTreeView *centralView = page.findChild(QStringLiteral("centralView")); centralView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); + QVERIFY(centralView->selectionModel()->currentIndex().isValid()); centralView->setFocus(); // Needed for shortcuts to work page.show(); QTest::qWaitForWindowShown(&page); QTest::qWait(100); // WHEN QTest::keyPress(centralView, Qt::Key_Delete); // THEN QVERIFY(msgbox->called()); QCOMPARE(stubPageModel.removedIndices.size(), 1); QCOMPARE(stubPageModel.removedIndices.first(), index); } void shouldDeleteItemsWhenHittingTheDeleteKey() { // GIVEN PageModelStub stubPageModel; Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); stubPageModel.addStubItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C")); QPersistentModelIndex index = stubPageModel.itemModel.index(1, 0); QPersistentModelIndex index2 = stubPageModel.itemModel.index(2, 0); Widgets::PageView page; page.setModel(&stubPageModel); auto msgbox = MessageBoxStub::Ptr::create(); page.setMessageBoxInterface(msgbox); QTreeView *centralView = page.findChild(QStringLiteral("centralView")); centralView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); centralView->selectionModel()->setCurrentIndex(index2, QItemSelectionModel::Select); centralView->setFocus(); // Needed for shortcuts to work page.show(); QTest::qWaitForWindowShown(&page); QTest::qWait(100); // WHEN QTest::keyPress(centralView, Qt::Key_Delete); // THEN QVERIFY(msgbox->called()); QCOMPARE(stubPageModel.removedIndices.size(), 2); QCOMPARE(stubPageModel.removedIndices.first(), index); QCOMPARE(stubPageModel.removedIndices.at(1), index2); } void shouldPromoteItem() { // GIVEN PageModelStub stubPageModel; Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); stubPageModel.addStubItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C")); QPersistentModelIndex index = stubPageModel.itemModel.index(1, 0); Widgets::PageView page; page.setModel(&stubPageModel); auto promoteAction = page.findChild(QStringLiteral("promoteItemAction")); QVERIFY(promoteAction); QTreeView *centralView = page.findChild(QStringLiteral("centralView")); centralView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); centralView->setFocus(); // WHEN promoteAction->trigger(); // THEN QCOMPARE(stubPageModel.promotedIndices.size(), 1); QCOMPARE(stubPageModel.promotedIndices.first(), index); } void shouldNotTryToPromoteItemIfThereIsNoSelection() { // GIVEN PageModelStub stubPageModel; Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); stubPageModel.addStubItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C")); Widgets::PageView page; page.setModel(&stubPageModel); auto promoteAction = page.findChild(QStringLiteral("promoteItemAction")); QVERIFY(promoteAction); QTreeView *centralView = page.findChild(QStringLiteral("centralView")); centralView->selectionModel()->clear(); centralView->setFocus(); // WHEN promoteAction->trigger(); // THEN QVERIFY(stubPageModel.promotedIndices.isEmpty()); } void shouldClearCentralViewSelectionOnEscape() { // GIVEN PageModelStub stubPageModel; Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); stubPageModel.addStubItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C")); QPersistentModelIndex index = stubPageModel.itemModel.index(1, 0); Widgets::PageView page; page.setModel(&stubPageModel); auto centralView = page.findChild(QStringLiteral("centralView")); centralView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect); QVERIFY(!centralView->selectionModel()->selectedIndexes().isEmpty()); // WHEN QTest::keyClick(centralView, Qt::Key_Escape); // THEN QVERIFY(centralView->selectionModel()->selectedIndexes().isEmpty()); } void shouldReturnSelectedIndexes() { // GIVEN PageModelStub stubPageModel; Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); stubPageModel.addStubItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C")); auto index = stubPageModel.itemModel.index(1, 0); auto index2 = stubPageModel.itemModel.index(2, 0); Widgets::PageView page; page.setModel(&stubPageModel); auto centralView = page.findChild(QStringLiteral("centralView")); auto filterWidget = page.findChild(QStringLiteral("filterWidget")); auto displayedModel = filterWidget->proxyModel(); auto displayedIndex = displayedModel->index(1, 0); auto displayedIndex2 = displayedModel->index(2, 0); // WHEN centralView->selectionModel()->setCurrentIndex(displayedIndex, QItemSelectionModel::ClearAndSelect); centralView->selectionModel()->setCurrentIndex(displayedIndex2, QItemSelectionModel::Select); // THEN auto selectedIndexes = page.selectedIndexes(); QCOMPARE(selectedIndexes.size(), 2); QCOMPARE(selectedIndexes.at(0), index); QCOMPARE(selectedIndexes.at(1), index2); QCOMPARE(selectedIndexes.at(0).model(), index.model()); QCOMPARE(selectedIndexes.at(1).model(), index2.model()); } void shouldDisplayMessageOnError() { // GIVEN Widgets::PageView page; page.show(); QTest::qWaitForWindowShown(&page); QTest::qWait(100); auto messageWidget = page.findChild(QStringLiteral("messageWidget")); QVERIFY(messageWidget); QVERIFY(!messageWidget->isVisibleTo(&page)); QCOMPARE(messageWidget->findChildren().size(), 1); auto closeButton = messageWidget->findChildren().first(); QVERIFY(closeButton); // WHEN page.displayErrorMessage(QStringLiteral("Foo Error")); // THEN QVERIFY(messageWidget->isVisibleTo(&page)); QVERIFY(messageWidget->isCloseButtonVisible()); QCOMPARE(messageWidget->text(), QStringLiteral("Foo Error")); QVERIFY(messageWidget->icon().isNull()); QCOMPARE(messageWidget->messageType(), KMessageWidget::Error); QVERIFY(messageWidget->isShowAnimationRunning()); QVERIFY(!messageWidget->isHideAnimationRunning()); // WHEN QTest::qWait(800); // THEN QVERIFY(!messageWidget->isShowAnimationRunning()); QVERIFY(!messageWidget->isHideAnimationRunning()); // WHEN closeButton->click(); // THEN QVERIFY(!messageWidget->isShowAnimationRunning()); QVERIFY(messageWidget->isHideAnimationRunning()); // WHEN QTest::qWait(800); // THEN QVERIFY(!messageWidget->isShowAnimationRunning()); QVERIFY(!messageWidget->isHideAnimationRunning()); } + + void shouldRunTask() + { + // GIVEN + PageModelStub stubPageModel; + Q_ASSERT(stubPageModel.property("centralListModel").canConvert()); + auto task1 = stubPageModel.addTaskItem(QStringLiteral("Task1")); + auto task2 = stubPageModel.addTaskItem(QStringLiteral("Task2")); + Widgets::PageView page; + page.setModel(&stubPageModel); + RunningTaskModelStub stubRunningTaskModel; + page.setRunningTaskModel(&stubRunningTaskModel); + auto centralView = page.findChild(QStringLiteral("centralView")); + + QModelIndex index = stubPageModel.itemModel.index(0, 0); + centralView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); + QVERIFY(centralView->selectionModel()->currentIndex().isValid()); + + auto runTaskAction = page.findChild(QStringLiteral("runTaskAction")); + QVERIFY(runTaskAction); + QVERIFY(runTaskAction->isEnabled()); + + // WHEN starting the first task + runTaskAction->trigger(); + + // THEN + QCOMPARE(stubRunningTaskModel.property("runningTask").value(), task1); + QCOMPARE(task1->startDate().date(), QDate::currentDate()); + + // WHEN starting the second task + QModelIndex index2 = stubPageModel.itemModel.index(1, 0); + centralView->selectionModel()->setCurrentIndex(index2, QItemSelectionModel::ClearAndSelect); + runTaskAction->trigger(); + + // THEN + QCOMPARE(stubRunningTaskModel.property("runningTask").value(), task2); + QCOMPARE(task2->startDate().date(), QDate::currentDate()); + } }; ZANSHIN_TEST_MAIN(PageViewTest) #include "pageviewtest.moc" diff --git a/tests/units/widgets/runningtaskwidgettest.cpp b/tests/units/widgets/runningtaskwidgettest.cpp new file mode 100644 index 00000000..3903c405 --- /dev/null +++ b/tests/units/widgets/runningtaskwidgettest.cpp @@ -0,0 +1,175 @@ +/* This file is part of Zanshin + + Copyright 2016-2017 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 + +#include "widgets/runningtaskwidget.h" +#include "presentation/runningtaskmodelinterface.h" +#include + +class RunningTaskModelStub : public Presentation::RunningTaskModelInterface +{ + Q_OBJECT +public: + Domain::Task::Ptr runningTask() const Q_DECL_OVERRIDE { return m_runningTask; } + void setRunningTask(const Domain::Task::Ptr &runningTask) Q_DECL_OVERRIDE + { + m_runningTask = runningTask; + emit runningTaskChanged(m_runningTask); + } + +public slots: + void stopTask() Q_DECL_OVERRIDE + { + Q_ASSERT(m_runningTask); + setRunningTask(nullptr); + } + + void doneTask() Q_DECL_OVERRIDE + { + Q_ASSERT(m_runningTask); + m_runningTask->setDone(true); + stopTask(); + } + +private: + Domain::Task::Ptr m_runningTask; +}; + +class RunningTaskWidgetTest : public QObject +{ + Q_OBJECT +private slots: + void shouldHaveDefaultState() + { + Widgets::RunningTaskWidget widget; + QVERIFY(widget.isHidden()); + } + + void shouldShowWhenRunningATask() + { + // GIVEN + Widgets::RunningTaskWidget widget; + auto task = Domain::Task::Ptr::create(); + RunningTaskModelStub model; + widget.setModel(&model); + + // WHEN + model.setRunningTask(task); + + // THEN + QVERIFY(!widget.isHidden()); + } + + void shouldShowWhenRunningADifferentTask() + { + // GIVEN + Widgets::RunningTaskWidget widget; + auto task1 = Domain::Task::Ptr::create(); + auto task2 = Domain::Task::Ptr::create(); + RunningTaskModelStub model; + widget.setModel(&model); + model.setRunningTask(task1); + + // WHEN + model.setRunningTask(task2); + + // THEN + QVERIFY(!widget.isHidden()); + } + + void shouldStopAndHideOnClickingStop() + { + // GIVEN + Widgets::RunningTaskWidget widget; + auto task = Domain::Task::Ptr::create(); + RunningTaskModelStub model; + widget.setModel(&model); + model.setRunningTask(task); + auto button = widget.findChild("stopButton"); + QVERIFY(button); + + // WHEN + button->click(); + + // THEN stopTask should have been called + QCOMPARE(model.runningTask(), Domain::Task::Ptr(nullptr)); + QVERIFY(widget.isHidden()); + } + + void shouldMarkAsDoneAndHideOnClickingDone() + { + // GIVEN + Widgets::RunningTaskWidget widget; + auto task = Domain::Task::Ptr::create(); + RunningTaskModelStub model; + widget.setModel(&model); + model.setRunningTask(task); + QVERIFY(!task->isDone()); + auto button = widget.findChild("doneButton"); + QVERIFY(button); + + // WHEN + button->click(); + + // THEN doneTask should have been called + QVERIFY(task->isDone()); + QVERIFY(widget.isHidden()); + } + + void shouldHideOnExternalStop() + { + // GIVEN + Widgets::RunningTaskWidget widget; + auto task = Domain::Task::Ptr::create(); + RunningTaskModelStub model; + widget.setModel(&model); + model.setRunningTask(task); + + // WHEN + model.stopTask(); + + // THEN + QVERIFY(widget.isHidden()); + } + + void shouldHideOnExternalDone() + { + // GIVEN + Widgets::RunningTaskWidget widget; + auto task = Domain::Task::Ptr::create(); + RunningTaskModelStub model; + widget.setModel(&model); + model.setRunningTask(task); + + // WHEN + model.doneTask(); + + // THEN + QVERIFY(widget.isHidden()); + } +}; + +ZANSHIN_TEST_MAIN(RunningTaskWidgetTest) + +#include "runningtaskwidgettest.moc" diff --git a/tests/units/widgets/taskapplicationcomponentstest.cpp b/tests/units/widgets/taskapplicationcomponentstest.cpp new file mode 100644 index 00000000..92a98a6a --- /dev/null +++ b/tests/units/widgets/taskapplicationcomponentstest.cpp @@ -0,0 +1,89 @@ +/* This file is part of Zanshin + + Copyright 2017 David Faure + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 + +#include "widgets/pageview.h" +#include "domain/task.h" + +#include "widgets/taskapplicationcomponents.h" +#include "widgets/runningtaskwidget.h" +#include "presentation/runningtaskmodelinterface.h" + +class RunningTaskModelStub : public Presentation::RunningTaskModelInterface +{ + Q_OBJECT +public: + Domain::Task::Ptr runningTask() const Q_DECL_OVERRIDE { return {}; } + void setRunningTask(const Domain::Task::Ptr &) Q_DECL_OVERRIDE {} +public slots: + void stopTask() Q_DECL_OVERRIDE {} + void doneTask() Q_DECL_OVERRIDE {} +}; + +class TaskApplicationComponentsTest : public QObject +{ + Q_OBJECT +public: + explicit TaskApplicationComponentsTest(QObject *parent = Q_NULLPTR) + : QObject(parent) + { + } + +private slots: + void shouldApplyRunningTaskModelToPageView() + { + // GIVEN + Widgets::TaskApplicationComponents components; + auto appModelStub = QObjectPtr::create(); + RunningTaskModelStub runningTaskModelStub; + appModelStub->setProperty("runningTaskModel", QVariant::fromValue(&runningTaskModelStub)); + + // WHEN + components.setModel(appModelStub); + auto pageView = components.pageView(); + + // THEN + QCOMPARE(pageView->runningTaskModel(), &runningTaskModelStub); + } + + void shouldApplyRunningTaskModelToRunningTaskWidget() + { + // GIVEN + Widgets::TaskApplicationComponents components; + auto appModelStub = QObjectPtr::create(); + RunningTaskModelStub runningTaskModelStub; + appModelStub->setProperty("runningTaskModel", QVariant::fromValue(&runningTaskModelStub)); + + // WHEN + components.setModel(appModelStub); + auto runningTaskWidget = components.runningTaskWidget(); + + // THEN + QCOMPARE(runningTaskWidget->model(), &runningTaskModelStub); + } +}; + +ZANSHIN_TEST_MAIN(TaskApplicationComponentsTest) + +#include "taskapplicationcomponentstest.moc"