1 /*
2   This file is part of KAddressBook.
3 
4   SPDX-FileCopyrightText: 2007 Tobias Koenig <tokoe@kde.org>
5 
6   SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 
9 #include "mainwidget.h"
10 
11 #include "categoryfilterproxymodel.h"
12 #include "categoryselectwidget.h"
13 #include "contactinfoproxymodel.h"
14 #include "contactswitcher.h"
15 #include "globalcontactmodel.h"
16 #include "kaddressbook_options.h"
17 #include "kaddressbookadaptor.h"
18 #include "manageshowcollectionproperties.h"
19 #include "modelcolumnmanager.h"
20 #include "printing/printingwizard.h"
21 #include "settings.h"
22 #include "stylecontactlistdelegate.h"
23 #include "widgets/quicksearchwidget.h"
24 
25 #include "importexport/contactselectiondialog.h"
26 #include "importexport/plugin.h"
27 #include "importexport/plugininterface.h"
28 #include "importexport/pluginmanager.h"
29 
30 #include <Akonadi/Contact/GrantleeContactFormatter>
31 #include <Akonadi/Contact/GrantleeContactGroupFormatter>
32 #include <GrantleeTheme/GrantleeThemeManager>
33 
34 #include "uistatesaver.h"
35 
36 #include <PimCommonAkonadi/ImapAclAttribute>
37 #include <PimCommonAkonadi/MailUtil>
38 
39 #include <Akonadi/AttributeFactory>
40 #include <Akonadi/CollectionFilterProxyModel>
41 #include <Akonadi/CollectionMaintenancePage>
42 #include <Akonadi/CollectionPropertiesDialog>
43 #include <Akonadi/ControlGui>
44 #include <Akonadi/ETMViewStateSaver>
45 #include <Akonadi/EntityMimeTypeFilterModel>
46 #include <Akonadi/EntityTreeView>
47 #include <Akonadi/MimeTypeChecker>
48 #include <AkonadiSearch/Debug/akonadisearchdebugdialog.h>
49 #include <KContacts/Addressee>
50 #include <PimCommon/PimUtil>
51 #include <PimCommonAkonadi/ManageServerSideSubscriptionJob>
52 #include <QPointer>
53 
54 #include <Akonadi/Contact/ContactDefaultActions>
55 #include <Akonadi/Contact/ContactGroupEditorDialog>
56 #include <Akonadi/Contact/ContactGroupViewer>
57 #include <Akonadi/Contact/ContactViewer>
58 #include <Akonadi/Contact/ContactsFilterProxyModel>
59 #include <Akonadi/Contact/ContactsTreeModel>
60 #include <Akonadi/Contact/StandardContactActionManager>
61 
62 #include "kaddressbook_debug.h"
63 #include <KActionCollection>
64 #include <KActionMenu>
65 #include <KCMultiDialog>
66 #include <KCheckableProxyModel>
67 #include <KContacts/ContactGroup>
68 #include <KDescendantsProxyModel>
69 #include <KLocalizedString>
70 #include <KPluginMetaData>
71 #include <KSelectionProxyModel>
72 #include <KToggleAction>
73 #include <KXMLGUIClient>
74 #include <QAction>
75 #include <QApplication>
76 #include <QTextBrowser>
77 
78 #include <Akonadi/ItemModifyJob>
79 #include <KPimPrintPreviewDialog>
80 #include <QActionGroup>
81 #include <QDBusConnection>
82 #include <QHBoxLayout>
83 #include <QHeaderView>
84 #include <QPrintDialog>
85 #include <QPrintPreviewDialog>
86 #include <QPrinter>
87 #include <QSplitter>
88 #include <QStackedWidget>
89 
90 #include "plugininterface/kaddressbookplugininterface.h"
91 
92 namespace
93 {
isStructuralCollection(const Akonadi::Collection & collection)94 static bool isStructuralCollection(const Akonadi::Collection &collection)
95 {
96     const QStringList mimeTypes = {KContacts::Addressee::mimeType(), KContacts::ContactGroup::mimeType()};
97     const QStringList collectionMimeTypes = collection.contentMimeTypes();
98     for (const QString &mimeType : mimeTypes) {
99         if (collectionMimeTypes.contains(mimeType)) {
100             return false;
101         }
102     }
103     return true;
104 }
105 
106 class StructuralCollectionsNotCheckableProxy : public KCheckableProxyModel
107 {
108 public:
StructuralCollectionsNotCheckableProxy(QObject * parent)109     explicit StructuralCollectionsNotCheckableProxy(QObject *parent)
110         : KCheckableProxyModel(parent)
111     {
112     }
113 
data(const QModelIndex & index,int role) const114     QVariant data(const QModelIndex &index, int role) const override
115     {
116         if (!index.isValid()) {
117             return QVariant();
118         }
119 
120         if (role == Qt::CheckStateRole) {
121             // Don't show the checkbox if the collection can't contain incidences
122             const auto collection = index.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
123             if (collection.isValid() && isStructuralCollection(collection)) {
124                 return QVariant();
125             }
126         }
127         return KCheckableProxyModel::data(index, role);
128     }
129 };
130 }
131 
MainWidget(KXMLGUIClient * guiClient,QWidget * parent)132 MainWidget::MainWidget(KXMLGUIClient *guiClient, QWidget *parent)
133     : QWidget(parent)
134     , mAllContactsModel(nullptr)
135     , mXmlGuiClient(guiClient)
136     , mGrantleeThemeManager(nullptr)
137     , mQuickSearchAction(nullptr)
138     , mServerSideSubscription(nullptr)
139 {
140     (void)new KaddressbookAdaptor(this);
141     QDBusConnection::sessionBus().registerObject(QStringLiteral("/KAddressBook"), this);
142 
143     mManageShowCollectionProperties = new ManageShowCollectionProperties(this, this);
144 
145     Akonadi::AttributeFactory::registerAttribute<PimCommon::ImapAclAttribute>();
146 
147     KAddressBookPluginInterface::self()->setActionCollection(guiClient->actionCollection());
148     KAddressBookPluginInterface::self()->initializePlugins();
149     setupGui();
150     setupActions(guiClient->actionCollection());
151 
152     /*
153      *  The item models, proxies and views have the following structure:
154      *
155      *                               mItemView
156      *                                   ^
157      *                                   |
158      *                           mContactsFilterModel
159      *                                   ^
160      *                                   |
161      * mCategorySelectWidget --> mCategoryFilterModel
162      *                                   ^
163      *                                   |
164      *                               mItemTree
165      *                                   ^
166      *                                   |
167      *                                   |           mAllContactsModel
168      *                                   |                  ^
169      *                                   |                  |
170      *      mCollectionView     selectionProxyModel  descendantsModel
171      *            ^               ^      ^                  ^
172      *            |               |      |                  |
173      *            |       selectionModel |                  |
174      *            |               |      |                  |
175      *        proxyModel ---------'      |                  |
176      *            ^                      |                  |
177      *            |                      |                  |
178      *      mCollectionTree              |                  |
179      *            ^                      |                  |
180      *            |                      |   _______________/
181      *             \                    /  /
182      *            GlobalContactModel::instance()
183      *
184      *
185      *  GlobalContactModel::instance():  The global contact model (contains collections and items)
186      *                 mCollectionTree:  Filters out all items
187      *                      proxyModel:  Allows the user to select collections by checkboxes
188      *                  selectionModel:  Represents the selected collections that have been
189      *                                   selected in proxyModel
190      *                 mCollectionView:  Shows the collections (address books) in a view
191      *             selectionProxyModel:  Filters out all collections and items that are no children
192      *                                   of the collections currently selected in selectionModel
193      *                       mItemTree:  Filters out all collections
194      *           mCategorySelectWidget:  Selects a list of categories for filtering
195      *            mCategoryFilterModel:  Filters the contacts by the selected categories
196      *            mContactsFilterModel:  Filters the contacts by the content of mQuickSearchWidget
197      *                       mItemView:  Shows the items (contacts and contact groups) in a view
198      *
199      *                descendantsModel:  Flattens the item/collection tree to a list
200      *               mAllContactsModel:  Provides a list of all available contacts from all
201      *                                   address books
202      */
203 
204     mCollectionTree = new Akonadi::EntityMimeTypeFilterModel(this);
205     mCollectionTree->setDynamicSortFilter(true);
206     mCollectionTree->setSortCaseSensitivity(Qt::CaseInsensitive);
207     mCollectionTree->setSourceModel(GlobalContactModel::instance()->model());
208     mCollectionTree->addMimeTypeInclusionFilter(Akonadi::Collection::mimeType());
209     mCollectionTree->setHeaderGroup(Akonadi::EntityTreeModel::CollectionTreeHeaders);
210 
211     mCollectionSelectionModel = new QItemSelectionModel(mCollectionTree);
212     auto checkableProxyModel = new StructuralCollectionsNotCheckableProxy(this);
213     checkableProxyModel->setSelectionModel(mCollectionSelectionModel);
214     checkableProxyModel->setSourceModel(mCollectionTree);
215 
216     mCollectionView->setModel(checkableProxyModel);
217     mCollectionView->setXmlGuiClient(guiClient);
218     mCollectionView->header()->setDefaultAlignment(Qt::AlignCenter);
219     mCollectionView->header()->setSortIndicatorShown(false);
220 
221     connect(mCollectionView->model(), &QAbstractItemModel::rowsInserted, this, &MainWidget::slotCheckNewCalendar);
222 
223     connect(mCollectionView, qOverload<const Akonadi::Collection &>(&Akonadi::EntityTreeView::currentChanged), this, &MainWidget::slotCurrentCollectionChanged);
224 
225     auto selectionProxyModel = new KSelectionProxyModel(mCollectionSelectionModel, this);
226     selectionProxyModel->setSourceModel(GlobalContactModel::instance()->model());
227     selectionProxyModel->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection);
228 
229     mItemTree = new Akonadi::EntityMimeTypeFilterModel(this);
230     mItemTree->setSourceModel(selectionProxyModel);
231     mItemTree->addMimeTypeExclusionFilter(Akonadi::Collection::mimeType());
232     mItemTree->setHeaderGroup(Akonadi::EntityTreeModel::ItemListHeaders);
233 
234     mCategoryFilterModel = new CategoryFilterProxyModel(this);
235     mCategoryFilterModel->setSourceModel(mItemTree);
236     mCategoryFilterModel->setFilterCategories(mCategorySelectWidget->filterTags());
237     mCategoryFilterModel->setFilterEnabled(true);
238 
239     connect(mCategorySelectWidget, &CategorySelectWidget::filterChanged, mCategoryFilterModel, &CategoryFilterProxyModel::setFilterCategories);
240 
241     mContactsFilterModel = new Akonadi::ContactsFilterProxyModel(this);
242     mContactsFilterModel->setSourceModel(mCategoryFilterModel);
243 
244     auto contactInfoProxyModel = new ContactInfoProxyModel(this);
245     contactInfoProxyModel->setSourceModel(mContactsFilterModel);
246 
247     connect(mQuickSearchWidget, &QuickSearchWidget::filterStringChanged, mContactsFilterModel, &Akonadi::ContactsFilterProxyModel::setFilterString);
248     connect(mQuickSearchWidget, &QuickSearchWidget::filterStringChanged, this, &MainWidget::selectFirstItem);
249     connect(mQuickSearchWidget, &QuickSearchWidget::arrowDownKeyPressed, this, &MainWidget::setFocusToTreeView);
250     mItemView->setModel(contactInfoProxyModel);
251     mItemView->setItemDelegate(new StyleContactListDelegate(this));
252     mItemView->setXmlGuiClient(guiClient);
253     mItemView->setSelectionMode(QAbstractItemView::ExtendedSelection);
254     mItemView->setRootIsDecorated(false);
255     mItemView->header()->setDefaultAlignment(Qt::AlignCenter);
256 
257     mActionManager = new Akonadi::StandardContactActionManager(guiClient->actionCollection(), this);
258     mActionManager->setCollectionSelectionModel(mCollectionView->selectionModel());
259     mActionManager->setItemSelectionModel(mItemView->selectionModel());
260 
261     QList<Akonadi::StandardActionManager::Type> standardActions;
262     standardActions << Akonadi::StandardActionManager::CreateCollection << Akonadi::StandardActionManager::DeleteCollections
263                     << Akonadi::StandardActionManager::SynchronizeCollections << Akonadi::StandardActionManager::CollectionProperties
264                     << Akonadi::StandardActionManager::CopyItems << Akonadi::StandardActionManager::Paste << Akonadi::StandardActionManager::DeleteItems
265                     << Akonadi::StandardActionManager::CutItems << Akonadi::StandardActionManager::CreateResource
266                     << Akonadi::StandardActionManager::DeleteResources << Akonadi::StandardActionManager::ResourceProperties
267                     << Akonadi::StandardActionManager::SynchronizeResources << Akonadi::StandardActionManager::SynchronizeCollectionsRecursive
268                     << Akonadi::StandardActionManager::MoveItemToMenu << Akonadi::StandardActionManager::CopyItemToMenu;
269 
270     for (Akonadi::StandardActionManager::Type standardAction : std::as_const(standardActions)) {
271         mActionManager->createAction(standardAction);
272     }
273     guiClient->actionCollection()->setDefaultShortcut(mActionManager->action(Akonadi::StandardActionManager::DeleteItems), QKeySequence(Qt::Key_Delete));
274     QList<Akonadi::StandardContactActionManager::Type> contactActions;
275     contactActions << Akonadi::StandardContactActionManager::CreateContact << Akonadi::StandardContactActionManager::CreateContactGroup
276                    << Akonadi::StandardContactActionManager::EditItem;
277 
278     for (Akonadi::StandardContactActionManager::Type contactAction : std::as_const(contactActions)) {
279         mActionManager->createAction(contactAction);
280     }
281 
282     mActionManager->interceptAction(Akonadi::StandardActionManager::CollectionProperties);
283     connect(mActionManager->action(Akonadi::StandardActionManager::CollectionProperties),
284             &QAction::triggered,
285             mManageShowCollectionProperties,
286             &ManageShowCollectionProperties::showCollectionProperties);
287 
288     connect(mItemView, qOverload<const Akonadi::Item &>(&Akonadi::EntityTreeView::currentChanged), this, &MainWidget::itemSelected);
289     connect(mItemView,
290             qOverload<const Akonadi::Item &>(&Akonadi::EntityTreeView::doubleClicked),
291             mActionManager->action(Akonadi::StandardContactActionManager::EditItem),
292             &QAction::trigger);
293     connect(mItemView->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWidget::itemSelectionChanged);
294 
295     // show the contact details view as default
296     mDetailsViewStack->setCurrentWidget(mContactDetails);
297 
298     mContactSwitcher->setView(mItemView);
299 
300     Akonadi::ControlGui::widgetNeedsAkonadi(this);
301 
302     mModelColumnManager = new ModelColumnManager(GlobalContactModel::instance()->model(), this);
303     mModelColumnManager->setWidget(mItemView->header());
304     mModelColumnManager->load();
305 
306     initializeImportExportPlugin(guiClient->actionCollection());
307     QMetaObject::invokeMethod(this, &MainWidget::delayedInit, Qt::QueuedConnection);
308     updateQuickSearchText();
309 }
310 
setFocusToTreeView()311 void MainWidget::setFocusToTreeView()
312 {
313     mItemView->setFocus();
314 }
315 
initializeImportExportPlugin(KActionCollection * collection)316 void MainWidget::initializeImportExportPlugin(KActionCollection *collection)
317 {
318     const QVector<KAddressBookImportExport::Plugin *> listPlugins = KAddressBookImportExport::PluginManager::self()->pluginsList();
319     QList<QAction *> importActions;
320     QList<QAction *> exportActions;
321     for (KAddressBookImportExport::Plugin *plugin : listPlugins) {
322         if (plugin->isEnabled()) {
323             auto interface = static_cast<KAddressBookImportExport::PluginInterface *>(plugin->createInterface(this));
324             interface->setItemSelectionModel(mItemView->selectionModel());
325             interface->setParentWidget(this);
326             interface->createAction(collection);
327             importActions.append(interface->importActions());
328             exportActions.append(interface->exportActions());
329             mImportExportPluginInterfaceList.append(interface);
330             connect(interface, &PimCommon::AbstractGenericPluginInterface::emitPluginActivated, this, &MainWidget::slotImportExportActivated);
331         }
332     }
333 
334     if (!importActions.isEmpty()) {
335         auto importMenu = new KActionMenu(i18n("Import"), this);
336         collection->addAction(QStringLiteral("import_menu"), importMenu);
337         for (QAction *act : std::as_const(importActions)) {
338             importMenu->addAction(act);
339         }
340     }
341     if (!exportActions.isEmpty()) {
342         auto exportMenu = new KActionMenu(i18n("Export"), this);
343         collection->addAction(QStringLiteral("export_menu"), exportMenu);
344         for (QAction *act : std::as_const(exportActions)) {
345             exportMenu->addAction(act);
346         }
347     }
348 }
349 
configure()350 void MainWidget::configure()
351 {
352     QPointer<KCMultiDialog> dlg = new KCMultiDialog(this);
353     const QVector<KPluginMetaData> availablePlugins = KPluginMetaData::findPlugins(QStringLiteral("pim/kcms/kaddressbook"));
354     for (const KPluginMetaData &metaData : availablePlugins) {
355         dlg->addModule(metaData);
356     }
357     dlg->exec();
358     delete dlg;
359 }
360 
handleCommandLine(const QStringList & arguments)361 void MainWidget::handleCommandLine(const QStringList &arguments)
362 {
363     QCommandLineParser parser;
364     kaddressbook_options(&parser);
365     parser.process(arguments);
366 
367     if (parser.isSet(QStringLiteral("import"))) {
368         const QStringList lst = parser.positionalArguments();
369         for (const QString &urlStr : lst) {
370             const QUrl url(QUrl::fromUserInput(urlStr));
371             for (KAddressBookImportExport::PluginInterface *interface : std::as_const(mImportExportPluginInterfaceList)) {
372                 if (interface->canImportFileType(url)) {
373                     interface->importFile(url);
374                     break;
375                 }
376             }
377         }
378     } else if (parser.isSet(QStringLiteral("newcontact"))) {
379         newContact();
380     } else if (parser.isSet(QStringLiteral("view"))) {
381         const auto url = QUrl{parser.value(QStringLiteral("view"))};
382         mPendingSelection = Akonadi::Item::fromUrl(url);
383     }
384 }
385 
updateQuickSearchText()386 void MainWidget::updateQuickSearchText()
387 {
388     mQuickSearchWidget->updateQuickSearchText(i18nc("@label Search contacts in list", "Search... <%1>", mQuickSearchAction->shortcut().toString()));
389 }
390 
delayedInit()391 void MainWidget::delayedInit()
392 {
393     setViewMode(0); // get default from settings
394 
395     const KConfigGroup group(Settings::self()->config(), "UiState_ContactView");
396     KAddressBook::UiStateSaver::restoreState(mItemView, group);
397 
398     mXmlGuiClient->actionCollection()->action(QStringLiteral("options_show_qrcodes"))->setChecked(showQRCodes());
399 
400     connect(GlobalContactModel::instance()->model(), &QAbstractItemModel::modelAboutToBeReset, this, &MainWidget::saveState);
401     connect(GlobalContactModel::instance()->model(), &QAbstractItemModel::modelReset, this, &MainWidget::restoreState);
402     connect(qApp, &QApplication::aboutToQuit, this, &MainWidget::saveState);
403 
404     restoreState();
405     updateQuickSearchText();
406     initializePluginActions();
407 }
408 
~MainWidget()409 MainWidget::~MainWidget()
410 {
411     mModelColumnManager->store();
412     saveSplitterStates();
413 
414     KConfigGroup group(Settings::self()->config(), "UiState_ContactView");
415     KAddressBook::UiStateSaver::saveState(mItemView, group);
416 
417     saveState();
418     delete mGrantleeThemeManager;
419     delete mFormatter;
420     delete mGroupFormatter;
421 
422     Settings::self()->save();
423 }
424 
restoreState()425 void MainWidget::restoreState()
426 {
427     // collection view
428     {
429         auto saver = new Akonadi::ETMViewStateSaver;
430         saver->setView(mCollectionView);
431 
432         const KConfigGroup group(Settings::self()->config(), "CollectionViewState");
433         saver->restoreState(group);
434     }
435 
436     // collection view
437     {
438         auto saver = new Akonadi::ETMViewStateSaver;
439         saver->setSelectionModel(mCollectionSelectionModel);
440 
441         const KConfigGroup group(Settings::self()->config(), "CollectionViewCheckState");
442         saver->restoreState(group);
443     }
444 
445     // item view
446     {
447         auto saver = new Akonadi::ETMViewStateSaver;
448         saver->setView(mItemView);
449         saver->setSelectionModel(mItemView->selectionModel());
450 
451         if (mPendingSelection.isValid()) {
452             saver->selectItems({mPendingSelection});
453             saver->setCurrentItem(mPendingSelection);
454             mPendingSelection = {};
455         } else {
456             const KConfigGroup group(Settings::self()->config(), "ItemViewState");
457             saver->restoreState(group);
458         }
459     }
460 }
461 
saveState()462 void MainWidget::saveState()
463 {
464     // collection view
465     {
466         Akonadi::ETMViewStateSaver saver;
467         saver.setView(mCollectionView);
468 
469         KConfigGroup group(Settings::self()->config(), "CollectionViewState");
470         saver.saveState(group);
471         group.sync();
472     }
473 
474     // collection view
475     {
476         Akonadi::ETMViewStateSaver saver;
477         saver.setSelectionModel(mCollectionSelectionModel);
478 
479         KConfigGroup group(Settings::self()->config(), "CollectionViewCheckState");
480         saver.saveState(group);
481         group.sync();
482     }
483 
484     // item view
485     {
486         Akonadi::ETMViewStateSaver saver;
487         saver.setView(mItemView);
488         saver.setSelectionModel(mItemView->selectionModel());
489 
490         KConfigGroup group(Settings::self()->config(), "ItemViewState");
491         saver.saveState(group);
492         group.sync();
493     }
494 }
495 
setupGui()496 void MainWidget::setupGui()
497 {
498     // the horizontal main layout
499     auto layout = new QHBoxLayout(this);
500     layout->setContentsMargins({});
501 
502     // Splitter 1 contains the two main parts of the GUI:
503     //  - collection and item view splitter 2 on the left (see below)
504     //  - details pane on the right, that contains
505     //   - details view stack on the top
506     //   - contact switcher at the bottom
507     mMainWidgetSplitter1 = new QSplitter(Qt::Horizontal);
508     mMainWidgetSplitter1->setObjectName(QStringLiteral("MainWidgetSplitter1"));
509     layout->addWidget(mMainWidgetSplitter1);
510 
511     // Splitter 2 contains the remaining parts of the GUI:
512     //  - collection view on either the left or the top
513     //  - item view on either the right or the bottom
514     // The orientation of this splitter is changed for either
515     // a three or two column view;  in simple mode it is hidden.
516     mMainWidgetSplitter2 = new QSplitter(Qt::Vertical);
517     mMainWidgetSplitter2->setObjectName(QStringLiteral("MainWidgetSplitter2"));
518     mMainWidgetSplitter1->addWidget(mMainWidgetSplitter2);
519 
520     // the collection view
521     mCollectionView = new Akonadi::EntityTreeView();
522     mMainWidgetSplitter2->addWidget(mCollectionView);
523 
524     // the items view
525     mItemView = new Akonadi::EntityTreeView();
526     mItemView->setObjectName(QStringLiteral("ContactView"));
527     mItemView->setDefaultPopupMenu(QStringLiteral("akonadi_itemview_contextmenu"));
528     mItemView->setAlternatingRowColors(true);
529     mMainWidgetSplitter2->addWidget(mItemView);
530 
531     // the details pane that contains the details view stack and contact switcher
532     mDetailsPane = new QWidget;
533     mMainWidgetSplitter1->addWidget(mDetailsPane);
534 
535     mMainWidgetSplitter1->setStretchFactor(1, 9); // maximum width for detail
536     mMainWidgetSplitter2->setStretchFactor(1, 9); // for intuitive resizing
537     mMainWidgetSplitter2->setChildrenCollapsible(false);
538     mMainWidgetSplitter1->setChildrenCollapsible(false);
539 
540     auto detailsPaneLayout = new QVBoxLayout(mDetailsPane);
541     detailsPaneLayout->setContentsMargins({});
542 
543     // the details view stack
544     mDetailsViewStack = new QStackedWidget();
545     detailsPaneLayout->addWidget(mDetailsViewStack);
546 
547     // the details widget for contacts
548     mContactDetails = new Akonadi::ContactViewer(mDetailsViewStack);
549     mDetailsViewStack->addWidget(mContactDetails);
550 
551     // the details widget for contact groups
552     mContactGroupDetails = new Akonadi::ContactGroupViewer(mDetailsViewStack);
553     mDetailsViewStack->addWidget(mContactGroupDetails);
554 
555     // the details widget for empty items
556     mEmptyDetails = new QTextBrowser(mDetailsViewStack);
557     mDetailsViewStack->addWidget(mEmptyDetails);
558 
559     // the contact switcher for the simple gui mode
560     mContactSwitcher = new ContactSwitcher;
561     detailsPaneLayout->addWidget(mContactSwitcher);
562     mContactSwitcher->setVisible(false);
563 
564     // the quick search widget which is embedded in the toolbar action
565     mQuickSearchWidget = new QuickSearchWidget;
566     mQuickSearchWidget->setMaximumWidth(500);
567 
568     // the category filter widget which is embedded in the toolbar action
569     mCategorySelectWidget = new CategorySelectWidget;
570 
571     // setup the default actions
572     auto actions = new Akonadi::ContactDefaultActions(this);
573     actions->connectToView(mContactDetails);
574     actions->connectToView(mContactGroupDetails);
575     mFormatter = new KAddressBookGrantlee::GrantleeContactFormatter;
576     mFormatter->setApplicationDomain("kaddressbook");
577 
578     mContactDetails->setContactFormatter(mFormatter);
579 
580     mGroupFormatter = new KAddressBookGrantlee::GrantleeContactGroupFormatter;
581 
582     mContactGroupDetails->setContactGroupFormatter(mGroupFormatter);
583 }
584 
initializePluginActions()585 void MainWidget::initializePluginActions()
586 {
587     KAddressBookPluginInterface::self()->initializePluginActions(QStringLiteral("kaddressbook"), mXmlGuiClient);
588 }
589 
slotImportExportActivated(PimCommon::AbstractGenericPluginInterface * interface)590 void MainWidget::slotImportExportActivated(PimCommon::AbstractGenericPluginInterface *interface)
591 {
592     auto importExportInterface = static_cast<KAddressBookImportExport::PluginInterface *>(interface);
593     if (importExportInterface) {
594         importExportInterface->exec();
595     }
596 }
597 
setupActions(KActionCollection * collection)598 void MainWidget::setupActions(KActionCollection *collection)
599 {
600     KAddressBookPluginInterface::self()->setParentWidget(this);
601     KAddressBookPluginInterface::self()->setMainWidget(this);
602     KAddressBookPluginInterface::self()->createPluginInterface();
603 
604     mGrantleeThemeManager = new GrantleeTheme::ThemeManager(QStringLiteral("addressbook"),
605                                                             QStringLiteral("theme.desktop"),
606                                                             collection,
607                                                             QStringLiteral("kaddressbook/viewertemplates/"),
608                                                             this);
609     mGrantleeThemeManager->setDownloadNewStuffConfigFile(QStringLiteral(":/knsrc/data/kaddressbook_themes.knsrc"));
610     connect(mGrantleeThemeManager, &GrantleeTheme::ThemeManager::grantleeThemeSelected, this, &MainWidget::slotGrantleeThemeSelected);
611     connect(mGrantleeThemeManager, &GrantleeTheme::ThemeManager::updateThemes, this, &MainWidget::slotGrantleeThemesUpdated);
612 
613     auto themeMenu = new KActionMenu(i18n("&Themes"), this);
614     collection->addAction(QStringLiteral("theme_menu"), themeMenu);
615 
616     initGrantleeThemeName();
617     auto group = new QActionGroup(this);
618     mGrantleeThemeManager->setThemeMenu(themeMenu);
619     mGrantleeThemeManager->setActionGroup(group);
620 
621     QAction *action = KStandardAction::print(this, &MainWidget::print, collection);
622     action->setWhatsThis(i18nc("@info:whatsthis", "Print the complete address book or a selected number of contacts."));
623 
624     KStandardAction::printPreview(this, &MainWidget::printPreview, collection);
625 
626     auto quicksearch = new QWidgetAction(this);
627     quicksearch->setText(i18n("Quick search"));
628     quicksearch->setDefaultWidget(mQuickSearchWidget);
629     collection->addAction(QStringLiteral("quick_search"), quicksearch);
630 
631     auto categoryFilter = new QWidgetAction(this);
632     categoryFilter->setText(i18n("Category filter"));
633     categoryFilter->setDefaultWidget(mCategorySelectWidget);
634     collection->addAction(QStringLiteral("category_filter"), categoryFilter);
635 
636     action = collection->addAction(QStringLiteral("select_all"));
637     action->setText(i18n("Select All"));
638     collection->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_A));
639     action->setWhatsThis(i18n("Select all contacts in the current address book view."));
640     connect(action, &QAction::triggered, mItemView, &Akonadi::EntityTreeView::selectAll);
641 
642     auto qrtoggleAction = collection->add<KToggleAction>(QStringLiteral("options_show_qrcodes"));
643     qrtoggleAction->setText(i18n("Show QR Codes"));
644     qrtoggleAction->setWhatsThis(i18n("Show QR Codes in the contact."));
645     connect(qrtoggleAction, &KToggleAction::toggled, this, &MainWidget::setQRCodeShow);
646 
647     mViewModeGroup = new QActionGroup(this);
648 
649     auto act = new QAction(i18nc("@action:inmenu", "Simple (one column)"), mViewModeGroup);
650     act->setCheckable(true);
651     act->setData(1);
652     collection->setDefaultShortcut(act, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_1));
653     act->setWhatsThis(i18n("Show a simple mode of the address book view."));
654     collection->addAction(QStringLiteral("view_mode_simple"), act);
655 
656     act = new QAction(i18nc("@action:inmenu", "Two Columns"), mViewModeGroup);
657     act->setCheckable(true);
658     act->setData(2);
659     collection->addAction(QStringLiteral("view_mode_2columns"), act);
660     collection->setDefaultShortcut(act, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_2));
661 
662     act = new QAction(i18nc("@action:inmenu", "Three Columns"), mViewModeGroup);
663     act->setCheckable(true);
664     act->setData(3);
665     collection->setDefaultShortcut(act, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_3));
666     collection->addAction(QStringLiteral("view_mode_3columns"), act);
667 
668     connect(mViewModeGroup, &QActionGroup::triggered, this, &MainWidget::setActivateViewMode);
669 
670     KToggleAction *actTheme = mGrantleeThemeManager->actionForTheme();
671     if (actTheme) {
672         actTheme->setChecked(true);
673     }
674 
675     mQuickSearchAction = new QAction(i18n("Set Focus to Quick Search"), this);
676     // If change shortcut change in quicksearchwidget->lineedit->setPlaceholderText
677     collection->addAction(QStringLiteral("focus_to_quickseach"), mQuickSearchAction);
678     connect(mQuickSearchAction, &QAction::triggered, mQuickSearchWidget, &QuickSearchWidget::slotFocusQuickSearch);
679     collection->setDefaultShortcut(mQuickSearchAction, QKeySequence(Qt::ALT | Qt::Key_Q));
680 
681     if (!qEnvironmentVariableIsEmpty("KDEPIM_DEBUGGING")) {
682         action = collection->addAction(QStringLiteral("debug_akonadi_search"));
683         // Don't translate it. It's just for debug
684         action->setText(QStringLiteral("Debug Akonadi Search..."));
685         connect(action, &QAction::triggered, this, &MainWidget::slotDebugAkonadiSearch);
686     }
687 
688     mServerSideSubscription = new QAction(QIcon::fromTheme(QStringLiteral("folder-bookmarks")), i18n("Serverside Subscription..."), this);
689     collection->addAction(QStringLiteral("serverside_subscription"), mServerSideSubscription);
690     connect(mServerSideSubscription, &QAction::triggered, this, &MainWidget::slotServerSideSubscription);
691 }
692 
printPreview()693 void MainWidget::printPreview()
694 {
695     QPrinter printer;
696     printer.setDocName(i18n("Address Book"));
697     printer.setOutputFileName(Settings::self()->defaultFileName());
698     printer.setOutputFormat(QPrinter::PdfFormat);
699     printer.setCollateCopies(true);
700 
701     QPointer<PimCommon::KPimPrintPreviewDialog> previewdlg = new PimCommon::KPimPrintPreviewDialog(&printer, this);
702     KABPrinting::PrintingWizard wizard(&printer, mItemView->selectionModel(), this);
703     wizard.setDefaultAddressBook(currentAddressBook());
704     connect(previewdlg.data(), &QPrintPreviewDialog::paintRequested, this, [&wizard]() {
705         wizard.print();
706     });
707 
708     const int result = wizard.exec();
709     if (result) {
710         Settings::self()->setDefaultFileName(printer.outputFileName());
711         Settings::self()->setPrintingStyle(wizard.printingStyle());
712         Settings::self()->setSortOrder(wizard.sortOrder());
713         previewdlg->exec();
714     }
715     delete previewdlg;
716 }
717 
print()718 void MainWidget::print()
719 {
720     QPrinter printer;
721     printer.setDocName(i18n("Address Book"));
722     printer.setOutputFileName(Settings::self()->defaultFileName());
723     printer.setCollateCopies(true);
724 
725     QPointer<QPrintDialog> printDialog = new QPrintDialog(&printer, this);
726 
727     printDialog->setWindowTitle(i18nc("@title:window", "Print Contacts"));
728     if (!printDialog->exec() || !printDialog) {
729         delete printDialog;
730         return;
731     }
732     KABPrinting::PrintingWizard wizard(&printer, mItemView->selectionModel(), this);
733     wizard.setDefaultAddressBook(currentAddressBook());
734 
735     wizard.exec(); // krazy:exclude=crashy
736 
737     Settings::self()->setDefaultFileName(printer.outputFileName());
738     Settings::self()->setPrintingStyle(wizard.printingStyle());
739     Settings::self()->setSortOrder(wizard.sortOrder());
740 }
741 
newContact()742 void MainWidget::newContact()
743 {
744     mActionManager->action(Akonadi::StandardContactActionManager::CreateContact)->trigger();
745 }
746 
newGroup()747 void MainWidget::newGroup()
748 {
749     mActionManager->action(Akonadi::StandardContactActionManager::CreateContactGroup)->trigger();
750 }
751 
752 /**
753  * Depending on the mime type of the selected item, this method
754  * brings up the right view on the detail view stack and sets the
755  * selected item on it.
756  */
itemSelected(const Akonadi::Item & item)757 void MainWidget::itemSelected(const Akonadi::Item &item)
758 {
759     if (Akonadi::MimeTypeChecker::isWantedItem(item, KContacts::Addressee::mimeType())) {
760         mDetailsViewStack->setCurrentWidget(mContactDetails);
761         mContactDetails->setContact(item);
762     } else if (Akonadi::MimeTypeChecker::isWantedItem(item, KContacts::ContactGroup::mimeType())) {
763         mDetailsViewStack->setCurrentWidget(mContactGroupDetails);
764         mContactGroupDetails->setContactGroup(item);
765     }
766 }
767 
768 /**
769  * Catch when the selection has gone ( e.g. an empty address book has been selected )
770  * clear the details view in this case.
771  */
itemSelectionChanged(const QModelIndex & current,const QModelIndex &)772 void MainWidget::itemSelectionChanged(const QModelIndex &current, const QModelIndex &)
773 {
774     if (!current.isValid()) {
775         mDetailsViewStack->setCurrentWidget(mEmptyDetails);
776     }
777 }
778 
selectFirstItem()779 void MainWidget::selectFirstItem()
780 {
781     // Whenever the quick search has changed, we select the first item
782     // in the item view, so that the detailsview is updated
783     if (mItemView && mItemView->selectionModel()) {
784         mItemView->selectionModel()->setCurrentIndex(mItemView->model()->index(0, 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect);
785     }
786 }
787 
showQRCodes()788 bool MainWidget::showQRCodes()
789 {
790     KConfig config(QStringLiteral("akonadi_contactrc"));
791     KConfigGroup group(&config, QStringLiteral("View"));
792     return group.readEntry("QRCodes", true);
793 }
794 
setQRCodeShow(bool on)795 void MainWidget::setQRCodeShow(bool on)
796 {
797     // must write the configuration setting first before updating the view.
798     KConfig config(QStringLiteral("akonadi_contactrc"));
799     KConfigGroup group(&config, QStringLiteral("View"));
800     group.writeEntry("QRCodes", on);
801     group.sync();
802     if (mDetailsViewStack->currentWidget() == mContactDetails) {
803         mFormatter->setShowQRCode(on);
804         mContactDetails->setShowQRCode(on);
805     }
806 }
807 
selectedItems()808 Akonadi::Item::List MainWidget::selectedItems()
809 {
810     Akonadi::Item::List items;
811     QPointer<KAddressBookImportExport::ContactSelectionDialog> dlg =
812         new KAddressBookImportExport::ContactSelectionDialog(mItemView->selectionModel(), false, this);
813     dlg->setDefaultAddressBook(currentAddressBook());
814     if (!dlg->exec() || !dlg) {
815         delete dlg;
816         return items;
817     }
818 
819     items = dlg->selectedItems();
820     delete dlg;
821 
822     return items;
823 }
824 
currentAddressBook() const825 Akonadi::Collection MainWidget::currentAddressBook() const
826 {
827     if (mCollectionView->selectionModel() && mCollectionView->selectionModel()->hasSelection()) {
828         const QModelIndex index = mCollectionView->selectionModel()->selectedIndexes().first();
829         const auto collection = index.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
830 
831         return collection;
832     }
833 
834     return Akonadi::Collection();
835 }
836 
allContactsModel()837 QAbstractItemModel *MainWidget::allContactsModel()
838 {
839     if (!mAllContactsModel) {
840         auto descendantsModel = new KDescendantsProxyModel(this);
841         descendantsModel->setSourceModel(GlobalContactModel::instance()->model());
842 
843         mAllContactsModel = new Akonadi::EntityMimeTypeFilterModel(this);
844         mAllContactsModel->setSourceModel(descendantsModel);
845         mAllContactsModel->addMimeTypeExclusionFilter(Akonadi::Collection::mimeType());
846         mAllContactsModel->setHeaderGroup(Akonadi::EntityTreeModel::ItemListHeaders);
847     }
848 
849     return mAllContactsModel;
850 }
851 
setActivateViewMode(QAction * action)852 void MainWidget::setActivateViewMode(QAction *action)
853 {
854     setViewMode(action->data().toInt());
855 }
856 
setViewMode(int mode)857 void MainWidget::setViewMode(int mode)
858 {
859     int currentMode = Settings::self()->viewMode();
860     // qCDebug(KADDRESSBOOK_LOG) << "cur" << currentMode << "new" << mode;
861     if (mode == currentMode) {
862         return; // nothing to do
863     }
864 
865     if (mode == 0) {
866         mode = currentMode; // initialization, no save
867     } else {
868         saveSplitterStates(); // for 2- or 3-column mode
869     }
870     if (mode == 1) { // simple mode
871         mMainWidgetSplitter2->setVisible(false);
872         mDetailsPane->setVisible(true);
873         mContactSwitcher->setVisible(true);
874     } else {
875         mMainWidgetSplitter2->setVisible(true);
876         mContactSwitcher->setVisible(false);
877 
878         if (mode == 2) { // 2 columns
879             mMainWidgetSplitter2->setOrientation(Qt::Vertical);
880         } else if (mode == 3) { // 3 columns
881             mMainWidgetSplitter2->setOrientation(Qt::Horizontal);
882         }
883     }
884 
885     Settings::self()->setViewMode(mode); // save new mode in settings
886     restoreSplitterStates(); // restore state for new mode
887     mViewModeGroup->actions().at(mode - 1)->setChecked(true);
888 
889     if (mItemView->model()) {
890         mItemView->setCurrentIndex(mItemView->model()->index(0, 0));
891     }
892 }
893 
saveSplitterStates() const894 void MainWidget::saveSplitterStates() const
895 {
896     // The splitter states are saved separately for each column view mode,
897     // but only if not in simple mode (1 column).
898     int currentMode = Settings::self()->viewMode();
899     if (currentMode == 1) {
900         return;
901     }
902 
903     const QString groupName = QStringLiteral("UiState_MainWidgetSplitter_%1").arg(currentMode);
904     // qCDebug(KADDRESSBOOK_LOG) << "saving to group" << groupName;
905     KConfigGroup group(Settings::self()->config(), groupName);
906     KAddressBook::UiStateSaver::saveState(mMainWidgetSplitter1, group);
907     KAddressBook::UiStateSaver::saveState(mMainWidgetSplitter2, group);
908 }
909 
restoreSplitterStates()910 void MainWidget::restoreSplitterStates()
911 {
912     // The splitter states are restored as appropriate for the current
913     // column view mode, but not for simple mode (1 column).
914     int currentMode = Settings::self()->viewMode();
915     if (currentMode == 1) {
916         return;
917     }
918 
919     const QString groupName = QStringLiteral("UiState_MainWidgetSplitter_%1").arg(currentMode);
920     // qCDebug(KADDRESSBOOK_LOG) << "restoring from group" << groupName;
921     KConfigGroup group(Settings::self()->config(), groupName);
922     KAddressBook::UiStateSaver::restoreState(mMainWidgetSplitter1, group);
923     KAddressBook::UiStateSaver::restoreState(mMainWidgetSplitter2, group);
924 }
925 
initGrantleeThemeName()926 void MainWidget::initGrantleeThemeName()
927 {
928     QString themeName = mGrantleeThemeManager->configuredThemeName();
929     if (themeName.isEmpty()) {
930         themeName = QStringLiteral("default");
931     }
932     mFormatter->setGrantleeTheme(mGrantleeThemeManager->theme(themeName));
933     mGroupFormatter->setGrantleeTheme(mGrantleeThemeManager->theme(themeName));
934 }
935 
slotGrantleeThemeSelected()936 void MainWidget::slotGrantleeThemeSelected()
937 {
938     initGrantleeThemeName();
939     if (mItemView->model()) {
940         mItemView->setCurrentIndex(mItemView->model()->index(0, 0));
941     }
942 }
943 
slotGrantleeThemesUpdated()944 void MainWidget::slotGrantleeThemesUpdated()
945 {
946     if (mItemView->model()) {
947         mItemView->setCurrentIndex(mItemView->model()->index(0, 0));
948     }
949 }
950 
entityTreeModel() const951 Akonadi::EntityTreeModel *MainWidget::entityTreeModel() const
952 {
953     auto proxy = qobject_cast<QAbstractProxyModel *>(mCollectionView->model());
954     while (proxy) {
955         auto etm = qobject_cast<Akonadi::EntityTreeModel *>(proxy->sourceModel());
956         if (etm) {
957             return etm;
958         }
959         proxy = qobject_cast<QAbstractProxyModel *>(proxy->sourceModel());
960     }
961 
962     qCWarning(KADDRESSBOOK_LOG) << "Couldn't find EntityTreeModel";
963     return nullptr;
964 }
965 
slotCheckNewCalendar(const QModelIndex & parent,int begin,int end)966 void MainWidget::slotCheckNewCalendar(const QModelIndex &parent, int begin, int end)
967 {
968     // HACK: Check newly created calendars
969 
970     if (begin < end) {
971         return;
972     }
973 
974     Akonadi::EntityTreeModel *etm = entityTreeModel();
975     QAbstractItemModel *model = mCollectionView->model();
976     if (etm && etm->isCollectionTreeFetched()) {
977         for (int row = begin; row <= end; ++row) {
978             QModelIndex index = model->index(row, 0, parent);
979             if (index.isValid()) {
980                 model->setData(index, Qt::Checked, Qt::CheckStateRole);
981                 slotCheckNewCalendar(index, 0, model->rowCount(index) - 1);
982             }
983         }
984         if (parent.isValid()) {
985             mCollectionView->setExpanded(parent, true);
986         }
987     }
988 }
989 
collectSelectedAllContactsItem()990 const Akonadi::Item::List MainWidget::collectSelectedAllContactsItem()
991 {
992     return collectSelectedAllContactsItem(mItemView->selectionModel());
993 }
994 
slotDebugAkonadiSearch()995 void MainWidget::slotDebugAkonadiSearch()
996 {
997     const Akonadi::Item::List lst = collectSelectedAllContactsItem(mItemView->selectionModel());
998     if (lst.isEmpty()) {
999         return;
1000     }
1001     QPointer<Akonadi::Search::AkonadiSearchDebugDialog> dlg = new Akonadi::Search::AkonadiSearchDebugDialog;
1002     dlg->setAkonadiId(lst.at(0).id());
1003     dlg->setAttribute(Qt::WA_DeleteOnClose);
1004     dlg->setSearchType(Akonadi::Search::AkonadiSearchDebugSearchPathComboBox::Contacts);
1005     dlg->doSearch();
1006     dlg->show();
1007 }
1008 
collectSelectedAllContactsItem(QItemSelectionModel * model)1009 const Akonadi::Item::List MainWidget::collectSelectedAllContactsItem(QItemSelectionModel *model)
1010 {
1011     Akonadi::Item::List lst;
1012 
1013     const QModelIndexList indexes = model->selectedRows(0);
1014     for (int i = 0; i < indexes.count(); ++i) {
1015         const QModelIndex index = indexes.at(i);
1016         if (index.isValid()) {
1017             const auto item = index.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
1018             if (item.isValid()) {
1019                 if (item.hasPayload<KContacts::Addressee>() || item.hasPayload<KContacts::ContactGroup>()) {
1020                     lst.append(item);
1021                 }
1022             }
1023         }
1024     }
1025     return lst;
1026 }
1027 
slotServerSideSubscription()1028 void MainWidget::slotServerSideSubscription()
1029 {
1030     Akonadi::Collection collection = currentAddressBook();
1031     if (collection.isValid()) {
1032         auto job = new PimCommon::ManageServerSideSubscriptionJob(this);
1033         job->setCurrentCollection(collection);
1034         job->setParentWidget(this);
1035         job->start();
1036     }
1037 }
1038 
slotCurrentCollectionChanged(const Akonadi::Collection & col)1039 void MainWidget::slotCurrentCollectionChanged(const Akonadi::Collection &col)
1040 {
1041     for (auto interface : std::as_const(mImportExportPluginInterfaceList)) {
1042         interface->setDefaultCollection(col);
1043     }
1044     bool isOnline;
1045     mServerSideSubscription->setEnabled(PimCommon::MailUtil::isImapFolder(col, isOnline));
1046 }
1047