collectionmodel.cpp 55.1 KB
Newer Older
1 2 3
/*
 *  collectionmodel.cpp  -  Akonadi collection models
 *  Program:  kalarm
4
 *  Copyright © 2007-2014 by David Jarvie <djarvie@kde.org>
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *
 *  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) any later version.
 *
 *  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 "collectionmodel.h"
#include "autoqpointer.h"
23
#include "messagebox.h"
24 25
#include "preferences.h"

Laurent Montel's avatar
Laurent Montel committed
26 27
#include <kalarmcal/collectionattribute.h>
#include <kalarmcal/compatibilityattribute.h>
28

Laurent Montel's avatar
Laurent Montel committed
29
#include <AkonadiCore/agentmanager.h>
Laurent Montel's avatar
Laurent Montel committed
30 31
#include <AkonadiCore/collectiondeletejob.h>
#include <AkonadiCore/collectionmodifyjob.h>
Laurent Montel's avatar
Laurent Montel committed
32
#include <AkonadiCore/entitymimetypefiltermodel.h>
David Jarvie's avatar
Tidy up  
David Jarvie committed
33
#include <AkonadiWidgets/collectiondialog.h>
34

35
#include <KLocalizedString>
David Jarvie's avatar
David Jarvie committed
36
#include <KSharedConfig>
37

Daniel Vrátil's avatar
Daniel Vrátil committed
38
#include <QUrl>
39 40 41 42
#include <QApplication>
#include <QMouseEvent>
#include <QHelpEvent>
#include <QToolTip>
43
#include <QTimer>
44
#include <QObject>
Laurent Montel's avatar
Laurent Montel committed
45
#include "kalarm_debug.h"
46 47

using namespace Akonadi;
48
using namespace KAlarmCal;
49 50 51 52 53 54

static Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem;


/*=============================================================================
= Class: CollectionMimeTypeFilterModel
55
= Proxy model to filter AkonadiModel to restrict its contents to Collections,
David Jarvie's avatar
David Jarvie committed
56
= not Items, containing specified KAlarm content mime types.
57
= It can optionally be restricted to writable and/or enabled Collections.
58 59 60 61 62
=============================================================================*/
class CollectionMimeTypeFilterModel : public Akonadi::EntityMimeTypeFilterModel
{
        Q_OBJECT
    public:
Laurent Montel's avatar
Laurent Montel committed
63
        explicit CollectionMimeTypeFilterModel(QObject* parent = nullptr);
64
        void setEventTypeFilter(CalEvent::Type);
65 66 67 68
        void setFilterWritable(bool writable);
        void setFilterEnabled(bool enabled);
        Akonadi::Collection collection(int row) const;
        Akonadi::Collection collection(const QModelIndex&) const;
69
        QModelIndex collectionIndex(const Akonadi::Collection&) const;
70 71

    protected:
72
        bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
73 74

    private:
75
        CalEvent::Type mAlarmType;  // collection content type contained in this model
76 77 78 79 80 81
        bool    mWritableOnly; // only include writable collections in this model
        bool    mEnabledOnly;  // only include enabled collections in this model
};

CollectionMimeTypeFilterModel::CollectionMimeTypeFilterModel(QObject* parent)
    : EntityMimeTypeFilterModel(parent),
82
      mAlarmType(CalEvent::EMPTY),
David Jarvie's avatar
David Jarvie committed
83 84
      mWritableOnly(false),
      mEnabledOnly(false)
85
{
David Jarvie's avatar
David Jarvie committed
86
    addMimeTypeInclusionFilter(Collection::mimeType());   // select collections, not items
87 88 89 90
    setHeaderGroup(EntityTreeModel::CollectionTreeHeaders);
    setSourceModel(AkonadiModel::instance());
}

91
void CollectionMimeTypeFilterModel::setEventTypeFilter(CalEvent::Type type)
92
{
93
    if (type != mAlarmType)
94
    {
95
        mAlarmType = type;
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
        invalidateFilter();
    }
}

void CollectionMimeTypeFilterModel::setFilterWritable(bool writable)
{
    if (writable != mWritableOnly)
    {
        mWritableOnly = writable;
        invalidateFilter();
    }
}

void CollectionMimeTypeFilterModel::setFilterEnabled(bool enabled)
{
    if (enabled != mEnabledOnly)
    {
Laurent Montel's avatar
Laurent Montel committed
113
        Q_EMIT layoutAboutToBeChanged();
114 115
        mEnabledOnly = enabled;
        invalidateFilter();
Laurent Montel's avatar
Laurent Montel committed
116
        Q_EMIT layoutChanged();
117 118 119 120 121 122 123 124
    }
}

bool CollectionMimeTypeFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
    if (!EntityMimeTypeFilterModel::filterAcceptsRow(sourceRow, sourceParent))
        return false;
    AkonadiModel* model = AkonadiModel::instance();
David Jarvie's avatar
David Jarvie committed
125 126
    const QModelIndex ix = model->index(sourceRow, 0, sourceParent);
    const Collection collection = model->data(ix, AkonadiModel::CollectionRole).value<Collection>();
127 128
    if (collection.remoteId().isEmpty())
        return false;   // invalidly configured resource
129 130 131 132
    if (!AgentManager::self()->instance(collection.resource()).isValid())
        return false;
    if (!mWritableOnly  &&  mAlarmType == CalEvent::EMPTY)
        return true;
133
    if (mWritableOnly  &&  (collection.rights() & writableRights) != writableRights)
134
        return false;
135
    if (mAlarmType != CalEvent::EMPTY  &&  !collection.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType)))
136
        return false;
137 138
    if ((mWritableOnly || mEnabledOnly)  &&  !collection.hasAttribute<CollectionAttribute>())
        return false;
139 140
    if (mWritableOnly  &&  (!collection.hasAttribute<CompatibilityAttribute>()
                         || collection.attribute<CompatibilityAttribute>()->compatibility() != KACalendar::Current))
141 142 143
        return false;
    if (mEnabledOnly  &&  !collection.attribute<CollectionAttribute>()->isEnabled(mAlarmType))
        return false;
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
    return true;
}

/******************************************************************************
* Return the collection for a given row.
*/
Collection CollectionMimeTypeFilterModel::collection(int row) const
{
    return static_cast<AkonadiModel*>(sourceModel())->data(mapToSource(index(row, 0)), EntityTreeModel::CollectionRole).value<Collection>();
}

Collection CollectionMimeTypeFilterModel::collection(const QModelIndex& index) const
{
    return static_cast<AkonadiModel*>(sourceModel())->data(mapToSource(index), EntityTreeModel::CollectionRole).value<Collection>();
}

160 161 162 163 164
QModelIndex CollectionMimeTypeFilterModel::collectionIndex(const Collection& collection) const
{
    return mapFromSource(static_cast<AkonadiModel*>(sourceModel())->collectionIndex(collection));
}

165 166 167

/*=============================================================================
= Class: CollectionListModel
168
= Proxy model converting the AkonadiModel collection tree into a flat list.
169
= The model may be restricted to specified content mime types.
170
= It can optionally be restricted to writable and/or enabled Collections.
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
=============================================================================*/

CollectionListModel::CollectionListModel(QObject* parent)
    : KDescendantsProxyModel(parent),
      mUseCollectionColour(true)
{
    setSourceModel(new CollectionMimeTypeFilterModel(this));
    setDisplayAncestorData(false);
}

/******************************************************************************
* Return the collection for a given row.
*/
Collection CollectionListModel::collection(int row) const
{
    return data(index(row, 0), EntityTreeModel::CollectionRole).value<Collection>();
}

Collection CollectionListModel::collection(const QModelIndex& index) const
{
    return data(index, EntityTreeModel::CollectionRole).value<Collection>();
}

194 195 196 197 198
QModelIndex CollectionListModel::collectionIndex(const Collection& collection) const
{
    return mapFromSource(static_cast<CollectionMimeTypeFilterModel*>(sourceModel())->collectionIndex(collection));
}

199
void CollectionListModel::setEventTypeFilter(CalEvent::Type type)
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
{
    static_cast<CollectionMimeTypeFilterModel*>(sourceModel())->setEventTypeFilter(type);
}

void CollectionListModel::setFilterWritable(bool writable)
{
    static_cast<CollectionMimeTypeFilterModel*>(sourceModel())->setFilterWritable(writable);
}

void CollectionListModel::setFilterEnabled(bool enabled)
{
    static_cast<CollectionMimeTypeFilterModel*>(sourceModel())->setFilterEnabled(enabled);
}

bool CollectionListModel::isDescendantOf(const QModelIndex& ancestor, const QModelIndex& descendant) const
{
    Q_UNUSED(descendant);
    return !ancestor.isValid();
}

/******************************************************************************
* Return the data for a given role, for a specified item.
*/
QVariant CollectionListModel::data(const QModelIndex& index, int role) const
{
    switch (role)
    {
        case Qt::BackgroundRole:
            if (!mUseCollectionColour)
                role = AkonadiModel::BaseColourRole;
            break;
        default:
            break;
    }
    return KDescendantsProxyModel::data(index, role);
}


/*=============================================================================
= Class: CollectionCheckListModel
240 241 242 243
= Proxy model providing a checkable list of all Collections. A Collection's
= checked status is equivalent to whether it is selected or not.
= An alarm type is specified, whereby Collections which are enabled for that
= alarm type are checked; Collections which do not contain that alarm type, or
244
= which are disabled for that alarm type, are unchecked.
245 246
=============================================================================*/

Laurent Montel's avatar
Laurent Montel committed
247
CollectionListModel* CollectionCheckListModel::mModel = nullptr;
248
int                  CollectionCheckListModel::mInstanceCount = 0;
249

250
CollectionCheckListModel::CollectionCheckListModel(CalEvent::Type type, QObject* parent)
251
    : KCheckableProxyModel(parent),
252
      mAlarmType(type)
253
{
254
    ++mInstanceCount;
255
    if (!mModel)
Laurent Montel's avatar
Laurent Montel committed
256
        mModel = new CollectionListModel(nullptr);
257
    setSourceModel(mModel);    // the source model is NOT filtered by alarm type
258
    mSelectionModel = new QItemSelectionModel(mModel);
259
    setSelectionModel(mSelectionModel);
260 261
    connect(mSelectionModel, &QItemSelectionModel::selectionChanged,
                             this, &CollectionCheckListModel::selectionChanged);
Laurent Montel's avatar
Laurent Montel committed
262
    connect(mModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), SIGNAL(layoutAboutToBeChanged()));
263
    connect(mModel, &QAbstractItemModel::rowsInserted, this, &CollectionCheckListModel::slotRowsInserted);
264 265
    // This is probably needed to make CollectionFilterCheckListModel update
    // (similarly to when rows are inserted).
Laurent Montel's avatar
Laurent Montel committed
266 267
    connect(mModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), SIGNAL(layoutAboutToBeChanged()));
    connect(mModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), SIGNAL(layoutChanged()));
268

269 270
    connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged,
                                      this, &CollectionCheckListModel::collectionStatusChanged);
271 272 273 274 275 276

    // Initialise checked status for all collections.
    // Note that this is only necessary if the model is recreated after
    // being deleted.
    for (int row = 0, count = mModel->rowCount();  row < count;  ++row)
        setSelectionStatus(mModel->collection(row), mModel->index(row, 0));
277 278
}

279 280 281 282 283
CollectionCheckListModel::~CollectionCheckListModel()
{
    if (--mInstanceCount <= 0)
    {
        delete mModel;
Laurent Montel's avatar
Laurent Montel committed
284
        mModel = nullptr;
285 286 287
    }
}

288

289 290 291 292 293
/******************************************************************************
* Return the collection for a given row.
*/
Collection CollectionCheckListModel::collection(int row) const
{
294
    return mModel->collection(mapToSource(index(row, 0)));
295 296 297 298
}

Collection CollectionCheckListModel::collection(const QModelIndex& index) const
{
299
    return mModel->collection(mapToSource(index));
300 301
}

302 303 304 305 306 307 308 309 310 311 312
/******************************************************************************
* Return model data for one index.
*/
QVariant CollectionCheckListModel::data(const QModelIndex& index, int role) const
{
    const Collection collection = mModel->collection(index);
    if (collection.isValid())
    {
        // This is a Collection row
        switch (role)
        {
313 314
            case Qt::ForegroundRole:
            {
David Jarvie's avatar
David Jarvie committed
315
                const QString mimeType = CalEvent::mimeType(mAlarmType);
316 317 318 319
                if (collection.contentMimeTypes().contains(mimeType))
                    return AkonadiModel::foregroundColor(collection, QStringList(mimeType));
                break;
            }
320 321
            case Qt::FontRole:
            {
322 323
                if (!collection.hasAttribute<CollectionAttribute>()
                ||  !AkonadiModel::isCompatible(collection))
324
                    break;
David Jarvie's avatar
David Jarvie committed
325
                const CollectionAttribute* attr = collection.attribute<CollectionAttribute>();
326 327
                if (!attr->enabled())
                    break;
David Jarvie's avatar
David Jarvie committed
328
                const QStringList mimeTypes = collection.contentMimeTypes();
329
                if (attr->isStandard(mAlarmType)  &&  mimeTypes.contains(CalEvent::mimeType(mAlarmType)))
330 331
                {
                    // It's the standard collection for a mime type
332
                    QFont font = qvariant_cast<QFont>(KCheckableProxyModel::data(index, role));
333 334 335 336 337 338 339 340 341
                    font.setBold(true);
                    return font;
                }
                break;
            }
            default:
                break;
        }
    }
342
    return KCheckableProxyModel::data(index, role);
343 344
}

345 346 347 348 349 350 351 352 353 354
/******************************************************************************
* Set model data for one index.
* If the change is to disable a collection, check for eligibility and prevent
* the change if necessary.
*/
bool CollectionCheckListModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
    if (role == Qt::CheckStateRole  &&  static_cast<Qt::CheckState>(value.toInt()) != Qt::Checked)
    {
        // A collection is to be disabled.
355
        const Collection collection = mModel->collection(index);
356 357 358
        if (collection.isValid()  &&  collection.hasAttribute<CollectionAttribute>())
        {
            const CollectionAttribute* attr = collection.attribute<CollectionAttribute>();
359
            if (attr->isEnabled(mAlarmType))
360 361 362
            {
                QString errmsg;
                QWidget* messageParent = qobject_cast<QWidget*>(QObject::parent());
363
                if (attr->isStandard(mAlarmType)
364
                &&  AkonadiModel::isCompatible(collection))
365 366
                {
                    // It's the standard collection for some alarm type.
367
                    if (mAlarmType == CalEvent::ACTIVE)
368 369 370
                    {
                        errmsg = i18nc("@info", "You cannot disable your default active alarm calendar.");
                    }
371
                    else if (mAlarmType == CalEvent::ARCHIVED  &&  Preferences::archivedKeepDays())
372 373 374 375 376 377
                    {
                        // Only allow the archived alarms standard collection to be disabled if
                        // we're not saving expired alarms.
                        errmsg = i18nc("@info", "You cannot disable your default archived alarm calendar "
                                                "while expired alarms are configured to be kept.");
                    }
378
                    else if (KAMessageBox::warningContinueCancel(messageParent,
379 380 381 382 383 384
                                                           i18nc("@info", "Do you really want to disable your default calendar?"))
                               == KMessageBox::Cancel)
                        return false;
                }
                if (!errmsg.isEmpty())
                {
385
                    KAMessageBox::sorry(messageParent, errmsg);
386 387 388 389 390
                    return false;
                }
            }
        }
    }
391
    return KCheckableProxyModel::setData(index, value, role);
392 393 394 395 396 397 398 399
}

/******************************************************************************
* Called when rows have been inserted into the model.
* Select or deselect them according to their enabled status.
*/
void CollectionCheckListModel::slotRowsInserted(const QModelIndex& parent, int start, int end)
{
David Jarvie's avatar
David Jarvie committed
400
    Q_EMIT layoutAboutToBeChanged();
401 402 403
    for (int row = start;  row <= end;  ++row)
    {
        const QModelIndex ix = mapToSource(index(row, 0, parent));
404
        const Collection collection = mModel->collection(ix);
405
        if (collection.isValid())
406
            setSelectionStatus(collection, ix);
407
    }
Laurent Montel's avatar
Laurent Montel committed
408
    Q_EMIT layoutChanged();   // this is needed to make CollectionFilterCheckListModel update
409 410 411
}

/******************************************************************************
412 413
* Called when the user has ticked/unticked a collection to enable/disable it
* (or when the selection changes for any other reason).
414 415 416 417 418
*/
void CollectionCheckListModel::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected)
{
    const QModelIndexList sel = selected.indexes();
    foreach (const QModelIndex& ix, sel)
419 420 421 422 423
    {
        // Try to enable the collection, but untick it if not possible
        if (!CollectionControlModel::setEnabled(mModel->collection(ix), mAlarmType, true))
            mSelectionModel->select(ix, QItemSelectionModel::Deselect);
    }
424 425
    const QModelIndexList desel = deselected.indexes();
    foreach (const QModelIndex& ix, desel)
426
        CollectionControlModel::setEnabled(mModel->collection(ix), mAlarmType, false);
427 428
}

429 430 431 432 433
/******************************************************************************
* Called when a collection parameter or status has changed.
* If the collection's alarm types have been reconfigured, ensure that the
* model views are updated to reflect this.
*/
434
void CollectionCheckListModel::collectionStatusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant&, bool inserted)
435
{
436
    if (inserted  ||  !collection.isValid())
437
        return;
438
    switch (change)
439
    {
440
        case AkonadiModel::Enabled:
Laurent Montel's avatar
Laurent Montel committed
441
            qCDebug(KALARM_LOG) << "Enabled" << collection.id();
442 443
            break;
        case AkonadiModel::AlarmTypes:
Laurent Montel's avatar
Laurent Montel committed
444
            qCDebug(KALARM_LOG) << "AlarmTypes" << collection.id();
445 446 447
            break;
        default:
            return;
448
    }
David Jarvie's avatar
David Jarvie committed
449
    const QModelIndex ix = mModel->collectionIndex(collection);
450 451 452
    if (ix.isValid())
        setSelectionStatus(collection, ix);
    if (change == AkonadiModel::AlarmTypes)
Laurent Montel's avatar
Laurent Montel committed
453
        Q_EMIT collectionTypeChange(this);
454 455 456 457 458
}

/******************************************************************************
* Select or deselect an index according to its enabled status.
*/
459
void CollectionCheckListModel::setSelectionStatus(const Collection& collection, const QModelIndex& sourceIndex)
460
{
David Jarvie's avatar
David Jarvie committed
461 462 463
    const QItemSelectionModel::SelectionFlags sel = (collection.hasAttribute<CollectionAttribute>()
                                                     &&  collection.attribute<CollectionAttribute>()->isEnabled(mAlarmType))
                                                  ? QItemSelectionModel::Select : QItemSelectionModel::Deselect;
464
    mSelectionModel->select(sourceIndex, sel);
465 466
}

467 468 469

/*=============================================================================
= Class: CollectionFilterCheckListModel
470 471 472
= Proxy model providing a checkable collection list. The model contains all
= alarm types, but returns only one type at any given time. The selected alarm
= type may be changed as desired.
473 474 475
=============================================================================*/
CollectionFilterCheckListModel::CollectionFilterCheckListModel(QObject* parent)
    : QSortFilterProxyModel(parent),
476 477 478 479
      mActiveModel(new CollectionCheckListModel(CalEvent::ACTIVE, this)),
      mArchivedModel(new CollectionCheckListModel(CalEvent::ARCHIVED, this)),
      mTemplateModel(new CollectionCheckListModel(CalEvent::TEMPLATE, this)),
      mAlarmType(CalEvent::EMPTY)
480
{
481
    setDynamicSortFilter(true);
482 483 484
    connect(mActiveModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged);
    connect(mArchivedModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged);
    connect(mTemplateModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged);
485 486
}

487
void CollectionFilterCheckListModel::setEventTypeFilter(CalEvent::Type type)
488
{
489
    if (type != mAlarmType)
490
    {
491 492 493
        CollectionCheckListModel* newModel;
        switch (type)
        {
494 495 496
            case CalEvent::ACTIVE:    newModel = mActiveModel;  break;
            case CalEvent::ARCHIVED:  newModel = mArchivedModel;  break;
            case CalEvent::TEMPLATE:  newModel = mTemplateModel;  break;
497 498 499 500 501
            default:
                return;
        }
        mAlarmType = type;
        setSourceModel(newModel);
502
        invalidate();
503 504 505 506 507 508 509 510
    }
}

/******************************************************************************
* Return the collection for a given row.
*/
Collection CollectionFilterCheckListModel::collection(int row) const
{
511
    return static_cast<CollectionCheckListModel*>(sourceModel())->collection(mapToSource(index(row, 0)));
512 513 514 515
}

Collection CollectionFilterCheckListModel::collection(const QModelIndex& index) const
{
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533
    return static_cast<CollectionCheckListModel*>(sourceModel())->collection(mapToSource(index));
}

QVariant CollectionFilterCheckListModel::data(const QModelIndex& index, int role) const
{
    switch (role)
    {
        case Qt::ToolTipRole:
        {
            const Collection col = collection(index);
            if (col.isValid())
                return AkonadiModel::instance()->tooltip(col, mAlarmType);
            break;
        }
        default:
            break;
    }
    return QSortFilterProxyModel::data(index, role);
534 535 536 537
}

bool CollectionFilterCheckListModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
538
    if (mAlarmType == CalEvent::EMPTY)
539
        return true;
David Jarvie's avatar
David Jarvie committed
540
    const CollectionCheckListModel* model = static_cast<CollectionCheckListModel*>(sourceModel());
541
    const Collection collection = model->collection(model->index(sourceRow, 0, sourceParent));
542
    return collection.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType));
543 544
}

545 546 547 548 549 550 551 552 553 554
/******************************************************************************
* Called when a collection alarm type has changed.
* Ensure that the collection is removed from or added to the current model view.
*/
void CollectionFilterCheckListModel::collectionTypeChanged(CollectionCheckListModel* model)
{
    if (model == sourceModel())
        invalidateFilter();
}

555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601

/*=============================================================================
= Class: CollectionView
= View displaying a list of collections.
=============================================================================*/
CollectionView::CollectionView(CollectionFilterCheckListModel* model, QWidget* parent)
    : QListView(parent)
{
    setModel(model);
}

void CollectionView::setModel(QAbstractItemModel* model)
{
    QListView::setModel(model);
}

/******************************************************************************
* Return the collection for a given row.
*/
Collection CollectionView::collection(int row) const
{
    return static_cast<CollectionFilterCheckListModel*>(model())->collection(row);
}

Collection CollectionView::collection(const QModelIndex& index) const
{
    return static_cast<CollectionFilterCheckListModel*>(model())->collection(index);
}

/******************************************************************************
* Called when a mouse button is released.
* Any currently selected collection is deselected.
*/
void CollectionView::mouseReleaseEvent(QMouseEvent* e)
{
    if (!indexAt(e->pos()).isValid())
        clearSelection();
    QListView::mouseReleaseEvent(e);
}

/******************************************************************************
* Called when a ToolTip or WhatsThis event occurs.
*/
bool CollectionView::viewportEvent(QEvent* e)
{
    if (e->type() == QEvent::ToolTip  &&  isActiveWindow())
    {
David Jarvie's avatar
David Jarvie committed
602 603
        const QHelpEvent* he = static_cast<QHelpEvent*>(e);
        const QModelIndex index = indexAt(he->pos());
604
        QVariant value = static_cast<CollectionFilterCheckListModel*>(model())->data(index, Qt::ToolTipRole);
605 606 607
        if (qVariantCanConvert<QString>(value))
        {
            QString toolTip = value.toString();
608
            int i = toolTip.indexOf(QLatin1Char('@'));
609 610
            if (i > 0)
            {
611
                int j = toolTip.indexOf(QRegExp(QLatin1String("<(nl|br)"), Qt::CaseInsensitive), i + 1);
612
                int k = toolTip.indexOf(QLatin1Char('@'), j);
David Jarvie's avatar
David Jarvie committed
613
                const QString name = toolTip.mid(i + 1, j - i - 1);
614
                value = model()->data(index, Qt::FontRole);
David Jarvie's avatar
David Jarvie committed
615
                const QFontMetrics fm(qvariant_cast<QFont>(value).resolve(viewOptions().font));
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655
                int textWidth = fm.boundingRect(name).width() + 1;
                const int margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
                QStyleOptionButton opt;
                opt.QStyleOption::operator=(viewOptions());
                opt.rect = rectForIndex(index);
                int checkWidth = QApplication::style()->subElementRect(QStyle::SE_ViewItemCheckIndicator, &opt).width();
                int left = spacing() + 3*margin + checkWidth + viewOptions().decorationSize.width();   // left offset of text
                int right = left + textWidth;
                if (left >= horizontalOffset() + spacing()
                &&  right <= horizontalOffset() + width() - spacing() - 2*frameWidth())
                {
                    // The whole of the collection name is already displayed,
                    // so omit it from the tooltip.
                    if (k > 0)
                        toolTip.remove(i, k + 1 - i);
                }
                else
                {
                    toolTip.remove(k, 1);
                    toolTip.remove(i, 1);
                }
            }
            QToolTip::showText(he->globalPos(), toolTip, this);
            return true;
        }
    }
    return QListView::viewportEvent(e);
}


/*=============================================================================
= Class: CollectionControlModel
= Proxy model to select which Collections will be enabled. Disabled Collections
= are not populated or monitored; their contents are ignored. The set of
= enabled Collections is stored in the config file's "Collections" group.
= Note that this model is not used directly for displaying - its purpose is to
= allow collections to be disabled, which will remove them from the other
= collection models.
=============================================================================*/

Laurent Montel's avatar
Laurent Montel committed
656
CollectionControlModel* CollectionControlModel::mInstance = nullptr;
657 658 659 660 661 662 663 664 665 666
bool                    CollectionControlModel::mAskDestination = false;

CollectionControlModel* CollectionControlModel::instance()
{
    if (!mInstance)
        mInstance = new CollectionControlModel(qApp);
    return mInstance;
}

CollectionControlModel::CollectionControlModel(QObject* parent)
667
    : FavoriteCollectionsModel(AkonadiModel::instance(), KConfigGroup(KSharedConfig::openConfig(), "Collections"), parent),
Laurent Montel's avatar
Laurent Montel committed
668
      mPopulatedCheckLoop(nullptr)
669 670 671 672 673 674 675 676 677
{
    // Initialise the list of enabled collections
    EntityMimeTypeFilterModel* filter = new EntityMimeTypeFilterModel(this);
    filter->addMimeTypeInclusionFilter(Collection::mimeType());
    filter->setSourceModel(AkonadiModel::instance());
    Collection::List collections;
    findEnabledCollections(filter, QModelIndex(), collections);
    setCollections(collections);

678 679 680 681 682 683
    connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged,
                                      this, &CollectionControlModel::statusChanged);
    connect(AkonadiModel::instance(), &EntityTreeModel::collectionTreeFetched,
                                      this, &CollectionControlModel::collectionPopulated);
    connect(AkonadiModel::instance(), &EntityTreeModel::collectionPopulated,
                                      this, &CollectionControlModel::collectionPopulated);
684
    connect(AkonadiModel::instance(), SIGNAL(serverStopped()), SLOT(reset()));
685 686 687
}

/******************************************************************************
688 689
* Recursive function to check all collections' enabled status, and to compile a
* list of all collections which have any alarm types enabled.
690 691
* Collections which duplicate the same backend storage are filtered out, to
* avoid crashes due to duplicate events in different resources.
692 693 694 695 696 697 698 699
*/
void CollectionControlModel::findEnabledCollections(const EntityMimeTypeFilterModel* filter, const QModelIndex& parent, Collection::List& collections) const
{
    AkonadiModel* model = AkonadiModel::instance();
    for (int row = 0, count = filter->rowCount(parent);  row < count;  ++row)
    {
        const QModelIndex ix = filter->index(row, 0, parent);
        const Collection collection = model->data(filter->mapToSource(ix), AkonadiModel::CollectionRole).value<Collection>();
700 701
        if (!AgentManager::self()->instance(collection.resource()).isValid())
            continue;    // the collection doesn't belong to a resource, so omit it
David Jarvie's avatar
David Jarvie committed
702
        const CalEvent::Types enabled = !collection.hasAttribute<CollectionAttribute>() ? CalEvent::EMPTY
703
                                           : collection.attribute<CollectionAttribute>()->enabled();
David Jarvie's avatar
David Jarvie committed
704
        const CalEvent::Types canEnable = checkTypesToEnable(collection, collections, enabled);
705 706 707 708 709 710 711 712
        if (canEnable != enabled)
        {
            // There is another collection which uses the same backend
            // storage. Disable alarm types enabled in the other collection.
            if (!model->isCollectionBeingDeleted(collection.id()))
                model->setData(model->collectionIndex(collection), static_cast<int>(canEnable), AkonadiModel::EnabledTypesRole);
        }
        if (canEnable)
713 714 715 716 717 718
            collections += collection;
        if (filter->rowCount(ix) > 0)
            findEnabledCollections(filter, ix, collections);
    }
}

719
bool CollectionControlModel::isEnabled(const Collection& collection, CalEvent::Type type)
720
{
721 722
    if (!collection.isValid()  ||  !instance()->collections().contains(collection))
        return false;
723 724 725 726 727 728 729
    if (!AgentManager::self()->instance(collection.resource()).isValid())
    {
        // The collection doesn't belong to a resource, so it can't be used.
        // Remove it from the list of collections.
        instance()->removeCollection(collection);
        return false;
    }
730 731 732 733
    Collection col = collection;
    AkonadiModel::instance()->refresh(col);    // update with latest data
    return col.hasAttribute<CollectionAttribute>()
       &&  col.attribute<CollectionAttribute>()->isEnabled(type);
734 735
}

736 737 738 739 740
/******************************************************************************
* Enable or disable the specified alarm types for a collection.
* Reply = alarm types which can be enabled
*/
CalEvent::Types CollectionControlModel::setEnabled(const Collection& collection, CalEvent::Types types, bool enabled)
741
{
Laurent Montel's avatar
Laurent Montel committed
742
    qCDebug(KALARM_LOG) << "id:" << collection.id() << ", alarm types" << types << "->" << enabled;
743
    if (!collection.isValid()  ||  (!enabled && !instance()->collections().contains(collection)))
744
        return CalEvent::EMPTY;
745 746
    Collection col = collection;
    AkonadiModel::instance()->refresh(col);    // update with latest data
747
    CalEvent::Types alarmTypes = !col.hasAttribute<CollectionAttribute>() ? CalEvent::EMPTY
748 749
                                         : col.attribute<CollectionAttribute>()->enabled();
    if (enabled)
750
        alarmTypes |= static_cast<CalEvent::Types>(types & (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE));
751
    else
752
        alarmTypes &= ~types;
753 754 755 756 757 758 759 760 761 762 763

    return instance()->setEnabledStatus(collection, alarmTypes, false);
}

/******************************************************************************
* Change the collection's enabled status.
* Add or remove the collection to/from the enabled list.
* Reply = alarm types which can be enabled
*/
CalEvent::Types CollectionControlModel::setEnabledStatus(const Collection& collection, CalEvent::Types types, bool inserted)
{
Laurent Montel's avatar
Laurent Montel committed
764
    qCDebug(KALARM_LOG) << "id:" << collection.id() << ", types=" << types;
Laurent Montel's avatar
Laurent Montel committed
765 766
    CalEvent::Types disallowedStdTypes(0);
    CalEvent::Types stdTypes(0);
767 768 769 770

    // Prevent the enabling of duplicate alarm types if another collection
    // uses the same backend storage.
    const Collection::List cols = collections();
David Jarvie's avatar
David Jarvie committed
771
    const CalEvent::Types canEnable = checkTypesToEnable(collection, cols, types);
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786

    // Update the list of enabled collections
    if (canEnable)
    {
        bool inList = false;
        const Collection::List cols = collections();
        foreach (const Collection& c, cols)
        {
            if (c.id() == collection.id())
            {
                inList = true;
                break;
            }
        }
        if (!inList)
787 788 789 790 791 792 793 794 795 796 797 798 799 800
        {
            // It's a new collection.
            // Prevent duplicate standard collections being created for any alarm type.
            stdTypes = collection.hasAttribute<CollectionAttribute>()
                                ? collection.attribute<CollectionAttribute>()->standard()
                                : CalEvent::EMPTY;
            if (stdTypes)
            {
                foreach (const Collection& col, cols)
                {
                    Collection c(col);
                    AkonadiModel::instance()->refresh(c);    // update with latest data
                    if (c.isValid())
                    {
David Jarvie's avatar
David Jarvie committed
801
                        const CalEvent::Types t = stdTypes & CalEvent::types(c.contentMimeTypes());
802 803 804 805 806 807 808 809 810 811 812 813 814
                        if (t)
                        {
                            if (c.hasAttribute<CollectionAttribute>()
                            &&  AkonadiModel::isCompatible(c))
                            {
                                disallowedStdTypes |= c.attribute<CollectionAttribute>()->standard() & t;
                                if (disallowedStdTypes == stdTypes)
                                    break;
                            }
                        }
                    }
                }
            }
815
            addCollection(collection);
816
        }
817 818 819 820
    }
    else
        removeCollection(collection);

821
    if (disallowedStdTypes  ||  !inserted  ||  canEnable != types)
822 823 824 825
    {
        // Update the collection's status
        AkonadiModel* model = static_cast<AkonadiModel*>(sourceModel());
        if (!model->isCollectionBeingDeleted(collection.id()))
826
        {
David Jarvie's avatar
David Jarvie committed
827
            const QModelIndex ix = model->collectionIndex(collection);
828 829 830 831 832
            if (!inserted  ||  canEnable != types)
                model->setData(ix, static_cast<int>(canEnable), AkonadiModel::EnabledTypesRole);
            if (disallowedStdTypes)
                model->setData(ix, static_cast<int>(stdTypes & ~disallowedStdTypes), AkonadiModel::IsStandardRole);
        }
833 834
    }
    return canEnable;
835 836
}

837 838 839 840 841
/******************************************************************************
* Called when a collection parameter or status has changed.
* If it's the enabled status, add or remove the collection to/from the enabled
* list.
*/
842
void CollectionControlModel::statusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant& value, bool inserted)
843
{
844 845 846
    if (!collection.isValid())
        return;

847
    switch (change)
848
    {
849
        case AkonadiModel::Enabled:
850
        {
David Jarvie's avatar
David Jarvie committed
851
            const CalEvent::Types enabled = static_cast<CalEvent::Types>(value.toInt());
Laurent Montel's avatar
Laurent Montel committed
852
            qCDebug(KALARM_LOG) << "id:" << collection.id() << ", enabled=" << enabled << ", inserted=" << inserted;
853
            setEnabledStatus(collection, enabled, inserted);
854 855 856
            break;
        }
        case AkonadiModel::ReadOnly:
857
        {
858
            bool readOnly = value.toBool();
Laurent Montel's avatar
Laurent Montel committed
859
            qCDebug(KALARM_LOG) << "id:" << collection.id() << ", readOnly=" << readOnly;
860 861 862
            if (readOnly)
            {
                // A read-only collection can't be the default for any alarm type
David Jarvie's avatar
David Jarvie committed
863
                const CalEvent::Types std = standardTypes(collection, false);
864
                if (std != CalEvent::EMPTY)
865 866
                {
                    Collection c(collection);
867
                    setStandard(c, CalEvent::Types(CalEvent::EMPTY));
868 869 870 871 872
                    QWidget* messageParent = qobject_cast<QWidget*>(QObject::parent());
                    bool singleType = true;
                    QString msg;
                    switch (std)
                    {
873
                        case CalEvent::ACTIVE:
Laurent Montel's avatar
Laurent Montel committed
874
                            msg = xi18nc("@info", "The calendar <resource>%1</resource> has been made read-only. "
875 876 877
                                                 "This was the default calendar for active alarms.",
                                        collection.name());
                            break;
878
                        case CalEvent::ARCHIVED:
Laurent Montel's avatar
Laurent Montel committed
879
                            msg = xi18nc("@info", "The calendar <resource>%1</resource> has been made read-only. "
880 881 882
                                                 "This was the default calendar for archived alarms.",
                                        collection.name());
                            break;
883
                        case CalEvent::TEMPLATE:
Laurent Montel's avatar
Laurent Montel committed
884
                            msg = xi18nc("@info", "The calendar <resource>%1</resource> has been made read-only. "
885 886 887 888
                                                 "This was the default calendar for alarm templates.",
                                        collection.name());
                            break;
                        default:
Laurent Montel's avatar
Laurent Montel committed
889
                            msg = xi18nc("@info", "<para>The calendar <resource>%1</resource> has been made read-only. "
890 891 892 893 894 895 896
                                                 "This was the default calendar for:%2</para>"
                                                 "<para>Please select new default calendars.</para>",
                                        collection.name(), typeListForDisplay(std));
                            singleType = false;
                            break;
                    }
                    if (singleType)
Laurent Montel's avatar
Laurent Montel committed
897
                        msg = xi18nc("@info", "<para>%1</para><para>Please select a new default calendar.</para>", msg);
898
                    KAMessageBox::information(messageParent, msg);
899 900 901
                }
            }
            break;
902
        }
903 904
        default:
            break;
905 906 907
    }
}

908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925
/******************************************************************************
* Check which alarm types can be enabled for a specified collection.
* If the collection uses the same backend storage as another collection, any
* alarm types already enabled in the other collection must be disabled in this
* collection. This is to avoid duplicating events between different resources,
* which causes user confusion and annoyance, and causes crashes.
* Parameters:
*   collection  - must be up to date (using AkonadiModel::refresh() etc.)
*   collections = list of collections to search for duplicates.
*   types       = alarm types to be enabled for the collection.
* Reply = alarm types which can be enabled without duplicating other collections.
*/
CalEvent::Types CollectionControlModel::checkTypesToEnable(const Collection& collection, const Collection::List& collections, CalEvent::Types types)
{
    types &= (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE);
    if (types)
    {
        // At least on alarm type is to be enabled
Daniel Vrátil's avatar
Daniel Vrátil committed
926
        const QUrl location = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile);
927 928
        foreach (const Collection& c, collections)
        {
Daniel Vrátil's avatar
Daniel Vrátil committed
929 930
            const QUrl cLocation = QUrl::fromUserInput(c.remoteId(), QString(), QUrl::AssumeLocalFile);
            if (c.id() != collection.id()  &&  cLocation == location)
931 932 933 934
            {
                // The collection duplicates the backend storage
                // used by another enabled collection.
                // N.B. don't refresh this collection - assume no change.
Laurent Montel's avatar
Laurent Montel committed
935
                qCDebug(KALARM_LOG) << "Collection" << c.id() << "duplicates backend for" << collection.id();
936 937 938 939 940 941 942 943 944 945 946 947
                if (c.hasAttribute<CollectionAttribute>())
                {
                    types &= ~c.attribute<CollectionAttribute>()->enabled();
                    if (!types)
                        break;
                }
            }
        }
    }
    return types;
}

948 949 950
/******************************************************************************
* Create a bulleted list of alarm types for insertion into <para>...</para>.
*/
951
QString CollectionControlModel::typeListForDisplay(CalEvent::Types alarmTypes)
952 953
{
    QString list;
954
    if (alarmTypes & CalEvent::ACTIVE)
955
        list += QLatin1String("<item>") + i18nc("@info", "Active Alarms") + QLatin1String("</item>");
956
    if (alarmTypes & CalEvent::ARCHIVED)
957
        list += QLatin1String("<item>") + i18nc("@info", "Archived Alarms") + QLatin1String("</item>");
958
    if (alarmTypes & CalEvent::TEMPLATE)
959
        list += QLatin1String("<item>") + i18nc("@info", "Alarm Templates") + QLatin1String("</item>");
960
    if (!list.isEmpty())
David Jarvie's avatar
David Jarvie committed
961
        list = QStringLiteral("<list>") + list + QStringLiteral("</list>");
962 963 964
    return list;
}

965
/******************************************************************************
966 967
* Return whether a collection is both enabled and fully writable for a given
* alarm type.
968
* Optionally, the enabled status can be ignored.
969 970 971
* Reply: 1 = fully enabled and writable,
*        0 = enabled and writable except that backend calendar is in an old KAlarm format,
*       -1 = not enabled, read-only, or incompatible format.
972
*/
973
int CollectionControlModel::isWritableEnabled(const Akonadi::Collection& collection, CalEvent::Type type)
974
{
975
    KACalendar::Compat format;
976
    return isWritableEnabled(collection, type, format);
977
}
978
int CollectionControlModel::isWritableEnabled(const Akonadi::Collection& collection, CalEvent::Type type, KACalendar::Compat& format)
David Jarvie's avatar