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 ¤t, 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