Verified Commit 57b17db5 authored by Daniel Vrátil's avatar Daniel Vrátil 🤖
Browse files

Port SubscriptionModel away from the deprecated CollectionModel

It now uses ETM with some filters on top internally.
parent 999b5c53
......@@ -63,7 +63,7 @@ class SubscriptionDialogTest: public QObject
model = widget->findChild<SubscriptionModel*>();
QVERIFY(model);
QSignalSpy modelLoadedSpy(model, &SubscriptionModel::loaded);
QSignalSpy modelLoadedSpy(model, &SubscriptionModel::modelLoaded);
buttonBox = widget->findChild<QDialogButtonBox*>();
QVERIFY(buttonBox);
......@@ -166,7 +166,7 @@ class SubscriptionDialogTest: public QObject
while (!idxQueue.empty()) {
const auto idx = idxQueue.front();
idxQueue.pop_front();
if (model->data(idx, CollectionModel::CollectionIdRole).value<qint64>() == col.id()) {
if (model->data(idx, EntityTreeModel::CollectionIdRole).value<qint64>() == col.id()) {
return idx;
}
for (int i = 0; i < model->rowCount(idx); ++i) {
......
......@@ -18,18 +18,65 @@
*/
#include "subscriptionmodel_p.h"
#include "collectionfetchjob.h"
#include "collectionutils.h"
#include "specialcollectionattribute.h"
#include "entityhiddenattribute.h"
#include "entitytreemodel.h"
#include "akonadicore_debug.h"
#include <qnamespace.h>
#include <shared/akranges.h>
#include <QFont>
#include <QSortFilterProxyModel>
using namespace Akonadi;
using namespace AkRanges;
namespace
{
class FilterProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
FilterProxyModel()
: QSortFilterProxyModel()
{
setDynamicSortFilter(true);
}
void setShowHidden(bool showHidden)
{
if (mShowHidden != showHidden) {
mShowHidden = showHidden;
invalidateFilter();
}
}
bool showHidden() const
{
return mShowHidden;
}
protected:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
{
const auto source_index = sourceModel()->index(source_row, 0, source_parent);
const auto col = source_index.data(EntityTreeModel::CollectionRole).value<Collection>();
if (mShowHidden) {
return true;
}
return !col.hasAttribute<EntityHiddenAttribute>();
}
private:
bool mShowHidden = false;
};
}
/**
* @internal
......@@ -37,44 +84,26 @@ using namespace Akonadi;
class SubscriptionModel::Private
{
public:
Private(SubscriptionModel *parent) : q(parent) {}
SubscriptionModel *q;
QHash<Collection::Id, bool> subscriptions;
QSet<Collection::Id> changes;
bool showHiddenCollection = false;
Collection::List changedSubscriptions(bool subscribed)
Private(Monitor *monitor)
: etm(monitor)
{
Collection::List list;
for (Collection::Id id : qAsConst(changes)) {
if (subscriptions.value(id) == subscribed) {
list << Collection(id);
}
}
return list;
etm.setShowSystemEntities(true); // show hidden collections
etm.setItemPopulationStrategy(EntityTreeModel::NoItemPopulation);
etm.setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive);
proxy.setSourceModel(&etm);
}
void listResult(KJob *job)
Collection::List changedSubscriptions(bool subscribed)
{
if (job->error()) {
// TODO
qCWarning(AKONADICORE_LOG) << job->errorString();
return;
}
q->beginResetModel();
const Collection::List cols = static_cast<CollectionFetchJob *>(job)->collections();
for (const Collection &col : cols) {
if (!CollectionUtils::isStructural(col)) {
subscriptions[ col.id() ] = true;
}
}
q->endResetModel();
Q_EMIT q->loaded();
return Views::range(subscriptions.constKeyValueBegin(), subscriptions.constKeyValueEnd())
| Views::filter([subscribed](const auto &val) { return val.second == subscribed; })
| Views::transform([](const auto &val) { return Collection{val.first}; })
| Actions::toQVector;
}
bool isSubscribable(Collection::Id id)
bool isSubscribable(const Collection &col)
{
Collection col = q->collectionForId(id);
if (CollectionUtils::isStructural(col) || col.isVirtual() || CollectionUtils::isUnifiedMailbox(col)) {
return false;
}
......@@ -86,73 +115,66 @@ public:
}
return true;
}
public:
EntityTreeModel etm;
FilterProxyModel proxy;
QHash<Collection::Id, bool> subscriptions;
};
SubscriptionModel::SubscriptionModel(QObject *parent) :
CollectionModel(parent),
d(new Private(this))
SubscriptionModel::SubscriptionModel(Monitor *monitor, QObject *parent) :
QIdentityProxyModel(parent),
d(new Private(monitor))
{
includeUnsubscribed();
CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this);
connect(job,&CollectionFetchJob::result, this, [this](KJob *job) { d->listResult(job); });
QIdentityProxyModel::setSourceModel(&d->proxy);
connect(&d->etm, &EntityTreeModel::collectionTreeFetched, this, &SubscriptionModel::modelLoaded);
}
SubscriptionModel::~SubscriptionModel()
SubscriptionModel::~SubscriptionModel() = default;
void SubscriptionModel::setSourceModel(QAbstractItemModel *)
{
delete d;
// no-op
}
QVariant SubscriptionModel::data(const QModelIndex &index, int role) const
{
switch (role) {
case Qt::CheckStateRole: {
const Collection::Id col = index.data(CollectionIdRole).toLongLong();
const auto col = index.data(EntityTreeModel::CollectionRole).value<Collection>();
if (!d->isSubscribable(col)) {
return QVariant();
}
if (d->subscriptions.value(col)) {
return Qt::Checked;
// Check if we have "override" for the subscription state stored
const auto it = d->subscriptions.constFind(col.id());
if (it != d->subscriptions.cend()) {
return (*it) ? Qt::Checked : Qt::Unchecked;
} else {
// Fallback to the current state of the collection
return col.enabled() ? Qt::Checked : Qt::Unchecked;
}
return Qt::Unchecked;
}
case SubscriptionChangedRole: {
const Collection::Id col = index.data(CollectionIdRole).toLongLong();
if (d->changes.contains(col)) {
return true;
}
return false;
const auto col = index.data(EntityTreeModel::CollectionIdRole).toLongLong();
return d->subscriptions.contains(col);
}
case Qt::FontRole: {
const Collection::Id col = index.data(CollectionIdRole).toLongLong();
QFont font = CollectionModel::data(index, role).value<QFont>();
font.setBold(d->changes.contains(col));
const auto col = index.data(EntityTreeModel::CollectionIdRole).toLongLong();
QFont font = QIdentityProxyModel::data(index, role).value<QFont>();
font.setBold(d->subscriptions.contains(col));
return font;
}
}
if (role == CollectionIdRole) {
return CollectionModel::data(index, CollectionIdRole);
} else {
const Collection::Id collectionId = index.data(CollectionIdRole).toLongLong();
const Collection collection = collectionForId(collectionId);
if (collection.hasAttribute<EntityHiddenAttribute>()) {
if (d->showHiddenCollection) {
return CollectionModel::data(index, role);
} else {
return QVariant();
}
} else {
return CollectionModel::data(index, role);
}
}
return QIdentityProxyModel::data(index, role);
}
Qt::ItemFlags SubscriptionModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags flags = CollectionModel::flags(index);
if (d->isSubscribable(index.data(CollectionIdRole).toLongLong())) {
Qt::ItemFlags flags = QIdentityProxyModel::flags(index);
const auto col = index.data(EntityTreeModel::CollectionRole).value<Collection>();
if (d->isSubscribable(col)) {
return flags | Qt::ItemIsUserCheckable;
}
return flags;
......@@ -161,23 +183,19 @@ Qt::ItemFlags SubscriptionModel::flags(const QModelIndex &index) const
bool SubscriptionModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (role == Qt::CheckStateRole) {
const Collection::Id col = index.data(CollectionIdRole).toLongLong();
const auto col = index.data(EntityTreeModel::CollectionRole).value<Collection>();
if (!d->isSubscribable(col)) {
return true; //No change
}
if (d->subscriptions.contains(col) && d->subscriptions.value(col) == (value == Qt::Checked)) {
return true; // no change
}
d->subscriptions[ col ] = value == Qt::Checked;
if (d->changes.contains(col)) {
d->changes.remove(col);
if (col.enabled() == (value == Qt::Checked)) { // No change compared to the underlying model
d->subscriptions.remove(col.id());
} else {
d->changes.insert(col);
d->subscriptions[col.id()] = (value == Qt::Checked);
}
Q_EMIT dataChanged(index, index);
return true;
}
return CollectionModel::setData(index, value, role);
return QIdentityProxyModel::setData(index, value, role);
}
Akonadi::Collection::List SubscriptionModel::subscribed() const
......@@ -190,9 +208,15 @@ Akonadi::Collection::List SubscriptionModel::unsubscribed() const
return d->changedSubscriptions(false);
}
void SubscriptionModel::showHiddenCollection(bool showHidden)
void SubscriptionModel::setShowHiddenCollections(bool showHidden)
{
d->proxy.setShowHidden(showHidden);
}
bool SubscriptionModel::showHiddenCollections() const
{
d->showHiddenCollection = showHidden;
return d->proxy.showHidden();
}
#include "moc_subscriptionmodel_p.cpp"
#include "subscriptionmodel.moc"
......@@ -21,61 +21,71 @@
#define AKONADI_SUBSCRIPTIONMODEL_P_H
#include "akonadicore_export.h"
#include "collection.h"
#include "collectionmodel.h"
#include <QIdentityProxyModel>
#include "entitytreemodel.h"
namespace Akonadi
{
class Monitor;
/**
* @internal
* @deprecated This should be replaced by something based on EntityTreeModel
*
* An extended collection model used for the subscription dialog.
* A proxy model to be used on top of ETM to display a checkable tree of collections
* for user to select which collections should be locally subscribed.
*
* Used in SubscriptionDialog
*/
class AKONADICORE_EXPORT SubscriptionModel : public CollectionModel
class AKONADICORE_EXPORT SubscriptionModel : public QIdentityProxyModel
{
Q_OBJECT
public:
/** Additional roles. */
enum Roles {
SubscriptionChangedRole = CollectionModel::UserRole + 1 ///< Indicate the subscription status has been changed.
SubscriptionChangedRole = EntityTreeModel::UserRole + 1 ///< Indicate the subscription status has been changed.
};
/**
Create a new subscription model.
@param parent The parent object.
*/
explicit SubscriptionModel(QObject *parent = nullptr);
explicit SubscriptionModel(Monitor *monitor, QObject *parent = nullptr);
/**
Destructor.
*/
~SubscriptionModel() override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
/**
* Sets a source model for the SubscriptionModel.
*
* Should be based on an ETM with only collections.
*/
void setSourceModel(QAbstractItemModel *model) override;
Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Collection::List subscribed() const;
Collection::List unsubscribed() const;
Q_REQUIRED_RESULT Collection::List subscribed() const;
Q_REQUIRED_RESULT Collection::List unsubscribed() const;
/**
* @param showHidden shows hidden collection if set as @c true
* @param showHidden shows hidden collections if set as @c true
* @since: 4.9
*/
void showHiddenCollection(bool showHidden);
void setShowHiddenCollections(bool showHidden);
Q_REQUIRED_RESULT bool showHiddenCollections() const;
Q_SIGNALS:
/**
Emitted when the collection model is fully loaded.
*/
void loaded();
void modelLoaded();
private:
class Private;
Private *const d;
Q_PRIVATE_SLOT(d, void listResult(KJob *))
QScopedPointer<Private> const d;
};
}
......
......@@ -24,6 +24,7 @@
#include "recursivecollectionfilterproxymodel.h"
#include "subscriptionjob_p.h"
#include "subscriptionmodel_p.h"
#include "monitor.h"
#include "akonadiwidgets_debug.h"
#include <KSharedConfig>
......@@ -40,6 +41,8 @@
#include <QLabel>
#include <QTreeView>
#include <QCheckBox>
#include <kmessagebox.h>
#include <qnamespace.h>
using namespace Akonadi;
......@@ -51,43 +54,58 @@ class Q_DECL_HIDDEN SubscriptionDialog::Private
public:
Private(SubscriptionDialog *parent)
: q(parent)
, model(&monitor, parent)
{
ui.setupUi(q);
connect(&model, &SubscriptionModel::modelLoaded, q, [this]() {
filterRecursiveCollectionFilter.sort(0, Qt::AscendingOrder);
ui.collectionView->setEnabled(true);
ui.collectionView->expandAll();
ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
});
filterRecursiveCollectionFilter.setSourceModel(&model);
filterRecursiveCollectionFilter.setFilterCaseSensitivity(Qt::CaseInsensitive);
filterRecursiveCollectionFilter.setSortRole(Qt::DisplayRole);
filterRecursiveCollectionFilter.setSortCaseSensitivity(Qt::CaseSensitive);
filterRecursiveCollectionFilter.setSortLocaleAware(true);
ui.collectionView->setModel(&filterRecursiveCollectionFilter);
ui.searchLineEdit->setFocus();
q->connect(ui.searchLineEdit, &QLineEdit::textChanged,
q, [this](const QString &str) {
filterRecursiveCollectionFilter.setSearchPattern(str);
ui.collectionView->expandAll();
});
q->connect(ui.subscribedOnlyCheckBox, &QCheckBox::toggled,
q, [this](bool state) {
filterRecursiveCollectionFilter.setIncludeCheckedOnly(state);
});
q->connect(ui.subscribeButton, &QPushButton::clicked,
q, [this]() { toggleSubscribed(Qt::Checked); });
q->connect(ui.unsubscribeButton, &QPushButton::clicked,
q, [this]() { toggleSubscribed(Qt::Unchecked); });
auto okButton = ui.buttonBox->button(QDialogButtonBox::Ok);
okButton->setEnabled(false);
connect(okButton, &QPushButton::clicked, q, [this] () { done(); });
}
void done()
{
SubscriptionJob *job = new SubscriptionJob(q);
job->subscribe(model->subscribed());
job->unsubscribe(model->unsubscribed());
connect(job, &SubscriptionJob::result, q, [this](KJob *job) { subscriptionResult(job); });
}
void subscriptionResult(KJob *job)
{
if (job->error()) {
// TODO
qCWarning(AKONADIWIDGETS_LOG) << job->errorString();
}
q->accept();
}
void modelLoaded()
{
filterRecursiveCollectionFilter->sort(0, Qt::AscendingOrder);
ui.collectionView->setEnabled(true);
ui.collectionView->expandAll();
ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
}
void slotSetPattern(const QString &text)
{
filterRecursiveCollectionFilter->setSearchPattern(text);
ui.collectionView->expandAll();
}
void slotSetIncludeCheckedOnly(bool checked)
{
filterRecursiveCollectionFilter->setIncludeCheckedOnly(checked);
job->subscribe(model.subscribed());
job->unsubscribe(model.unsubscribed());
connect(job, &SubscriptionJob::result, q, [this](KJob *job) {
if (job->error()) {
qCWarning(AKONADIWIDGETS_LOG) << job->errorString();
KMessageBox::sorry(q, i18n("Failed to update subscription: %1", job->errorString()),
i18nc("@title", "Subscription Error"));
q->reject();
}
q->accept();
});
}
void writeConfig()
......@@ -105,83 +123,40 @@ public:
}
}
void slotUnSubscribe();
void slotSubscribe();
void toggleSubscribed(Qt::CheckState state)
{
const QModelIndexList list = ui.collectionView->selectionModel()->selectedIndexes();
for (const QModelIndex &index : list) {
model.setData(index, state, Qt::CheckStateRole);
}
ui.collectionView->setFocus();
}
SubscriptionDialog *q = nullptr;
SubscriptionDialog * const q;
Ui::SubscriptionDialog ui;
SubscriptionModel *model = nullptr;
RecursiveCollectionFilterProxyModel *filterRecursiveCollectionFilter = nullptr;
};
Monitor monitor;
SubscriptionModel model;
RecursiveCollectionFilterProxyModel filterRecursiveCollectionFilter;
void SubscriptionDialog::Private::slotSubscribe()
{
const QModelIndexList list = ui.collectionView->selectionModel()->selectedIndexes();
for (const QModelIndex &index : list) {
model->setData(index, Qt::Checked, Qt::CheckStateRole);
}
ui.collectionView->setFocus();
}
};
void SubscriptionDialog::Private::slotUnSubscribe()
{
const QModelIndexList list = ui.collectionView->selectionModel()->selectedIndexes();
for (const QModelIndex &index : list) {
model->setData(index, Qt::Unchecked, Qt::CheckStateRole);
}
ui.collectionView->setFocus();
}
SubscriptionDialog::SubscriptionDialog(QWidget *parent)
: QDialog(parent)
, d(new Private(this))
: SubscriptionDialog({}, parent)
{
init(QStringList());
}
SubscriptionDialog::SubscriptionDialog(const QStringList &mimetypes, QWidget *parent)
: QDialog(parent)
, d(new Private(this))
{
init(mimetypes);
}
void SubscriptionDialog::showHiddenCollection(bool showHidden)
{
d->model->showHiddenCollection(showHidden);
}
void SubscriptionDialog::init(const QStringList &mimetypes)
{
setAttribute(Qt::WA_DeleteOnClose);
d->ui.setupUi(this);
d->model = new SubscriptionModel(this);
d->filterRecursiveCollectionFilter = new RecursiveCollectionFilterProxyModel(this);
d->filterRecursiveCollectionFilter->setSourceModel(d->model);
d->filterRecursiveCollectionFilter->setFilterCaseSensitivity(Qt::CaseInsensitive);
d->filterRecursiveCollectionFilter->setSortRole(Qt::DisplayRole);
d->filterRecursiveCollectionFilter->setSortCaseSensitivity(Qt::CaseSensitive);
d->filterRecursiveCollectionFilter->setSortLocaleAware(true);
if (!mimetypes.isEmpty()) {
d->filterRecursiveCollectionFilter->addContentMimeTypeInclusionFilters(mimetypes);
d->filterRecursiveCollectionFilter.addContentMimeTypeInclusionFilters(mimetypes);
}
d->ui.collectionView->setModel(d->filterRecursiveCollectionFilter);
d->ui.searchLineEdit->setFocus();
connect(d->ui.searchLineEdit, &QLineEdit::textChanged, this, [this](const QString &str) { d->slotSetPattern(str); });
connect(d->ui.subscribedOnlyCheckBox, &QCheckBox::toggled, this, [this](bool state) { d->slotSetIncludeCheckedOnly(state); });
connect(d->ui.subscribeButton, &QPushButton::clicked, this, [this]() { d->slotSubscribe(); });
connect(d->ui.unsubscribeButton, &QPushButton::clicked, this, [this]() { d->slotUnSubscribe(); });
auto okButton = d->ui.buttonBox->button(QDialogButtonBox::Ok);
okButton->setEnabled(false);
connect(d->model, &SubscriptionModel::loaded, this, [this]() { d->modelLoaded(); });
connect(okButton,<