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