Commit 463f23a1 authored by Carl Schwan's avatar Carl Schwan 🚴
Browse files

Add a simple contact view



For now, only a few properties are exposed, future work should expose
more of it and also add a contact editor.

Signed-off-by: Carl Schwan's avatarCarl Schwan <carl@carlschwan.eu>
parent 06598663
Pipeline #174050 passed with stage
in 6 minutes and 20 seconds
......@@ -10,3 +10,4 @@ CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
Testing/
.cache
......@@ -6,3 +6,7 @@ Files: logo.png org.kde.kalendar.svg
Copyright: 2021 Áron Kovács <aronkvh@gmail.com>
License: LGPL-2.0-or-later
Files: src/contents/resources/fallbackBackground.png
Copyright: Jonah Brüchert <jbb.prv@gmx.de>
License: GPL-3.0-or-later
......@@ -72,7 +72,11 @@ if (BUILD_TESTING)
add_subdirectory(autotests)
endif()
install(PROGRAMS org.kde.kalendar.desktop DESTINATION ${KDE_INSTALL_APPDIR})
if (BUILD_FLATPAK)
install(PROGRAMS org.kde.kalendar.desktop DESTINATION ${KDE_INSTALL_APPDIR} RENAME org.kde.kontact.kalendar.desktop)
else()
install(PROGRAMS org.kde.kalendar.desktop DESTINATION ${KDE_INSTALL_APPDIR})
endif()
install(FILES org.kde.kalendar.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES org.kde.kalendar.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
......
......@@ -19,8 +19,6 @@ set(kalendar_SRCS
calendarmanager.h
models/commandbarfiltermodel.cpp
models/commandbarfiltermodel.h
contactsmanager.cpp
contactsmanager.h
models/hourlyincidencemodel.cpp
models/hourlyincidencemodel.h
models/incidenceoccurrencemodel.cpp
......@@ -48,6 +46,18 @@ set(kalendar_SRCS
models/timezonelistmodel.h
models/todosortfilterproxymodel.cpp
models/todosortfilterproxymodel.h
models/colorproxymodel.h
models/colorproxymodel.cpp
contacts/addresseewrapper.cpp
contacts/addresseewrapper.h
contacts/addressmodel.cpp
contacts/addressmodel.h
contacts/globalcontactmodel.cpp
contacts/globalcontactmodel.h
contacts/contactmanager.h
contacts/contactmanager.cpp
contacts/contactcollectionmodel.cpp
contacts/contactcollectionmodel.h
resources.qrc
)
qt_add_dbus_adaptor(kalendar_SRCS org.kde.calendar.Calendar.xml kalendarapplication.h KalendarApplication)
......
......@@ -12,39 +12,51 @@
// Akonadi
#include "kalendar_debug.h"
#include <Akonadi/AgentFilterProxyModel>
#include <Akonadi/AgentInstanceModel>
#include <Akonadi/AgentManager>
#include <Akonadi/AttributeFactory>
#include <Akonadi/Collection>
#include <Akonadi/CollectionColorAttribute>
#include <Akonadi/CollectionDeleteJob>
#include <Akonadi/CollectionFilterProxyModel>
#include <Akonadi/CollectionIdentificationAttribute>
#include <Akonadi/CollectionModifyJob>
#include <Akonadi/CollectionPropertiesDialog>
#include <Akonadi/CollectionUtils>
#include <Akonadi/Control>
#include <Akonadi/ETMViewStateSaver>
#include <Akonadi/EntityDisplayAttribute>
#include <Akonadi/EntityRightsFilterModel>
#include <Akonadi/EntityTreeModel>
#include <Akonadi/ItemModifyJob>
#include <Akonadi/ItemMoveJob>
#include <Akonadi/Monitor>
#if AKONADICALENDAR_VERSION > QT_VERSION_CHECK(5, 19, 41)
#include <Akonadi/IncidenceChanger>
#include <Akonadi/History>
#else
#include <Akonadi/Calendar/IncidenceChanger>
#include <Akonadi/Calendar/History>
#endif
#include <CalendarSupport/KCalPrefs>
#include <CalendarSupport/Utils>
#include <EventViews/Prefs>
#include <KCheckableProxyModel>
#include <KDescendantsProxyModel>
#include <KLocalizedString>
#include <QApplication>
#include <QDebug>
#include <QPointer>
#include <QRandomGenerator>
#include <models/todosortfilterproxymodel.h>
#if AKONADICALENDAR_VERSION > QT_VERSION_CHECK(5, 19, 41)
#include <Akonadi/ETMCalendar>
#else
#include <etmcalendar.h>
#endif
#include "models/colorproxymodel.h"
using namespace Akonadi;
......@@ -67,24 +79,6 @@ bool isStandardCalendar(Akonadi::Collection::Id id)
return id == CalendarSupport::KCalPrefs::instance()->defaultCalendarId();
}
static bool hasCompatibleMimeTypes(const Akonadi::Collection &collection)
{
static QStringList goodMimeTypes;
if (goodMimeTypes.isEmpty()) {
goodMimeTypes << QStringLiteral("text/calendar") << KCalendarCore::Event::eventMimeType() << KCalendarCore::Todo::todoMimeType()
<< KCalendarCore::Journal::journalMimeType();
}
for (int i = 0; i < goodMimeTypes.count(); ++i) {
if (collection.contentMimeTypes().contains(goodMimeTypes.at(i))) {
return true;
}
}
return false;
}
/**
* Automatically checks new calendar entries
*/
......@@ -186,168 +180,6 @@ protected:
}
};
/// Despite the name, this handles the presentation of collections including display text and icons, not just colors.
class ColorProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
enum Roles {
isResource = Akonadi::EntityTreeModel::UserRole + 1,
};
Q_ENUM(Roles);
explicit ColorProxyModel(QObject *parent = nullptr)
: QSortFilterProxyModel(parent)
, mInitDefaultCalendar(false)
{
// Needed to read colorattribute of collections for incidence colors
Akonadi::AttributeFactory::registerAttribute<Akonadi::CollectionColorAttribute>();
// Used to get color settings from KOrganizer as fallback
const auto korganizerrc = KSharedConfig::openConfig(QStringLiteral("korganizerrc"));
const auto skel = new KCoreConfigSkeleton(korganizerrc);
mEventViewsPrefs = EventViews::PrefsPtr(new EventViews::Prefs(skel));
mEventViewsPrefs->readConfig();
load();
}
QVariant data(const QModelIndex &index, int role) const override
{
if (!index.isValid()) {
return {};
}
if (role == Qt::DecorationRole) {
const Akonadi::Collection collection = CalendarSupport::collectionFromIndex(index);
if (hasCompatibleMimeTypes(collection)) {
if (collection.hasAttribute<Akonadi::EntityDisplayAttribute>()
&& !collection.attribute<Akonadi::EntityDisplayAttribute>()->iconName().isEmpty()) {
return collection.attribute<Akonadi::EntityDisplayAttribute>()->iconName();
}
}
} else if (role == Qt::FontRole) {
const Akonadi::Collection collection = CalendarSupport::collectionFromIndex(index);
if (!collection.contentMimeTypes().isEmpty() && isStandardCalendar(collection.id()) && collection.rights() & Akonadi::Collection::CanCreateItem) {
auto font = qvariant_cast<QFont>(QSortFilterProxyModel::data(index, Qt::FontRole));
font.setBold(true);
if (!mInitDefaultCalendar) {
mInitDefaultCalendar = true;
CalendarSupport::KCalPrefs::instance()->setDefaultCalendarId(collection.id());
}
return font;
}
} else if (role == Qt::DisplayRole) {
const Akonadi::Collection collection = CalendarSupport::collectionFromIndex(index);
const Akonadi::Collection::Id colId = collection.id();
const Akonadi::AgentInstance instance = Akonadi::AgentManager::self()->instance(collection.resource());
if (!instance.isOnline() && !collection.isVirtual()) {
return i18nc("@item this is the default calendar", "%1 (Offline)", collection.displayName());
}
if (colId == CalendarSupport::KCalPrefs::instance()->defaultCalendarId()) {
return i18nc("@item this is the default calendar", "%1 (Default)", collection.displayName());
}
} else if (role == Qt::BackgroundRole) {
auto color = getCollectionColor(CalendarSupport::collectionFromIndex(index));
// Otherwise QML will get black
if (color.isValid()) {
return color;
} else {
return {};
}
} else if (role == isResource) {
return Akonadi::CollectionUtils::isResource(CalendarSupport::collectionFromIndex(index));
}
return QSortFilterProxyModel::data(index, role);
}
Qt::ItemFlags flags(const QModelIndex &index) const override
{
return Qt::ItemIsSelectable | QSortFilterProxyModel::flags(index);
}
QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> roleNames = QSortFilterProxyModel::roleNames();
roleNames[Qt::CheckStateRole] = "checkState";
roleNames[Qt::BackgroundRole] = "collectionColor";
roleNames[isResource] = "isResource";
return roleNames;
}
QColor getCollectionColor(Akonadi::Collection collection) const
{
const QString id = QString::number(collection.id());
auto supportsMimeType = collection.contentMimeTypes().contains(QLatin1String("application/x-vnd.akonadi.calendar.event"))
|| collection.contentMimeTypes().contains(QLatin1String("application/x-vnd.akonadi.calendar.todo"))
|| collection.contentMimeTypes().contains(QLatin1String("application/x-vnd.akonadi.calendar.journal"));
// qDebug() << "Collection id: " << collection.id();
if (!supportsMimeType) {
return {};
}
if (colorCache.contains(id)) {
return colorCache[id];
}
if (collection.hasAttribute<Akonadi::CollectionColorAttribute>()) {
const auto colorAttr = collection.attribute<Akonadi::CollectionColorAttribute>();
if (colorAttr && colorAttr->color().isValid()) {
colorCache[id] = colorAttr->color();
save();
return colorAttr->color();
}
}
QColor korgColor = mEventViewsPrefs->resourceColorKnown(id);
if (korgColor.isValid()) {
colorCache[id] = korgColor;
save();
return korgColor;
}
QColor color;
color.setRgb(QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256));
colorCache[id] = color;
save();
return color;
}
void load()
{
KSharedConfig::Ptr config = KSharedConfig::openConfig();
KConfigGroup rColorsConfig(config, "Resources Colors");
const QStringList colorKeyList = rColorsConfig.keyList();
for (const QString &key : colorKeyList) {
QColor color = rColorsConfig.readEntry(key, QColor("blue"));
colorCache[key] = color;
}
}
void save() const
{
KSharedConfig::Ptr config = KSharedConfig::openConfig();
KConfigGroup rColorsConfig(config, "Resources Colors");
for (auto it = colorCache.constBegin(); it != colorCache.constEnd(); ++it) {
rColorsConfig.writeEntry(it.key(), it.value(), KConfigBase::Notify | KConfigBase::Normal);
}
config->sync();
}
mutable QHash<QString, QColor> colorCache;
private:
mutable bool mInitDefaultCalendar;
EventViews::PrefsPtr mEventViewsPrefs;
};
CalendarManager::CalendarManager(QObject *parent)
: QObject(parent)
, m_calendar(nullptr)
......@@ -390,6 +222,7 @@ CalendarManager::CalendarManager(QObject *parent)
m_eventMimeTypeFilterModel = new Akonadi::CollectionFilterProxyModel(this);
m_eventMimeTypeFilterModel->setSourceModel(collectionFilter);
m_eventMimeTypeFilterModel->addMimeTypeFilter(QStringLiteral("application/x-vnd.akonadi.calendar.event"));
// text/calendar mimetype includes todo cals
m_todoMimeTypeFilterModel = new Akonadi::CollectionFilterProxyModel(this);
m_todoMimeTypeFilterModel->setSourceModel(collectionFilter);
......
......@@ -5,30 +5,29 @@
#pragma once
#include <QObject>
#include <Akonadi/AgentFilterProxyModel>
#include <Akonadi/CollectionFilterProxyModel>
#include <Akonadi/ETMViewStateSaver>
#include <Akonadi/EntityRightsFilterModel>
#include <QObject>
#include <akonadi-calendar_version.h>
#if AKONADICALENDAR_VERSION > QT_VERSION_CHECK(5, 19, 41)
#include <Akonadi/IncidenceChanger>
#include <Akonadi/ETMCalendar>
#else
#include <Akonadi/Calendar/IncidenceChanger>
#include <Akonadi/Calendar/ETMCalendar>
#endif
#include <CalendarSupport/KCalPrefs>
#include <CalendarSupport/Utils>
#include <KConfigWatcher>
#include <KDescendantsProxyModel>
#include <QObject>
#include <akonadi-calendar_version.h>
#include <incidencewrapper.h>
#include <models/todosortfilterproxymodel.h>
namespace Akonadi
{
class ETMCalendar;
class AgentFilterProxyModel;
class CollectionFilterProxyModel;
class ETMViewStateSaver;
class EntityRightsFilterModel;
class EntityMimeTypeFilterModel;
class IncidenceChanger;
}
class KDescendantsProxyModel;
class KCheckableProxyModel;
class QAbstractProxyModel;
class QAbstractItemModel;
......
// SPDX-FileCopyrightText: 2022 Claudio Cambra <claudio.cambra@gmail.com>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "addresseewrapper.h"
#include "kalendar_debug.h"
#include <KLocalizedString>
#include <QBitArray>
#include <QJSValue>
#include <akonadi/itemmonitor.h>
#include <kcontacts/addressee.h>
AddresseeWrapper::AddresseeWrapper(QObject *parent)
: QObject(parent)
, Akonadi::ItemMonitor()
, m_addressesModel(new AddressModel(this))
{
Akonadi::ItemFetchScope scope;
scope.fetchFullPayload();
scope.fetchAllAttributes();
scope.setFetchRelations(true);
scope.setAncestorRetrieval(Akonadi::ItemFetchScope::Parent);
setFetchScope(scope);
}
AddresseeWrapper::~AddresseeWrapper() = default;
void AddresseeWrapper::notifyDataChanged()
{
Q_EMIT collectionIdChanged();
Q_EMIT nameChanged();
Q_EMIT birthdayChanged();
Q_EMIT photoChanged();
Q_EMIT phoneNumbersChanged();
Q_EMIT preferredEmailChanged();
Q_EMIT uidChanged();
}
Akonadi::Item AddresseeWrapper::addresseeItem() const
{
return item();
}
AddressModel *AddresseeWrapper::addressesModel() const
{
return m_addressesModel;
}
void AddresseeWrapper::setAddresseeItem(const Akonadi::Item &addresseeItem)
{
if (addresseeItem.hasPayload<KContacts::Addressee>()) {
setItem(addresseeItem);
setAddressee(addresseeItem.payload<KContacts::Addressee>());
Q_EMIT addresseeItemChanged();
Q_EMIT collectionIdChanged();
} else {
// Payload not found, try to fetch it
auto job = new Akonadi::ItemFetchJob(addresseeItem);
job->fetchScope().fetchFullPayload();
connect(job, &Akonadi::ItemFetchJob::result, this, [this](KJob *job) {
auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
auto item = fetchJob->items().at(0);
if (item.hasPayload<KContacts::Addressee>()) {
setItem(item);
setAddressee(item.payload<KContacts::Addressee>());
Q_EMIT addresseeItemChanged();
Q_EMIT collectionIdChanged();
} else {
qCWarning(KALENDAR_LOG) << "This is not an addressee item.";
}
});
}
}
void AddresseeWrapper::itemChanged(const Akonadi::Item &item)
{
setAddressee(item.payload<KContacts::Addressee>());
}
void AddresseeWrapper::setAddressee(const KContacts::Addressee &addressee)
{
m_addressee = addressee;
m_addressesModel->setAddresses(addressee.addresses());
notifyDataChanged();
}
QString AddresseeWrapper::uid() const
{
return m_addressee.uid();
}
qint64 AddresseeWrapper::collectionId() const
{
return m_collectionId < 0 ? item().parentCollection().id() : m_collectionId;
}
void AddresseeWrapper::setCollectionId(qint64 collectionId)
{
m_collectionId = collectionId;
Q_EMIT collectionIdChanged();
}
QString AddresseeWrapper::name() const
{
return m_addressee.formattedName();
}
void AddresseeWrapper::setName(const QString &name)
{
if (name == m_addressee.formattedName()) {
return;
}
m_addressee.setFormattedName(name);
Q_EMIT nameChanged();
}
QDateTime AddresseeWrapper::birthday() const
{
return m_addressee.birthday();
}
void AddresseeWrapper::setBirthday(const QDateTime &birthday)
{
if (birthday == m_addressee.birthday()) {
return;
}
m_addressee.setBirthday(birthday);
Q_EMIT birthdayChanged();
}
KContacts::PhoneNumber::List AddresseeWrapper::phoneNumbers() const
{
return m_addressee.phoneNumbers();
}
KContacts::Picture AddresseeWrapper::photo() const
{
return m_addressee.photo();
}
QString AddresseeWrapper::preferredEmail() const
{
return m_addressee.preferredEmail();
}
// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "addressmodel.h"
#include <Akonadi/CollectionIdentificationAttribute>
#include <Akonadi/Item>
#include <Akonadi/ItemFetchJob>
#include <Akonadi/ItemFetchScope>
#include <Akonadi/ItemMonitor>
#include <KContacts/Addressee>
#include <QObject>
#include <qdatetime.h>
/// This class is a QObject wrapper for a KContact::Adressee
class AddresseeWrapper : public QObject, public Akonadi::ItemMonitor
{
Q_OBJECT
Q_PROPERTY(Akonadi::Item addresseeItem READ addresseeItem WRITE setAddresseeItem NOTIFY addresseeItemChanged)
Q_PROPERTY(QString uid READ uid NOTIFY uidChanged)
Q_PROPERTY(qint64 collectionId READ collectionId WRITE setCollectionId NOTIFY collectionIdChanged)
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
Q_PROPERTY(QString preferredEmail READ preferredEmail NOTIFY preferredEmailChanged)
Q_PROPERTY(QDateTime birthday READ birthday WRITE setBirthday NOTIFY birthdayChanged)
Q_PROPERTY(KContacts::PhoneNumber::List phoneNumbers READ phoneNumbers NOTIFY phoneNumbersChanged)
Q_PROPERTY(KContacts::Picture photo READ photo NOTIFY photoChanged)
Q_PROPERTY(AddressModel *addressesModel READ addressesModel CONSTANT)
public:
AddresseeWrapper(QObject *parent = nullptr);
~AddresseeWrapper() override;
Akonadi::Item addresseeItem() const;
void setAddresseeItem(const Akonadi::Item &item);
QString uid() const;
KContacts::PhoneNumber::List phoneNumbers() const;
AddressModel *addressesModel() const;
qint64 collectionId() const;
void setCollectionId(qint64 collectionId);
QString name() const;
QString preferredEmail() const;
QDateTime birthday() const;
void setName(const QString &name);
void setBirthday(const QDateTime &birthday);
KContacts::Picture photo() const;
void setAddressee(const KContacts::Addressee &addressee);
void notifyDataChanged();
Q_SIGNALS:
void addresseeItemChanged();
void collectionIdChanged();
void nameChanged();
void birthdayChanged();
void photoChanged();
void phoneNumbersChanged();
void preferredEmailChanged();
void uidChanged();
private:
void itemChanged(const Akonadi::Item &item) override;
KContacts::Addressee m_addressee;
qint64 m_collectionId = -1; // For when we want to edit, this is temporary
AddressModel *m_addressesModel;
};
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "contacts/addressmodel.h"
#include <QDebug>
#include <qabstractitemmodel.h>
AddressModel::AddressModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int AddressModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_addresses.count();
}
QVariant AddressModel::data(const QModelIndex &idx, int role) const
{
const auto &address = m_addresses[idx.row()];
switch (role) {
case CountryRole:
return address.country();
case ExtendedRole:
return address.extended();
case FormattedAddressRole:
return address.formatted(KContacts::AddressFormatStyle::MultiLineInternational);
case HasGeoRole:
return address.geo().isValid();
case LongitudeRole:
return address.geo().longitude();
case LatitudeRole:
return address.geo().latitude();
case IdRole:
return address.id();
case IsEmptyRole:
return address.isEmpty();
case LabelRole:
return address.label();
case PostalCodeRole:
return address.postalCode();
case PostOfficeBoxRole:
return address.postOfficeBox();
case RegionRole:
return address.region();
case StreetRole: