1 /*
2     SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "browserwidget.h"
8 
9 #include "akonadibrowsermodel.h"
10 #include "collectionaclpage.h"
11 #include "collectionattributespage.h"
12 #include "collectioninternalspage.h"
13 #include "config-akonadiconsole.h"
14 #include "dbaccess.h"
15 #include "tagpropertiesdialog.h"
16 
17 #include <Akonadi/AttributeFactory>
18 #include <Akonadi/ChangeRecorder>
19 #include <Akonadi/CollectionFilterProxyModel>
20 #include <Akonadi/CollectionPropertiesDialog>
21 #include <Akonadi/ControlGui>
22 #include <Akonadi/EntityListView>
23 #include <Akonadi/EntityMimeTypeFilterModel>
24 #include <Akonadi/EntityTreeView>
25 #include <Akonadi/FavoriteCollectionsModel>
26 #include <Akonadi/ItemFetchJob>
27 #include <Akonadi/ItemFetchScope>
28 #include <Akonadi/ItemModifyJob>
29 #include <Akonadi/Job>
30 #include <Akonadi/SelectionProxyModel>
31 #include <Akonadi/Session>
32 #include <Akonadi/StandardActionManager>
33 #include <Akonadi/StatisticsProxyModel>
34 #include <Akonadi/TagDeleteJob>
35 #include <Akonadi/TagFetchScope>
36 #include <Akonadi/TagModel>
37 #include <Akonadi/XmlWriteJob>
38 #include <KViewStateMaintainer>
39 #include <akonadi/private/compressionstream_p.h>
40 
41 #include <KCalendarCore/ICalFormat>
42 #include <KCalendarCore/Incidence>
43 #include <KContacts/Addressee>
44 #include <KContacts/ContactGroup>
45 
46 #include "akonadiconsole_debug.h"
47 #include <Akonadi/TagCreateJob>
48 #include <Akonadi/TagModifyJob>
49 #include <KActionCollection>
50 #include <KConfig>
51 #include <KConfigGroup>
52 #include <KMessageBox>
53 #include <KToggleAction>
54 #include <KXmlGuiWindow>
55 
56 #include <KSharedConfig>
57 #include <QBuffer>
58 #include <QFileDialog>
59 #include <QMenu>
60 #include <QSplitter>
61 #include <QSqlError>
62 #include <QSqlQuery>
63 #include <QStandardItemModel>
64 #include <QTimer>
65 #include <QVBoxLayout>
66 
67 #ifdef ENABLE_CONTENTVIEWS
68 #include <Akonadi/Contact/ContactGroupViewer>
69 #include <Akonadi/Contact/ContactViewer>
70 #include <CalendarSupport/IncidenceViewer>
71 #include <MessageViewer/Viewer>
72 #endif
73 
74 using namespace Akonadi;
75 
AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY(CollectionAttributePageFactory,CollectionAttributePage)76 AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY(CollectionAttributePageFactory, CollectionAttributePage)
77 AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY(CollectionInternalsPageFactory, CollectionInternalsPage)
78 AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY(CollectionAclPageFactory, CollectionAclPage)
79 
80 Q_DECLARE_METATYPE(QSet<QByteArray>)
81 
82 BrowserWidget::BrowserWidget(KXmlGuiWindow *xmlGuiWindow, QWidget *parent)
83     : QWidget(parent)
84 {
85     Q_ASSERT(xmlGuiWindow);
86     auto layout = new QVBoxLayout(this);
87 
88     auto splitter = new QSplitter(Qt::Horizontal, this);
89     splitter->setObjectName(QStringLiteral("collectionSplitter"));
90     layout->addWidget(splitter);
91 
92     auto splitter2 = new QSplitter(Qt::Vertical, this);
93     splitter2->setObjectName(QStringLiteral("ffvSplitter"));
94 
95     mCollectionView = new Akonadi::EntityTreeView(xmlGuiWindow, this);
96     mCollectionView->setObjectName(QStringLiteral("CollectionView"));
97     mCollectionView->setSelectionMode(QAbstractItemView::ExtendedSelection);
98     splitter2->addWidget(mCollectionView);
99 
100     auto favoritesView = new EntityListView(xmlGuiWindow, this);
101     // favoritesView->setViewMode( QListView::IconMode );
102     splitter2->addWidget(favoritesView);
103 
104     splitter->addWidget(splitter2);
105 
106     auto tagRecorder = new ChangeRecorder(this);
107     tagRecorder->setObjectName(QStringLiteral("tagRecorder"));
108     tagRecorder->setTypeMonitored(Monitor::Tags);
109     tagRecorder->setChangeRecordingEnabled(false);
110     mTagView = new QTreeView(this);
111     mTagModel = new Akonadi::TagModel(tagRecorder, this);
112     mTagView->setModel(mTagModel);
113     splitter2->addWidget(mTagView);
114 
115     mTagView->setContextMenuPolicy(Qt::CustomContextMenu);
116     connect(mTagView, &QTreeView::customContextMenuRequested, this, &BrowserWidget::tagViewContextMenuRequested);
117     connect(mTagView, &QTreeView::doubleClicked, this, &BrowserWidget::tagViewDoubleClicked);
118 
119     auto session = new Session(("AkonadiConsole Browser Widget"), this);
120 
121     // monitor collection changes
122     mBrowserMonitor = new ChangeRecorder(this);
123     mBrowserMonitor->setObjectName(QStringLiteral("collectionMonitor"));
124     mBrowserMonitor->setSession(session);
125     mBrowserMonitor->setCollectionMonitored(Collection::root());
126     mBrowserMonitor->fetchCollection(true);
127     mBrowserMonitor->setAllMonitored(true);
128     // TODO: Only fetch the envelope etc if possible.
129     mBrowserMonitor->itemFetchScope().fetchFullPayload(true);
130     mBrowserMonitor->itemFetchScope().setCacheOnly(true);
131     mBrowserMonitor->itemFetchScope().setFetchGid(true);
132 
133     mBrowserModel = new AkonadiBrowserModel(mBrowserMonitor, this);
134     mBrowserModel->setItemPopulationStrategy(EntityTreeModel::LazyPopulation);
135     mBrowserModel->setShowSystemEntities(true);
136     mBrowserModel->setListFilter(CollectionFetchScope::Display);
137 
138     //   new ModelTest( mBrowserModel );
139 
140     auto collectionFilter = new EntityMimeTypeFilterModel(this);
141     collectionFilter->setSourceModel(mBrowserModel);
142     collectionFilter->addMimeTypeInclusionFilter(Collection::mimeType());
143     collectionFilter->setHeaderGroup(EntityTreeModel::CollectionTreeHeaders);
144     collectionFilter->setDynamicSortFilter(true);
145     collectionFilter->setSortCaseSensitivity(Qt::CaseInsensitive);
146 
147     statisticsProxyModel = new Akonadi::StatisticsProxyModel(this);
148     statisticsProxyModel->setToolTipEnabled(true);
149     statisticsProxyModel->setSourceModel(collectionFilter);
150 
151     mCollectionView->setModel(statisticsProxyModel);
152 
153     auto selectionProxyModel = new Akonadi::SelectionProxyModel(mCollectionView->selectionModel(), this);
154     selectionProxyModel->setSourceModel(mBrowserModel);
155     selectionProxyModel->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection);
156 
157     auto itemFilter = new EntityMimeTypeFilterModel(this);
158     itemFilter->setSourceModel(selectionProxyModel);
159     itemFilter->addMimeTypeExclusionFilter(Collection::mimeType());
160     itemFilter->setHeaderGroup(EntityTreeModel::ItemListHeaders);
161 
162     const KConfigGroup group = KSharedConfig::openConfig()->group("FavoriteCollectionsModel");
163     connect(mBrowserModel, &AkonadiBrowserModel::columnsChanged, itemFilter, &EntityMimeTypeFilterModel::invalidate);
164     auto sortModel = new AkonadiBrowserSortModel(mBrowserModel, this);
165     sortModel->setDynamicSortFilter(true);
166     sortModel->setSourceModel(itemFilter);
167     auto favoritesModel = new FavoriteCollectionsModel(mBrowserModel, group, this);
168     favoritesView->setModel(favoritesModel);
169 
170     auto splitter3 = new QSplitter(Qt::Vertical, this);
171     splitter3->setObjectName(QStringLiteral("itemSplitter"));
172     splitter->addWidget(splitter3);
173 
174     auto itemViewParent = new QWidget(this);
175     itemUi.setupUi(itemViewParent);
176 
177     itemUi.modelBox->addItem(QStringLiteral("Generic"));
178     itemUi.modelBox->addItem(QStringLiteral("Mail"));
179     itemUi.modelBox->addItem(QStringLiteral("Contacts"));
180     itemUi.modelBox->addItem(QStringLiteral("Calendar/Tasks"));
181     connect(itemUi.modelBox, static_cast<void (KComboBox::*)(int)>(&KComboBox::activated), this, &BrowserWidget::modelChanged);
182     QTimer::singleShot(0, this, &BrowserWidget::modelChanged);
183 
184     itemUi.itemView->setXmlGuiClient(xmlGuiWindow);
185     itemUi.itemView->setModel(sortModel);
186     itemUi.itemView->setSelectionMode(QAbstractItemView::ExtendedSelection);
187     connect(itemUi.itemView->selectionModel(), &QItemSelectionModel::currentChanged, this, &BrowserWidget::currentChanged);
188 
189     splitter3->addWidget(itemViewParent);
190     itemViewParent->layout()->setContentsMargins(0, 0, 0, 0);
191 
192     auto contentViewParent = new QWidget(this);
193     contentUi.setupUi(contentViewParent);
194     contentUi.saveButton->setEnabled(false);
195     connect(contentUi.saveButton, &QPushButton::clicked, this, &BrowserWidget::save);
196     splitter3->addWidget(contentViewParent);
197 
198 #ifdef ENABLE_CONTENTVIEWS
199     auto w = new QWidget;
200     w->setLayout(new QVBoxLayout);
201     w->layout()->addWidget(mContactView = new Akonadi::ContactViewer);
202     contentUi.stack->addWidget(w);
203 
204     w = new QWidget;
205     w->setLayout(new QVBoxLayout);
206     w->layout()->addWidget(mContactGroupView = new Akonadi::ContactGroupViewer);
207     contentUi.stack->addWidget(w);
208 
209     w = new QWidget;
210     w->setLayout(new QVBoxLayout);
211     w->layout()->addWidget(mIncidenceView = new CalendarSupport::IncidenceViewer);
212     contentUi.stack->addWidget(w);
213 
214     w = new QWidget;
215     w->setLayout(new QVBoxLayout);
216     w->layout()->addWidget(mMailView = new MessageViewer::Viewer(this));
217     contentUi.stack->addWidget(w);
218 #endif
219 
220     connect(contentUi.attrAddButton, &QPushButton::clicked, this, &BrowserWidget::addAttribute);
221     connect(contentUi.attrDeleteButton, &QPushButton::clicked, this, &BrowserWidget::delAttribute);
222     connect(contentUi.flags, &KEditListWidget::changed, this, &BrowserWidget::contentViewChanged);
223     connect(contentUi.tags, &KEditListWidget::changed, this, &BrowserWidget::contentViewChanged);
224     connect(contentUi.remoteId, &QLineEdit::textChanged, this, &BrowserWidget::contentViewChanged);
225     connect(contentUi.gid, &QLineEdit::textChanged, this, &BrowserWidget::contentViewChanged);
226 
227     CollectionPropertiesDialog::registerPage(new CollectionAclPageFactory());
228     CollectionPropertiesDialog::registerPage(new CollectionAttributePageFactory());
229     CollectionPropertiesDialog::registerPage(new CollectionInternalsPageFactory());
230 
231     ControlGui::widgetNeedsAkonadi(this);
232 
233     mStdActionManager = new StandardActionManager(xmlGuiWindow->actionCollection(), xmlGuiWindow);
234     mStdActionManager->setCollectionSelectionModel(mCollectionView->selectionModel());
235     mStdActionManager->setItemSelectionModel(itemUi.itemView->selectionModel());
236     mStdActionManager->setFavoriteCollectionsModel(favoritesModel);
237     mStdActionManager->setFavoriteSelectionModel(favoritesView->selectionModel());
238     mStdActionManager->createAllActions();
239 
240     mCacheOnlyAction = new KToggleAction(QStringLiteral("Cache only retrieval"), xmlGuiWindow);
241     mCacheOnlyAction->setChecked(true);
242     xmlGuiWindow->actionCollection()->addAction(QStringLiteral("akonadiconsole_cacheonly"), mCacheOnlyAction);
243     connect(mCacheOnlyAction, &KToggleAction::toggled, this, &BrowserWidget::updateItemFetchScope);
244 
245     m_stateMaintainer = new KViewStateMaintainer<ETMViewStateSaver>(KSharedConfig::openConfig()->group("CollectionViewState"), this);
246     m_stateMaintainer->setView(mCollectionView);
247 
248     m_stateMaintainer->restoreState();
249 }
250 
~BrowserWidget()251 BrowserWidget::~BrowserWidget()
252 {
253     m_stateMaintainer->saveState();
254 }
255 
clear()256 void BrowserWidget::clear()
257 {
258     contentUi.stack->setCurrentWidget(contentUi.unsupportedTypePage);
259     contentUi.dataView->clear();
260     contentUi.id->clear();
261     contentUi.remoteId->clear();
262     contentUi.gid->clear();
263     contentUi.mimeType->clear();
264     contentUi.revision->clear();
265     contentUi.size->clear();
266     contentUi.modificationtime->clear();
267     contentUi.flags->clear();
268     contentUi.tags->clear();
269     contentUi.attrView->setModel(nullptr);
270 }
271 
currentChanged(const QModelIndex & index)272 void BrowserWidget::currentChanged(const QModelIndex &index)
273 {
274     const Item item = index.sibling(index.row(), 0).data(EntityTreeModel::ItemRole).value<Item>();
275     if (!item.isValid()) {
276         clear();
277         return;
278     }
279 
280     auto job = new ItemFetchJob(item, this);
281     job->fetchScope().fetchFullPayload();
282     job->fetchScope().fetchAllAttributes();
283     job->fetchScope().setFetchTags(true);
284     auto &tfs = job->fetchScope().tagFetchScope();
285     tfs.setFetchIdOnly(false);
286     tfs.fetchAllAttributes();
287     connect(job, &ItemFetchJob::result, this, &BrowserWidget::itemFetchDone);
288 }
289 
itemFetchDone(KJob * job)290 void BrowserWidget::itemFetchDone(KJob *job)
291 {
292     auto fetch = static_cast<ItemFetchJob *>(job);
293     if (job->error()) {
294         qCWarning(AKONADICONSOLE_LOG) << "Item fetch failed: " << job->errorString();
295     } else if (fetch->items().isEmpty()) {
296         qCWarning(AKONADICONSOLE_LOG) << "No item found!";
297     } else {
298         const Item item = fetch->items().first();
299         setItem(item);
300     }
301 }
302 
contentViewChanged()303 void BrowserWidget::contentViewChanged()
304 {
305     contentUi.saveButton->setEnabled(true);
306 }
307 
setItem(const Akonadi::Item & item)308 void BrowserWidget::setItem(const Akonadi::Item &item)
309 {
310     mCurrentItem = item;
311 #ifdef ENABLE_CONTENTVIEWS
312     if (item.hasPayload<KContacts::Addressee>()) {
313         mContactView->setItem(item);
314         contentUi.stack->setCurrentWidget(mContactView->parentWidget());
315     } else if (item.hasPayload<KContacts::ContactGroup>()) {
316         mContactGroupView->setItem(item);
317         contentUi.stack->setCurrentWidget(mContactGroupView->parentWidget());
318     } else if (item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
319         mIncidenceView->setItem(item);
320         contentUi.stack->setCurrentWidget(mIncidenceView->parentWidget());
321     } else if (item.mimeType() == QLatin1String("message/rfc822") || item.mimeType() == QLatin1String("message/news")) {
322         mMailView->setMessageItem(item, MimeTreeParser::Force);
323         contentUi.stack->setCurrentWidget(mMailView->parentWidget());
324     } else
325 #endif
326         if (item.hasPayload<QPixmap>()) {
327         contentUi.imageView->setPixmap(item.payload<QPixmap>());
328         contentUi.stack->setCurrentWidget(contentUi.imageViewPage);
329     } else {
330         contentUi.stack->setCurrentWidget(contentUi.unsupportedTypePage);
331     }
332 
333     contentUi.saveButton->setEnabled(false);
334 
335     QByteArray data = item.payloadData();
336     QBuffer buffer(&data);
337     buffer.open(QIODevice::ReadOnly);
338 
339     if (Akonadi::CompressionStream::isCompressed(&buffer)) {
340         Akonadi::CompressionStream stream(&buffer);
341         stream.open(QIODevice::ReadOnly);
342         data = stream.readAll();
343     }
344 
345     // Note that this is true for *all* items as soon as the binary format is enabled.
346     // Independently from how they are actually stored in the database.
347     if (item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
348         quint32 magic;
349         QDataStream input(data);
350         input >> magic;
351         KCalendarCore::ICalFormat format;
352         if (magic == KCalendarCore::IncidenceBase::magicSerializationIdentifier()) {
353             // Binary format isn't readable, show KCalendarCore string instead.
354             auto incidence = item.payload<KCalendarCore::Incidence::Ptr>();
355             data = "(converted from binary format)\n" + format.toRawString(incidence);
356         }
357     }
358 
359     contentUi.dataView->setPlainText(QString::fromLatin1(data));
360 
361     contentUi.id->setText(QString::number(item.id()));
362     contentUi.remoteId->setText(item.remoteId());
363     contentUi.gid->setText(item.gid());
364     contentUi.mimeType->setText(item.mimeType());
365     contentUi.revision->setText(QString::number(item.revision()));
366     contentUi.size->setText(QString::number(item.size()));
367     contentUi.modificationtime->setText(item.modificationTime().toString() + QStringLiteral(" UTC"));
368     QStringList flags;
369     const auto itemFlags = item.flags();
370     for (const Item::Flag &f : itemFlags) {
371         flags << QString::fromUtf8(f);
372     }
373     contentUi.flags->setItems(flags);
374 
375     QStringList tags;
376     const auto itemTags = item.tags();
377     for (const Tag &tag : itemTags) {
378         tags << QLatin1String(tag.gid());
379     }
380     contentUi.tags->setItems(tags);
381 
382     Attribute::List list = item.attributes();
383     delete mAttrModel;
384     mAttrModel = new QStandardItemModel();
385     QStringList labels{QStringLiteral("Attribute"), QStringLiteral("Value")};
386     mAttrModel->setHorizontalHeaderLabels(labels);
387     for (const auto attr : list) {
388         auto type = new QStandardItem(QString::fromLatin1(attr->type()));
389         type->setEditable(false);
390         mAttrModel->appendRow({type, new QStandardItem(QString::fromLatin1(attr->serialized()))});
391     }
392     contentUi.attrView->setModel(mAttrModel);
393     connect(mAttrModel, &QStandardItemModel::itemChanged, this, &BrowserWidget::contentViewChanged);
394 
395     if (mMonitor) {
396         mMonitor->deleteLater(); // might be the one calling us
397     }
398     mMonitor = new Monitor(this);
399     mMonitor->setObjectName(QStringLiteral("itemMonitor"));
400     mMonitor->setItemMonitored(item);
401     mMonitor->itemFetchScope().fetchFullPayload();
402     mMonitor->itemFetchScope().fetchAllAttributes();
403     qRegisterMetaType<QSet<QByteArray>>();
404     connect(mMonitor, &Akonadi::Monitor::itemChanged, this, &BrowserWidget::setItem, Qt::QueuedConnection);
405     contentUi.saveButton->setEnabled(false);
406 }
407 
modelChanged()408 void BrowserWidget::modelChanged()
409 {
410     switch (itemUi.modelBox->currentIndex()) {
411     case 1:
412         mBrowserModel->setItemDisplayMode(AkonadiBrowserModel::MailMode);
413         break;
414     case 2:
415         mBrowserModel->setItemDisplayMode(AkonadiBrowserModel::ContactsMode);
416         break;
417     case 3:
418         mBrowserModel->setItemDisplayMode(AkonadiBrowserModel::CalendarMode);
419         break;
420     default:
421         mBrowserModel->setItemDisplayMode(AkonadiBrowserModel::GenericMode);
422     }
423 }
424 
save()425 void BrowserWidget::save()
426 {
427     Q_ASSERT(mAttrModel);
428 
429     const QByteArray data = contentUi.dataView->toPlainText().toUtf8();
430     Item item = mCurrentItem;
431     item.setRemoteId(contentUi.remoteId->text());
432     item.setGid(contentUi.gid->text());
433     const auto currentItemFlags = mCurrentItem.flags();
434     for (const Item::Flag &f : currentItemFlags) {
435         item.clearFlag(f);
436     }
437     const auto contentUiItemFlags = contentUi.flags->items();
438     for (const QString &s : contentUiItemFlags) {
439         item.setFlag(s.toUtf8());
440     }
441     const auto contentUiItemTags = mCurrentItem.tags();
442     for (const Tag &tag : contentUiItemTags) {
443         item.clearTag(tag);
444     }
445     const auto contentUiTagsItems = contentUi.tags->items();
446     for (const QString &s : contentUiTagsItems) {
447         Tag tag;
448         tag.setGid(s.toLatin1());
449         item.setTag(tag);
450     }
451     item.setPayloadFromData(data);
452 
453     item.clearAttributes();
454     for (int i = 0; i < mAttrModel->rowCount(); ++i) {
455         const QModelIndex typeIndex = mAttrModel->index(i, 0);
456         Q_ASSERT(typeIndex.isValid());
457         const QModelIndex valueIndex = mAttrModel->index(i, 1);
458         Q_ASSERT(valueIndex.isValid());
459         Attribute *attr = AttributeFactory::createAttribute(mAttrModel->data(typeIndex).toString().toLatin1());
460         Q_ASSERT(attr);
461         attr->deserialize(mAttrModel->data(valueIndex).toString().toLatin1());
462         item.addAttribute(attr);
463     }
464 
465     auto store = new ItemModifyJob(item, this);
466     connect(store, &ItemModifyJob::result, this, &BrowserWidget::saveResult);
467 }
468 
saveResult(KJob * job)469 void BrowserWidget::saveResult(KJob *job)
470 {
471     if (job->error()) {
472         KMessageBox::error(this, QStringLiteral("Failed to save changes: %1").arg(job->errorString()));
473     } else {
474         contentUi.saveButton->setEnabled(false);
475     }
476 }
477 
addAttribute()478 void BrowserWidget::addAttribute()
479 {
480     if (!mAttrModel || contentUi.attrName->text().isEmpty()) {
481         return;
482     }
483     const int row = mAttrModel->rowCount();
484     mAttrModel->insertRow(row);
485     QModelIndex index = mAttrModel->index(row, 0);
486     Q_ASSERT(index.isValid());
487     mAttrModel->setData(index, contentUi.attrName->text());
488     contentUi.attrName->clear();
489     contentUi.saveButton->setEnabled(true);
490 }
491 
delAttribute()492 void BrowserWidget::delAttribute()
493 {
494     if (!mAttrModel) {
495         return;
496     }
497     QModelIndexList selection = contentUi.attrView->selectionModel()->selectedRows();
498     if (selection.count() != 1) {
499         return;
500     }
501     mAttrModel->removeRow(selection.first().row());
502     contentUi.saveButton->setEnabled(true);
503 }
504 
dumpToXml()505 void BrowserWidget::dumpToXml()
506 {
507     const Collection root = currentCollection();
508     if (!root.isValid()) {
509         return;
510     }
511     const QString fileName = QFileDialog::getSaveFileName(this, QStringLiteral("Select XML file"), QString(), QStringLiteral("*.xml"));
512     if (fileName.isEmpty()) {
513         return;
514     }
515 
516     auto job = new XmlWriteJob(root, fileName, this);
517     connect(job, &XmlWriteJob::result, this, &BrowserWidget::dumpToXmlResult);
518 }
519 
dumpToXmlResult(KJob * job)520 void BrowserWidget::dumpToXmlResult(KJob *job)
521 {
522     if (job->error()) {
523         KMessageBox::error(this, job->errorString());
524     }
525 }
526 
clearCache()527 void BrowserWidget::clearCache()
528 {
529     const Collection coll = currentCollection();
530     if (!coll.isValid()) {
531         return;
532     }
533 
534     const auto ridCount = QStringLiteral("SELECT COUNT(*) FROM PimItemTable WHERE collectionId=%1 AND remoteId=''").arg(coll.id());
535     QSqlQuery query(DbAccess::database());
536     if (!query.exec(ridCount)) {
537         qCWarning(AKONADICONSOLE_LOG) << "Failed to execute query" << ridCount << ":" << query.lastError().text();
538         KMessageBox::error(this, query.lastError().text());
539         return;
540     }
541 
542     query.next();
543     const int emptyRidCount = query.value(0).toInt();
544     if (emptyRidCount > 0) {
545         if (KMessageBox::warningContinueCancel(this,
546                                                QStringLiteral("The collection '%1' contains %2 items without Remote ID. "
547                                                               "Those items were likely never uploaded to the destination server, "
548                                                               "so clearing this collection means that those <b>data will be lost</b>. "
549                                                               "Are you sure you want to proceed?")
550                                                    .arg(coll.id())
551                                                    .arg(emptyRidCount),
552                                                QStringLiteral("POSSIBLE DATA LOSS!"))
553             == KMessageBox::Cancel) {
554             return;
555         }
556     }
557 
558     QString str = QStringLiteral("DELETE FROM PimItemTable WHERE collectionId=%1").arg(coll.id());
559     qCDebug(AKONADICONSOLE_LOG) << str;
560     query = QSqlQuery(str, DbAccess::database());
561     if (query.exec()) {
562         if (query.lastError().isValid()) {
563             qCDebug(AKONADICONSOLE_LOG) << query.lastError();
564             KMessageBox::error(this, query.lastError().text());
565         }
566     }
567 
568     // TODO: Clear external parts
569     // TODO: Reset Akonadi's internal collection statistics cache
570     // TODO: Notify all clients EXCEPT FOR THE RESOURCE about the deletion?
571     // TODO: Clear search index
572     // TODO: ???
573 
574     KMessageBox::information(this, QStringLiteral("Collection cache cleared. You should restart Akonadi now."));
575 }
576 
currentCollection() const577 Akonadi::Collection BrowserWidget::currentCollection() const
578 {
579     return mCollectionView->currentIndex().data(EntityTreeModel::CollectionRole).value<Collection>();
580 }
581 
updateItemFetchScope()582 void BrowserWidget::updateItemFetchScope()
583 {
584     mBrowserMonitor->itemFetchScope().setCacheOnly(mCacheOnlyAction->isChecked());
585 }
586 
tagViewContextMenuRequested(const QPoint & pos)587 void BrowserWidget::tagViewContextMenuRequested(const QPoint &pos)
588 {
589     const QModelIndex index = mTagView->indexAt(pos);
590     auto menu = new QMenu(this);
591     connect(menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater);
592     menu->addAction(QIcon::fromTheme(QStringLiteral("list-add")), QStringLiteral("&Add tag..."), this, &BrowserWidget::addTagRequested);
593     if (index.isValid()) {
594         menu->addAction(QStringLiteral("Add &subtag..."), this, &BrowserWidget::addSubTagRequested);
595         menu->addAction(QIcon::fromTheme(QStringLiteral("document-edit")),
596                         QStringLiteral("&Edit tag..."),
597                         this,
598                         &BrowserWidget::editTagRequested,
599                         QKeySequence(Qt::Key_Return));
600         menu->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")),
601                         QStringLiteral("&Delete tag..."),
602                         this,
603                         &BrowserWidget::removeTagRequested,
604                         QKeySequence::Delete);
605         menu->setProperty("Tag", index.data(TagModel::TagRole));
606     }
607 
608     menu->popup(mTagView->mapToGlobal(pos));
609 }
610 
addTagRequested()611 void BrowserWidget::addTagRequested()
612 {
613     auto dlg = new TagPropertiesDialog(this);
614     connect(dlg, &TagPropertiesDialog::accepted, this, &BrowserWidget::createTag);
615     connect(dlg, &TagPropertiesDialog::rejected, dlg, &TagPropertiesDialog::deleteLater);
616     dlg->show();
617 }
618 
addSubTagRequested()619 void BrowserWidget::addSubTagRequested()
620 {
621     auto action = qobject_cast<QAction *>(sender());
622     const auto parentTag = action->parent()->property("Tag").value<Akonadi::Tag>();
623 
624     Akonadi::Tag tag;
625     tag.setParent(parentTag);
626 
627     auto dlg = new TagPropertiesDialog(tag, this);
628     connect(dlg, &TagPropertiesDialog::accepted, this, &BrowserWidget::createTag);
629     connect(dlg, &TagPropertiesDialog::rejected, dlg, &TagPropertiesDialog::deleteLater);
630     dlg->show();
631 }
632 
editTagRequested()633 void BrowserWidget::editTagRequested()
634 {
635     auto action = qobject_cast<QAction *>(sender());
636     const auto tag = action->parent()->property("Tag").value<Akonadi::Tag>();
637     auto dlg = new TagPropertiesDialog(tag, this);
638     connect(dlg, &TagPropertiesDialog::accepted, this, &BrowserWidget::modifyTag);
639     connect(dlg, &TagPropertiesDialog::rejected, dlg, &TagPropertiesDialog::deleteLater);
640     dlg->show();
641 }
642 
tagViewDoubleClicked(const QModelIndex & index)643 void BrowserWidget::tagViewDoubleClicked(const QModelIndex &index)
644 {
645     if (!index.isValid()) {
646         addTagRequested();
647         return;
648     }
649 
650     const auto tag = mTagModel->data(index, TagModel::TagRole).value<Akonadi::Tag>();
651     Q_ASSERT(tag.isValid());
652 
653     auto dlg = new TagPropertiesDialog(tag, this);
654     connect(dlg, &TagPropertiesDialog::accepted, this, &BrowserWidget::modifyTag);
655     connect(dlg, &TagPropertiesDialog::rejected, dlg, &TagPropertiesDialog::deleteLater);
656     dlg->show();
657 }
658 
removeTagRequested()659 void BrowserWidget::removeTagRequested()
660 {
661     if (KMessageBox::questionYesNo(this,
662                                    QStringLiteral("Do you really want to remove selected tag?"),
663                                    QStringLiteral("Delete tag?"),
664                                    KStandardGuiItem::del(),
665                                    KStandardGuiItem::cancel(),
666                                    QString(),
667                                    KMessageBox::Dangerous)
668         == KMessageBox::No) {
669         return;
670     }
671 
672     auto action = qobject_cast<QAction *>(sender());
673     const auto tag = action->parent()->property("Tag").value<Akonadi::Tag>();
674     new Akonadi::TagDeleteJob(tag, this);
675 }
676 
createTag()677 void BrowserWidget::createTag()
678 {
679     auto dlg = qobject_cast<TagPropertiesDialog *>(sender());
680     Q_ASSERT(dlg);
681 
682     if (dlg->changed()) {
683         new TagCreateJob(dlg->tag(), this);
684     }
685 }
686 
modifyTag()687 void BrowserWidget::modifyTag()
688 {
689     auto dlg = qobject_cast<TagPropertiesDialog *>(sender());
690     Q_ASSERT(dlg);
691 
692     if (dlg->changed()) {
693         new TagModifyJob(dlg->tag(), this);
694     }
695 }
696