File indexing completed on 2024-05-19 05:14:37

0001 /*
0002   This file is part of KAddressBook.
0003 
0004   SPDX-FileCopyrightText: 2007 Tobias Koenig <tokoe@kde.org>
0005 
0006   SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "mainwidget.h"
0010 
0011 #include "categoryfilterproxymodel.h"
0012 #include "categoryselectwidget.h"
0013 #include "contactinfoproxymodel.h"
0014 #include "contactswitcher.h"
0015 #include "globalcontactmodel.h"
0016 #include "kaddressbook_options.h"
0017 #include "kaddressbookadaptor.h"
0018 #include "manageshowcollectionproperties.h"
0019 #include "modelcolumnmanager.h"
0020 #include "printing/printingwizard.h"
0021 #include "settings.h"
0022 #include "stylecontactlistdelegate.h"
0023 #include "widgets/quicksearchwidget.h"
0024 
0025 #include "importexport/contactselectiondialog.h"
0026 #include "importexport/plugin.h"
0027 #include "importexport/plugininterface.h"
0028 #include "importexport/pluginmanager.h"
0029 
0030 #include <Akonadi/GrantleeContactFormatter>
0031 #include <Akonadi/GrantleeContactGroupFormatter>
0032 #include <GrantleeTheme/GrantleeThemeManager>
0033 
0034 #include "uistatesaver.h"
0035 
0036 #include <PimCommonAkonadi/ImapAclAttribute>
0037 #include <PimCommonAkonadi/MailUtil>
0038 
0039 #include <Akonadi/AttributeFactory>
0040 #include <Akonadi/CollectionFilterProxyModel>
0041 #include <Akonadi/CollectionMaintenancePage>
0042 #include <Akonadi/CollectionPropertiesDialog>
0043 #include <Akonadi/ControlGui>
0044 #include <Akonadi/ETMViewStateSaver>
0045 #include <Akonadi/EntityMimeTypeFilterModel>
0046 #include <Akonadi/EntityTreeView>
0047 #include <Akonadi/MimeTypeChecker>
0048 #ifndef FORCE_DISABLE_AKONADI_SEARCH
0049 #include <Debug/akonadisearchdebugdialog.h>
0050 #endif
0051 #include <KContacts/Addressee>
0052 #include <PimCommon/PimUtil>
0053 #include <PimCommonAkonadi/ManageServerSideSubscriptionJob>
0054 #include <QPointer>
0055 
0056 #include <Akonadi/ContactGroupEditorDialog>
0057 #include <Akonadi/ContactGroupViewer>
0058 #include <Akonadi/ContactViewer>
0059 #include <Akonadi/ContactsFilterProxyModel>
0060 #include <Akonadi/ContactsTreeModel>
0061 #include <Akonadi/StandardContactActionManager>
0062 
0063 #include "kaddressbook_debug.h"
0064 #include <KActionCollection>
0065 #include <KActionMenu>
0066 #include <KCMultiDialog>
0067 #include <KCheckableProxyModel>
0068 #include <KColorSchemeMenu>
0069 #include <KContacts/ContactGroup>
0070 #include <KDescendantsProxyModel>
0071 #include <KLocalizedString>
0072 #include <KPluginMetaData>
0073 #include <KSelectionProxyModel>
0074 #include <KToggleAction>
0075 #include <KXMLGUIClient>
0076 #include <QAction>
0077 #include <QApplication>
0078 #include <QTextBrowser>
0079 
0080 #include <Akonadi/ItemModifyJob>
0081 #include <KColorSchemeManager>
0082 #include <KWindowStateSaver>
0083 #include <QActionGroup>
0084 #include <QDBusConnection>
0085 #include <QDesktopServices>
0086 #include <QHBoxLayout>
0087 #include <QHeaderView>
0088 #include <QPrintDialog>
0089 #include <QPrintPreviewDialog>
0090 #include <QPrinter>
0091 #include <QSplitter>
0092 #include <QStackedWidget>
0093 
0094 #include "plugininterface/kaddressbookplugininterface.h"
0095 
0096 namespace
0097 {
0098 static bool isStructuralCollection(const Akonadi::Collection &collection)
0099 {
0100     const QStringList mimeTypes = {KContacts::Addressee::mimeType(), KContacts::ContactGroup::mimeType()};
0101     const QStringList collectionMimeTypes = collection.contentMimeTypes();
0102     for (const QString &mimeType : mimeTypes) {
0103         if (collectionMimeTypes.contains(mimeType)) {
0104             return false;
0105         }
0106     }
0107     return true;
0108 }
0109 
0110 class StructuralCollectionsNotCheckableProxy : public KCheckableProxyModel
0111 {
0112 public:
0113     explicit StructuralCollectionsNotCheckableProxy(QObject *parent)
0114         : KCheckableProxyModel(parent)
0115     {
0116     }
0117 
0118     [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override
0119     {
0120         if (!index.isValid()) {
0121             return {};
0122         }
0123 
0124         if (role == Qt::CheckStateRole) {
0125             // Don't show the checkbox if the collection can't contain incidences
0126             const auto collection = index.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
0127             if (collection.isValid() && isStructuralCollection(collection)) {
0128                 return {};
0129             }
0130         }
0131         return KCheckableProxyModel::data(index, role);
0132     }
0133 };
0134 }
0135 
0136 MainWidget::MainWidget(KXMLGUIClient *guiClient, QWidget *parent)
0137     : QWidget(parent)
0138     , mXmlGuiClient(guiClient)
0139 {
0140     (void)new KaddressbookAdaptor(this);
0141     QDBusConnection::sessionBus().registerObject(QStringLiteral("/KAddressBook"), this);
0142 
0143     mManageShowCollectionProperties = new ManageShowCollectionProperties(this, this);
0144 
0145     Akonadi::AttributeFactory::registerAttribute<PimCommon::ImapAclAttribute>();
0146 
0147     KAddressBookPluginInterface::self()->setActionCollection(guiClient->actionCollection());
0148     KAddressBookPluginInterface::self()->initializePlugins();
0149     setupGui();
0150     setupActions(guiClient->actionCollection());
0151 
0152     /*
0153      *  The item models, proxies and views have the following structure:
0154      *
0155      *                               mItemView
0156      *                                   ^
0157      *                                   |
0158      *                           mContactsFilterModel
0159      *                                   ^
0160      *                                   |
0161      * mCategorySelectWidget --> mCategoryFilterModel
0162      *                                   ^
0163      *                                   |
0164      *                               mItemTree
0165      *                                   ^
0166      *                                   |
0167      *                                   |           mAllContactsModel
0168      *                                   |                  ^
0169      *                                   |                  |
0170      *      mCollectionView     selectionProxyModel  descendantsModel
0171      *            ^               ^      ^                  ^
0172      *            |               |      |                  |
0173      *            |       selectionModel |                  |
0174      *            |               |      |                  |
0175      *        proxyModel ---------'      |                  |
0176      *            ^                      |                  |
0177      *            |                      |                  |
0178      *      mCollectionTree              |                  |
0179      *            ^                      |                  |
0180      *            |                      |   _______________/
0181      *             \                    /  /
0182      *            GlobalContactModel::instance()
0183      *
0184      *
0185      *  GlobalContactModel::instance():  The global contact model (contains collections and items)
0186      *                 mCollectionTree:  Filters out all items
0187      *                      proxyModel:  Allows the user to select collections by checkboxes
0188      *                  selectionModel:  Represents the selected collections that have been
0189      *                                   selected in proxyModel
0190      *                 mCollectionView:  Shows the collections (address books) in a view
0191      *             selectionProxyModel:  Filters out all collections and items that are no children
0192      *                                   of the collections currently selected in selectionModel
0193      *                       mItemTree:  Filters out all collections
0194      *           mCategorySelectWidget:  Selects a list of categories for filtering
0195      *            mCategoryFilterModel:  Filters the contacts by the selected categories
0196      *            mContactsFilterModel:  Filters the contacts by the content of mQuickSearchWidget
0197      *                       mItemView:  Shows the items (contacts and contact groups) in a view
0198      *
0199      *                descendantsModel:  Flattens the item/collection tree to a list
0200      *               mAllContactsModel:  Provides a list of all available contacts from all
0201      *                                   address books
0202      */
0203 
0204     mCollectionTree = new Akonadi::EntityMimeTypeFilterModel(this);
0205     mCollectionTree->setDynamicSortFilter(true);
0206     mCollectionTree->setSortCaseSensitivity(Qt::CaseInsensitive);
0207     mCollectionTree->setSourceModel(GlobalContactModel::instance()->model());
0208     mCollectionTree->addMimeTypeInclusionFilter(Akonadi::Collection::mimeType());
0209     mCollectionTree->setHeaderGroup(Akonadi::EntityTreeModel::CollectionTreeHeaders);
0210 
0211     mCollectionSelectionModel = new QItemSelectionModel(mCollectionTree);
0212     auto checkableProxyModel = new StructuralCollectionsNotCheckableProxy(this);
0213     checkableProxyModel->setSelectionModel(mCollectionSelectionModel);
0214     checkableProxyModel->setSourceModel(mCollectionTree);
0215 
0216     mCollectionView->setModel(checkableProxyModel);
0217     mCollectionView->setXmlGuiClient(guiClient);
0218     mCollectionView->header()->setDefaultAlignment(Qt::AlignCenter);
0219     mCollectionView->header()->setSortIndicatorShown(false);
0220 
0221     connect(mCollectionView->model(), &QAbstractItemModel::rowsInserted, this, &MainWidget::slotCheckNewCalendar);
0222 
0223     connect(mCollectionView, qOverload<const Akonadi::Collection &>(&Akonadi::EntityTreeView::currentChanged), this, &MainWidget::slotCurrentCollectionChanged);
0224 
0225     auto selectionProxyModel = new KSelectionProxyModel(mCollectionSelectionModel, this);
0226     selectionProxyModel->setSourceModel(GlobalContactModel::instance()->model());
0227     selectionProxyModel->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection);
0228 
0229     mItemTree = new Akonadi::EntityMimeTypeFilterModel(this);
0230     mItemTree->setSourceModel(selectionProxyModel);
0231     mItemTree->addMimeTypeExclusionFilter(Akonadi::Collection::mimeType());
0232     mItemTree->setHeaderGroup(Akonadi::EntityTreeModel::ItemListHeaders);
0233 
0234     mCategoryFilterModel = new CategoryFilterProxyModel(this);
0235     mCategoryFilterModel->setSourceModel(mItemTree);
0236     mCategoryFilterModel->setFilterCategories(mCategorySelectWidget->filterTags());
0237     mCategoryFilterModel->setFilterEnabled(true);
0238 
0239     connect(mCategorySelectWidget, &CategorySelectWidget::filterChanged, mCategoryFilterModel, &CategoryFilterProxyModel::setFilterCategories);
0240 
0241     mContactsFilterModel = new Akonadi::ContactsFilterProxyModel(this);
0242     mContactsFilterModel->setSourceModel(mCategoryFilterModel);
0243 
0244     auto contactInfoProxyModel = new ContactInfoProxyModel(this);
0245     contactInfoProxyModel->setSourceModel(mContactsFilterModel);
0246 
0247     connect(mQuickSearchWidget, &QuickSearchWidget::filterStringChanged, mContactsFilterModel, &Akonadi::ContactsFilterProxyModel::setFilterString);
0248     connect(mQuickSearchWidget, &QuickSearchWidget::filterStringChanged, this, &MainWidget::selectFirstItem);
0249     connect(mQuickSearchWidget, &QuickSearchWidget::arrowDownKeyPressed, this, &MainWidget::setFocusToTreeView);
0250     mItemView->setModel(contactInfoProxyModel);
0251     mItemView->setItemDelegate(new StyleContactListDelegate(this));
0252     mItemView->setXmlGuiClient(guiClient);
0253     mItemView->setSelectionMode(QAbstractItemView::ExtendedSelection);
0254     mItemView->setRootIsDecorated(false);
0255     mItemView->header()->setDefaultAlignment(Qt::AlignCenter);
0256 
0257     mActionManager = new Akonadi::StandardContactActionManager(guiClient->actionCollection(), this);
0258     mActionManager->setCollectionSelectionModel(mCollectionView->selectionModel());
0259     mActionManager->setItemSelectionModel(mItemView->selectionModel());
0260 
0261     QList<Akonadi::StandardActionManager::Type> standardActions;
0262     standardActions << Akonadi::StandardActionManager::CreateCollection << Akonadi::StandardActionManager::DeleteCollections
0263                     << Akonadi::StandardActionManager::SynchronizeCollections << Akonadi::StandardActionManager::CollectionProperties
0264                     << Akonadi::StandardActionManager::CopyItems << Akonadi::StandardActionManager::Paste << Akonadi::StandardActionManager::DeleteItems
0265                     << Akonadi::StandardActionManager::CutItems << Akonadi::StandardActionManager::CreateResource
0266                     << Akonadi::StandardActionManager::DeleteResources << Akonadi::StandardActionManager::ResourceProperties
0267                     << Akonadi::StandardActionManager::SynchronizeResources << Akonadi::StandardActionManager::SynchronizeCollectionsRecursive
0268                     << Akonadi::StandardActionManager::MoveItemToMenu << Akonadi::StandardActionManager::CopyItemToMenu;
0269 
0270     for (Akonadi::StandardActionManager::Type standardAction : std::as_const(standardActions)) {
0271         mActionManager->createAction(standardAction);
0272     }
0273     guiClient->actionCollection()->setDefaultShortcut(mActionManager->action(Akonadi::StandardActionManager::DeleteItems), QKeySequence(Qt::Key_Delete));
0274     QList<Akonadi::StandardContactActionManager::Type> contactActions;
0275     contactActions << Akonadi::StandardContactActionManager::CreateContact << Akonadi::StandardContactActionManager::CreateContactGroup
0276                    << Akonadi::StandardContactActionManager::EditItem;
0277 
0278     for (Akonadi::StandardContactActionManager::Type contactAction : std::as_const(contactActions)) {
0279         mActionManager->createAction(contactAction);
0280     }
0281 
0282     mActionManager->interceptAction(Akonadi::StandardActionManager::CollectionProperties);
0283     connect(mActionManager->action(Akonadi::StandardActionManager::CollectionProperties),
0284             &QAction::triggered,
0285             mManageShowCollectionProperties,
0286             &ManageShowCollectionProperties::showCollectionProperties);
0287 
0288     connect(mItemView, qOverload<const Akonadi::Item &>(&Akonadi::EntityTreeView::currentChanged), this, &MainWidget::itemSelected);
0289     connect(mItemView,
0290             qOverload<const Akonadi::Item &>(&Akonadi::EntityTreeView::doubleClicked),
0291             mActionManager->action(Akonadi::StandardContactActionManager::EditItem),
0292             &QAction::trigger);
0293     connect(mItemView->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWidget::itemSelectionChanged);
0294 
0295     // show the contact details view as default
0296     mDetailsViewStack->setCurrentWidget(mContactDetails);
0297 
0298     mContactSwitcher->setView(mItemView);
0299 
0300     Akonadi::ControlGui::widgetNeedsAkonadi(this);
0301 
0302     mModelColumnManager = new ModelColumnManager(GlobalContactModel::instance()->model(), this);
0303     mModelColumnManager->setWidget(mItemView->header());
0304     mModelColumnManager->load();
0305 
0306     initializeImportExportPlugin(guiClient->actionCollection());
0307     QMetaObject::invokeMethod(this, &MainWidget::delayedInit, Qt::QueuedConnection);
0308     updateQuickSearchText();
0309 }
0310 
0311 void MainWidget::slotGeneralPaletteChanged()
0312 {
0313     mContactDetails->updateView();
0314     mContactGroupDetails->updateView();
0315 }
0316 
0317 bool MainWidget::event(QEvent *e)
0318 {
0319     if (e->type() == QEvent::ApplicationPaletteChange) {
0320         slotGeneralPaletteChanged();
0321     }
0322     return QWidget::event(e);
0323 }
0324 
0325 void MainWidget::setFocusToTreeView()
0326 {
0327     mItemView->setFocus();
0328 }
0329 
0330 void MainWidget::initializeImportExportPlugin(KActionCollection *collection)
0331 {
0332     const QList<KAddressBookImportExport::Plugin *> listPlugins = KAddressBookImportExport::PluginManager::self()->pluginsList();
0333     QList<QAction *> importActions;
0334     QList<QAction *> exportActions;
0335     for (KAddressBookImportExport::Plugin *plugin : listPlugins) {
0336         if (plugin->isEnabled()) {
0337             auto interface = static_cast<KAddressBookImportExport::PluginInterface *>(plugin->createInterface(this));
0338             interface->setItemSelectionModel(mItemView->selectionModel());
0339             interface->setParentWidget(this);
0340             interface->createAction(collection);
0341             importActions.append(interface->importActions());
0342             exportActions.append(interface->exportActions());
0343             mImportExportPluginInterfaceList.append(interface);
0344             connect(interface, &PimCommon::AbstractGenericPluginInterface::emitPluginActivated, this, &MainWidget::slotImportExportActivated);
0345         }
0346     }
0347 
0348     if (!importActions.isEmpty()) {
0349         auto importMenu = new KActionMenu(i18n("Import"), this);
0350         collection->addAction(QStringLiteral("import_menu"), importMenu);
0351         for (QAction *act : std::as_const(importActions)) {
0352             importMenu->addAction(act);
0353         }
0354     }
0355     if (!exportActions.isEmpty()) {
0356         auto exportMenu = new KActionMenu(i18n("Export"), this);
0357         collection->addAction(QStringLiteral("export_menu"), exportMenu);
0358         for (QAction *act : std::as_const(exportActions)) {
0359             exportMenu->addAction(act);
0360         }
0361     }
0362 }
0363 
0364 void MainWidget::configure()
0365 {
0366     KCMultiDialog dlg(this);
0367     const QList<KPluginMetaData> availablePlugins = KPluginMetaData::findPlugins(QStringLiteral("pim6/kcms/kaddressbook"));
0368     for (const KPluginMetaData &metaData : availablePlugins) {
0369         dlg.addModule(metaData);
0370     }
0371     dlg.exec();
0372 }
0373 
0374 void MainWidget::handleCommandLine(const QStringList &arguments)
0375 {
0376     QCommandLineParser parser;
0377     kaddressbook_options(&parser);
0378     parser.process(arguments);
0379 
0380     if (parser.isSet(QStringLiteral("import"))) {
0381         const QStringList lst = parser.positionalArguments();
0382         for (const QString &urlStr : lst) {
0383             const QUrl url(QUrl::fromUserInput(urlStr));
0384             for (KAddressBookImportExport::PluginInterface *interface : std::as_const(mImportExportPluginInterfaceList)) {
0385                 if (interface->canImportFileType(url)) {
0386                     interface->importFile(url);
0387                     break;
0388                 }
0389             }
0390         }
0391     } else if (parser.isSet(QStringLiteral("newcontact"))) {
0392         newContact();
0393     } else if (parser.isSet(QStringLiteral("view"))) {
0394         const auto url = QUrl{parser.value(QStringLiteral("view"))};
0395         mPendingSelection = Akonadi::Item::fromUrl(url);
0396     }
0397 }
0398 
0399 void MainWidget::updateQuickSearchText()
0400 {
0401     mQuickSearchWidget->updateQuickSearchText(i18nc("@label Search contacts in list", "Search... <%1>", mQuickSearchAction->shortcut().toString()));
0402 }
0403 
0404 void MainWidget::delayedInit()
0405 {
0406     setViewMode(0); // get default from settings
0407 
0408     const KConfigGroup group(Settings::self()->config(), QStringLiteral("UiState_ContactView"));
0409     KAddressBook::UiStateSaver::restoreState(mItemView, group);
0410 
0411     mXmlGuiClient->actionCollection()->action(QStringLiteral("options_show_qrcodes"))->setChecked(showQRCodes());
0412 
0413     connect(GlobalContactModel::instance()->model(), &QAbstractItemModel::modelAboutToBeReset, this, &MainWidget::saveState);
0414     connect(GlobalContactModel::instance()->model(), &QAbstractItemModel::modelReset, this, &MainWidget::restoreState);
0415     connect(qApp, &QApplication::aboutToQuit, this, &MainWidget::saveState);
0416 
0417     restoreState();
0418     updateQuickSearchText();
0419     initializePluginActions();
0420 }
0421 
0422 MainWidget::~MainWidget()
0423 {
0424     mModelColumnManager->store();
0425     saveSplitterStates();
0426 
0427     KConfigGroup group(Settings::self()->config(), QStringLiteral("UiState_ContactView"));
0428     KAddressBook::UiStateSaver::saveState(mItemView, group);
0429 
0430     saveState();
0431     delete mGrantleeThemeManager;
0432     delete mFormatter;
0433     delete mGroupFormatter;
0434 
0435     Settings::self()->save();
0436 }
0437 
0438 void MainWidget::restoreState()
0439 {
0440     // collection view
0441     {
0442         auto saver = new Akonadi::ETMViewStateSaver;
0443         saver->setView(mCollectionView);
0444 
0445         const KConfigGroup group(Settings::self()->config(), QStringLiteral("CollectionViewState"));
0446         saver->restoreState(group);
0447     }
0448 
0449     // collection view
0450     {
0451         auto saver = new Akonadi::ETMViewStateSaver;
0452         saver->setSelectionModel(mCollectionSelectionModel);
0453 
0454         const KConfigGroup group(Settings::self()->config(), QStringLiteral("CollectionViewCheckState"));
0455         saver->restoreState(group);
0456     }
0457 
0458     // item view
0459     {
0460         auto saver = new Akonadi::ETMViewStateSaver;
0461         saver->setView(mItemView);
0462         saver->setSelectionModel(mItemView->selectionModel());
0463 
0464         if (mPendingSelection.isValid()) {
0465             saver->selectItems({mPendingSelection});
0466             saver->setCurrentItem(mPendingSelection);
0467             mPendingSelection = {};
0468         } else {
0469             const KConfigGroup group(Settings::self()->config(), QStringLiteral("ItemViewState"));
0470             saver->restoreState(group);
0471         }
0472     }
0473 }
0474 
0475 void MainWidget::saveState()
0476 {
0477     // collection view
0478     {
0479         Akonadi::ETMViewStateSaver saver;
0480         saver.setView(mCollectionView);
0481 
0482         KConfigGroup group(Settings::self()->config(), QStringLiteral("CollectionViewState"));
0483         saver.saveState(group);
0484         group.sync();
0485     }
0486 
0487     // collection view
0488     {
0489         Akonadi::ETMViewStateSaver saver;
0490         saver.setSelectionModel(mCollectionSelectionModel);
0491 
0492         KConfigGroup group(Settings::self()->config(), QStringLiteral("CollectionViewCheckState"));
0493         saver.saveState(group);
0494         group.sync();
0495     }
0496 
0497     // item view
0498     {
0499         Akonadi::ETMViewStateSaver saver;
0500         saver.setView(mItemView);
0501         saver.setSelectionModel(mItemView->selectionModel());
0502 
0503         KConfigGroup group(Settings::self()->config(), QStringLiteral("ItemViewState"));
0504         saver.saveState(group);
0505         group.sync();
0506     }
0507 }
0508 
0509 void MainWidget::setupGui()
0510 {
0511     // the horizontal main layout
0512     auto layout = new QHBoxLayout(this);
0513     layout->setContentsMargins({});
0514 
0515     // Splitter 1 contains the two main parts of the GUI:
0516     //  - collection and item view splitter 2 on the left (see below)
0517     //  - details pane on the right, that contains
0518     //   - details view stack on the top
0519     //   - contact switcher at the bottom
0520     mMainWidgetSplitter1 = new QSplitter(Qt::Horizontal);
0521     mMainWidgetSplitter1->setObjectName(QLatin1StringView("MainWidgetSplitter1"));
0522     layout->addWidget(mMainWidgetSplitter1);
0523 
0524     // Splitter 2 contains the remaining parts of the GUI:
0525     //  - collection view on either the left or the top
0526     //  - item view on either the right or the bottom
0527     // The orientation of this splitter is changed for either
0528     // a three or two column view;  in simple mode it is hidden.
0529     mMainWidgetSplitter2 = new QSplitter(Qt::Vertical);
0530     mMainWidgetSplitter2->setObjectName(QLatin1StringView("MainWidgetSplitter2"));
0531     mMainWidgetSplitter1->addWidget(mMainWidgetSplitter2);
0532 
0533     // the collection view
0534     mCollectionView = new Akonadi::EntityTreeView();
0535     mMainWidgetSplitter2->addWidget(mCollectionView);
0536 
0537     // the items view
0538     mItemView = new Akonadi::EntityTreeView();
0539     mItemView->setObjectName(QLatin1StringView("ContactView"));
0540     mItemView->setDefaultPopupMenu(QStringLiteral("akonadi_itemview_contextmenu"));
0541     mItemView->setAlternatingRowColors(true);
0542     mMainWidgetSplitter2->addWidget(mItemView);
0543 
0544     // the details pane that contains the details view stack and contact switcher
0545     mDetailsPane = new QWidget;
0546     mMainWidgetSplitter1->addWidget(mDetailsPane);
0547 
0548     mMainWidgetSplitter1->setStretchFactor(1, 9); // maximum width for detail
0549     mMainWidgetSplitter2->setStretchFactor(1, 9); // for intuitive resizing
0550     mMainWidgetSplitter2->setChildrenCollapsible(false);
0551     mMainWidgetSplitter1->setChildrenCollapsible(false);
0552 
0553     auto detailsPaneLayout = new QVBoxLayout(mDetailsPane);
0554     detailsPaneLayout->setContentsMargins({});
0555 
0556     // the details view stack
0557     mDetailsViewStack = new QStackedWidget();
0558     detailsPaneLayout->addWidget(mDetailsViewStack);
0559 
0560     // the details widget for contacts
0561     mContactDetails = new Akonadi::ContactViewer(mDetailsViewStack);
0562     connect(mContactDetails, &Akonadi::ContactViewer::urlClicked, this, [](const QUrl &url) {
0563         QDesktopServices::openUrl(url);
0564     });
0565     mDetailsViewStack->addWidget(mContactDetails);
0566 
0567     // the details widget for contact groups
0568     mContactGroupDetails = new Akonadi::ContactGroupViewer(mDetailsViewStack);
0569     connect(mContactGroupDetails, &Akonadi::ContactGroupViewer::urlClicked, this, [](const QUrl &url) {
0570         QDesktopServices::openUrl(url);
0571     });
0572     mDetailsViewStack->addWidget(mContactGroupDetails);
0573 
0574     // the details widget for empty items
0575     mEmptyDetails = new QTextBrowser(mDetailsViewStack);
0576     mDetailsViewStack->addWidget(mEmptyDetails);
0577 
0578     // the contact switcher for the simple gui mode
0579     mContactSwitcher = new ContactSwitcher;
0580     detailsPaneLayout->addWidget(mContactSwitcher);
0581     mContactSwitcher->setVisible(false);
0582 
0583     // the quick search widget which is embedded in the toolbar action
0584     mQuickSearchWidget = new QuickSearchWidget;
0585     mQuickSearchWidget->setMaximumWidth(500);
0586 
0587     // the category filter widget which is embedded in the toolbar action
0588     mCategorySelectWidget = new CategorySelectWidget;
0589 
0590     mFormatter = new KAddressBookGrantlee::GrantleeContactFormatter;
0591     mFormatter->setApplicationDomain("kaddressbook");
0592 
0593     mContactDetails->setContactFormatter(mFormatter);
0594 
0595     mGroupFormatter = new KAddressBookGrantlee::GrantleeContactGroupFormatter;
0596 
0597     mContactGroupDetails->setContactGroupFormatter(mGroupFormatter);
0598 }
0599 
0600 void MainWidget::initializePluginActions()
0601 {
0602     KAddressBookPluginInterface::self()->initializePluginActions(QStringLiteral("kaddressbook"), mXmlGuiClient);
0603 }
0604 
0605 bool MainWidget::canClose() const
0606 {
0607     // TODO
0608     return true;
0609 }
0610 
0611 void MainWidget::slotImportExportActivated(PimCommon::AbstractGenericPluginInterface *interface)
0612 {
0613     auto importExportInterface = static_cast<KAddressBookImportExport::PluginInterface *>(interface);
0614     if (importExportInterface) {
0615         importExportInterface->exec();
0616     }
0617 }
0618 
0619 void MainWidget::setupActions(KActionCollection *collection)
0620 {
0621     KAddressBookPluginInterface::self()->setParentWidget(this);
0622     KAddressBookPluginInterface::self()->setMainWidget(this);
0623     KAddressBookPluginInterface::self()->createPluginInterface();
0624 
0625     mGrantleeThemeManager = new GrantleeTheme::ThemeManager(QStringLiteral("addressbook"),
0626                                                             QStringLiteral("theme.desktop"),
0627                                                             collection,
0628                                                             QStringLiteral("kaddressbook/viewertemplates/"),
0629                                                             QString(),
0630                                                             this);
0631     connect(mGrantleeThemeManager, &GrantleeTheme::ThemeManager::grantleeThemeSelected, this, &MainWidget::slotGrantleeThemeSelected);
0632     connect(mGrantleeThemeManager, &GrantleeTheme::ThemeManager::updateThemes, this, &MainWidget::slotGrantleeThemesUpdated);
0633 
0634     initGrantleeThemeName();
0635 
0636     QAction *action = KStandardAction::print(this, &MainWidget::print, collection);
0637     action->setWhatsThis(i18nc("@info:whatsthis", "Print the complete address book or a selected number of contacts."));
0638 
0639     KStandardAction::printPreview(this, &MainWidget::printPreview, collection);
0640 
0641     auto quicksearch = new QWidgetAction(this);
0642     quicksearch->setText(i18n("Quick search"));
0643     quicksearch->setDefaultWidget(mQuickSearchWidget);
0644     collection->addAction(QStringLiteral("quick_search"), quicksearch);
0645 
0646     auto categoryFilter = new QWidgetAction(this);
0647     categoryFilter->setText(i18n("Category filter"));
0648     categoryFilter->setDefaultWidget(mCategorySelectWidget);
0649     collection->addAction(QStringLiteral("category_filter"), categoryFilter);
0650 
0651     action = collection->addAction(QStringLiteral("select_all"));
0652     action->setText(i18n("Select All"));
0653     collection->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_A));
0654     action->setWhatsThis(i18n("Select all contacts in the current address book view."));
0655     connect(action, &QAction::triggered, mItemView, &Akonadi::EntityTreeView::selectAll);
0656 
0657     auto qrtoggleAction = collection->add<KToggleAction>(QStringLiteral("options_show_qrcodes"));
0658     qrtoggleAction->setText(i18n("Show QR Codes"));
0659     qrtoggleAction->setWhatsThis(i18n("Show QR Codes in the contact."));
0660     connect(qrtoggleAction, &KToggleAction::toggled, this, &MainWidget::setQRCodeShow);
0661 
0662     mViewModeGroup = new QActionGroup(this);
0663 
0664     auto act = new QAction(i18nc("@action:inmenu", "Simple (one column)"), mViewModeGroup);
0665     act->setCheckable(true);
0666     act->setData(1);
0667     collection->setDefaultShortcut(act, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_1));
0668     act->setWhatsThis(i18n("Show a simple mode of the address book view."));
0669     collection->addAction(QStringLiteral("view_mode_simple"), act);
0670 
0671     act = new QAction(i18nc("@action:inmenu", "Two Columns"), mViewModeGroup);
0672     act->setCheckable(true);
0673     act->setData(2);
0674     collection->addAction(QStringLiteral("view_mode_2columns"), act);
0675     collection->setDefaultShortcut(act, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_2));
0676 
0677     act = new QAction(i18nc("@action:inmenu", "Three Columns"), mViewModeGroup);
0678     act->setCheckable(true);
0679     act->setData(3);
0680     collection->setDefaultShortcut(act, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_3));
0681     collection->addAction(QStringLiteral("view_mode_3columns"), act);
0682 
0683     connect(mViewModeGroup, &QActionGroup::triggered, this, &MainWidget::setActivateViewMode);
0684 
0685     KToggleAction *actTheme = mGrantleeThemeManager->actionForTheme();
0686     if (actTheme) {
0687         actTheme->setChecked(true);
0688     }
0689 
0690     mQuickSearchAction = new QAction(i18n("Set Focus to Quick Search"), this);
0691     // If change shortcut change in quicksearchwidget->lineedit->setPlaceholderText
0692     collection->addAction(QStringLiteral("focus_to_quickseach"), mQuickSearchAction);
0693     connect(mQuickSearchAction, &QAction::triggered, mQuickSearchWidget, &QuickSearchWidget::slotFocusQuickSearch);
0694     collection->setDefaultShortcut(mQuickSearchAction, QKeySequence(Qt::ALT | Qt::Key_Q));
0695 #ifndef FORCE_DISABLE_AKONADI_SEARCH
0696     if (!qEnvironmentVariableIsEmpty("KDEPIM_DEBUGGING")) {
0697         action = collection->addAction(QStringLiteral("debug_akonadi_search"));
0698         // Don't translate it. It's just for debug
0699         action->setText(QStringLiteral("Debug Akonadi Search..."));
0700         connect(action, &QAction::triggered, this, &MainWidget::slotDebugAkonadiSearch);
0701     }
0702 #endif
0703     mServerSideSubscription = new QAction(QIcon::fromTheme(QStringLiteral("folder-bookmarks")), i18n("Serverside Subscription..."), this);
0704     collection->addAction(QStringLiteral("serverside_subscription"), mServerSideSubscription);
0705     connect(mServerSideSubscription, &QAction::triggered, this, &MainWidget::slotServerSideSubscription);
0706 
0707     auto manager = new KColorSchemeManager(this);
0708     collection->addAction(QStringLiteral("colorscheme_menu"), KColorSchemeMenu::createMenu(manager, this));
0709 }
0710 
0711 void MainWidget::printPreview()
0712 {
0713     QPrinter printer;
0714     printer.setDocName(i18n("Address Book"));
0715     printer.setOutputFileName(Settings::self()->defaultFileName());
0716     printer.setOutputFormat(QPrinter::PdfFormat);
0717     printer.setCollateCopies(true);
0718 
0719     QPointer<QPrintPreviewDialog> previewdlg = new QPrintPreviewDialog(&printer, this);
0720     new KWindowStateSaver(previewdlg.data(), QLatin1StringView("KAddressBookPrintPreviewDialog"));
0721 
0722     KABPrinting::PrintingWizard wizard(&printer, mItemView->selectionModel(), this);
0723     wizard.setDefaultAddressBook(currentAddressBook());
0724     connect(previewdlg.data(), &QPrintPreviewDialog::paintRequested, this, [&wizard]() {
0725         wizard.print();
0726     });
0727 
0728     const int result = wizard.exec();
0729     if (result) {
0730         Settings::self()->setDefaultFileName(printer.outputFileName());
0731         Settings::self()->setPrintingStyle(wizard.printingStyle());
0732         Settings::self()->setSortOrder(wizard.sortOrder());
0733         previewdlg->exec();
0734     }
0735     delete previewdlg;
0736 }
0737 
0738 void MainWidget::print()
0739 {
0740     QPrinter printer;
0741     printer.setDocName(i18n("Address Book"));
0742     printer.setOutputFileName(Settings::self()->defaultFileName());
0743     printer.setCollateCopies(true);
0744 
0745     QPointer<QPrintDialog> printDialog = new QPrintDialog(&printer, this);
0746 
0747     printDialog->setWindowTitle(i18nc("@title:window", "Print Contacts"));
0748     if (!printDialog->exec() || !printDialog) {
0749         delete printDialog;
0750         return;
0751     }
0752     KABPrinting::PrintingWizard wizard(&printer, mItemView->selectionModel(), this);
0753     wizard.setDefaultAddressBook(currentAddressBook());
0754 
0755     wizard.exec(); // krazy:exclude=crashy
0756 
0757     Settings::self()->setDefaultFileName(printer.outputFileName());
0758     Settings::self()->setPrintingStyle(wizard.printingStyle());
0759     Settings::self()->setSortOrder(wizard.sortOrder());
0760 }
0761 
0762 void MainWidget::newContact()
0763 {
0764     mActionManager->action(Akonadi::StandardContactActionManager::CreateContact)->trigger();
0765 }
0766 
0767 void MainWidget::newGroup()
0768 {
0769     mActionManager->action(Akonadi::StandardContactActionManager::CreateContactGroup)->trigger();
0770 }
0771 
0772 /**
0773  * Depending on the mime type of the selected item, this method
0774  * brings up the right view on the detail view stack and sets the
0775  * selected item on it.
0776  */
0777 void MainWidget::itemSelected(const Akonadi::Item &item)
0778 {
0779     if (Akonadi::MimeTypeChecker::isWantedItem(item, KContacts::Addressee::mimeType())) {
0780         mDetailsViewStack->setCurrentWidget(mContactDetails);
0781         mContactDetails->setContact(item);
0782     } else if (Akonadi::MimeTypeChecker::isWantedItem(item, KContacts::ContactGroup::mimeType())) {
0783         mDetailsViewStack->setCurrentWidget(mContactGroupDetails);
0784         mContactGroupDetails->setContactGroup(item);
0785     }
0786 }
0787 
0788 /**
0789  * Catch when the selection has gone ( e.g. an empty address book has been selected )
0790  * clear the details view in this case.
0791  */
0792 void MainWidget::itemSelectionChanged(const QModelIndex &current, const QModelIndex &)
0793 {
0794     if (!current.isValid()) {
0795         mDetailsViewStack->setCurrentWidget(mEmptyDetails);
0796     }
0797 }
0798 
0799 void MainWidget::selectFirstItem()
0800 {
0801     // Whenever the quick search has changed, we select the first item
0802     // in the item view, so that the detailsview is updated
0803     if (mItemView && mItemView->selectionModel()) {
0804         mItemView->selectionModel()->setCurrentIndex(mItemView->model()->index(0, 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect);
0805     }
0806 }
0807 
0808 bool MainWidget::showQRCodes()
0809 {
0810     KConfig config(QStringLiteral("akonadi_contactrc"));
0811     KConfigGroup group(&config, QStringLiteral("View"));
0812     return group.readEntry("QRCodes", true);
0813 }
0814 
0815 void MainWidget::setQRCodeShow(bool on)
0816 {
0817     // must write the configuration setting first before updating the view.
0818     KConfig config(QStringLiteral("akonadi_contactrc"));
0819     KConfigGroup group(&config, QStringLiteral("View"));
0820     group.writeEntry("QRCodes", on);
0821     group.sync();
0822     if (mDetailsViewStack->currentWidget() == mContactDetails) {
0823         mFormatter->setShowQRCode(on);
0824         mContactDetails->setShowQRCode(on);
0825     }
0826 }
0827 
0828 Akonadi::Item::List MainWidget::selectedItems(bool &canceled)
0829 {
0830     Akonadi::Item::List items;
0831     QPointer<KAddressBookImportExport::ContactSelectionDialog> dlg =
0832         new KAddressBookImportExport::ContactSelectionDialog(mItemView->selectionModel(), false, this);
0833     dlg->setDefaultAddressBook(currentAddressBook());
0834     if (!dlg->exec() || !dlg) {
0835         canceled = true;
0836         delete dlg;
0837         return items;
0838     }
0839 
0840     items = dlg->selectedItems();
0841     canceled = false;
0842     delete dlg;
0843 
0844     return items;
0845 }
0846 
0847 Akonadi::Collection MainWidget::currentAddressBook() const
0848 {
0849     if (mCollectionView->selectionModel() && mCollectionView->selectionModel()->hasSelection()) {
0850         const QModelIndex index = mCollectionView->selectionModel()->selectedIndexes().first();
0851         const auto collection = index.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
0852 
0853         return collection;
0854     }
0855 
0856     return {};
0857 }
0858 
0859 QAbstractItemModel *MainWidget::allContactsModel()
0860 {
0861     if (!mAllContactsModel) {
0862         auto descendantsModel = new KDescendantsProxyModel(this);
0863         descendantsModel->setSourceModel(GlobalContactModel::instance()->model());
0864 
0865         mAllContactsModel = new Akonadi::EntityMimeTypeFilterModel(this);
0866         mAllContactsModel->setSourceModel(descendantsModel);
0867         mAllContactsModel->addMimeTypeExclusionFilter(Akonadi::Collection::mimeType());
0868         mAllContactsModel->setHeaderGroup(Akonadi::EntityTreeModel::ItemListHeaders);
0869     }
0870 
0871     return mAllContactsModel;
0872 }
0873 
0874 void MainWidget::setActivateViewMode(QAction *action)
0875 {
0876     setViewMode(action->data().toInt());
0877 }
0878 
0879 void MainWidget::setViewMode(int mode)
0880 {
0881     int currentMode = Settings::self()->viewMode();
0882     // qCDebug(KADDRESSBOOK_LOG) << "cur" << currentMode << "new" << mode;
0883     if (mode == currentMode) {
0884         return; // nothing to do
0885     }
0886 
0887     if (mode == 0) {
0888         mode = currentMode; // initialization, no save
0889     } else {
0890         saveSplitterStates(); // for 2- or 3-column mode
0891     }
0892     if (mode == 1) { // simple mode
0893         mMainWidgetSplitter2->setVisible(false);
0894         mDetailsPane->setVisible(true);
0895         mContactSwitcher->setVisible(true);
0896     } else {
0897         mMainWidgetSplitter2->setVisible(true);
0898         mContactSwitcher->setVisible(false);
0899 
0900         if (mode == 2) { // 2 columns
0901             mMainWidgetSplitter2->setOrientation(Qt::Vertical);
0902         } else if (mode == 3) { // 3 columns
0903             mMainWidgetSplitter2->setOrientation(Qt::Horizontal);
0904         }
0905     }
0906 
0907     Settings::self()->setViewMode(mode); // save new mode in settings
0908     restoreSplitterStates(); // restore state for new mode
0909     mViewModeGroup->actions().at(mode - 1)->setChecked(true);
0910 
0911     if (mItemView->model()) {
0912         mItemView->setCurrentIndex(mItemView->model()->index(0, 0));
0913     }
0914 }
0915 
0916 void MainWidget::saveSplitterStates() const
0917 {
0918     // The splitter states are saved separately for each column view mode,
0919     // but only if not in simple mode (1 column).
0920     int currentMode = Settings::self()->viewMode();
0921     if (currentMode == 1) {
0922         return;
0923     }
0924 
0925     const QString groupName = QStringLiteral("UiState_MainWidgetSplitter_%1").arg(currentMode);
0926     // qCDebug(KADDRESSBOOK_LOG) << "saving to group" << groupName;
0927     KConfigGroup group(Settings::self()->config(), groupName);
0928     KAddressBook::UiStateSaver::saveState(mMainWidgetSplitter1, group);
0929     KAddressBook::UiStateSaver::saveState(mMainWidgetSplitter2, group);
0930 }
0931 
0932 void MainWidget::restoreSplitterStates()
0933 {
0934     // The splitter states are restored as appropriate for the current
0935     // column view mode, but not for simple mode (1 column).
0936     int currentMode = Settings::self()->viewMode();
0937     if (currentMode == 1) {
0938         return;
0939     }
0940 
0941     const QString groupName = QStringLiteral("UiState_MainWidgetSplitter_%1").arg(currentMode);
0942     // qCDebug(KADDRESSBOOK_LOG) << "restoring from group" << groupName;
0943     KConfigGroup group(Settings::self()->config(), groupName);
0944     KAddressBook::UiStateSaver::restoreState(mMainWidgetSplitter1, group);
0945     KAddressBook::UiStateSaver::restoreState(mMainWidgetSplitter2, group);
0946 }
0947 
0948 void MainWidget::initGrantleeThemeName()
0949 {
0950     QString themeName = mGrantleeThemeManager->configuredThemeName();
0951     if (themeName.isEmpty()) {
0952         themeName = QStringLiteral("default");
0953     }
0954     mFormatter->setGrantleeTheme(mGrantleeThemeManager->theme(themeName));
0955     mGroupFormatter->setGrantleeTheme(mGrantleeThemeManager->theme(themeName));
0956 }
0957 
0958 void MainWidget::slotGrantleeThemeSelected()
0959 {
0960     initGrantleeThemeName();
0961     if (mItemView->model()) {
0962         mItemView->setCurrentIndex(mItemView->model()->index(0, 0));
0963     }
0964 }
0965 
0966 void MainWidget::slotGrantleeThemesUpdated()
0967 {
0968     if (mItemView->model()) {
0969         mItemView->setCurrentIndex(mItemView->model()->index(0, 0));
0970     }
0971 }
0972 
0973 Akonadi::EntityTreeModel *MainWidget::entityTreeModel() const
0974 {
0975     auto proxy = qobject_cast<QAbstractProxyModel *>(mCollectionView->model());
0976     while (proxy) {
0977         auto etm = qobject_cast<Akonadi::EntityTreeModel *>(proxy->sourceModel());
0978         if (etm) {
0979             return etm;
0980         }
0981         proxy = qobject_cast<QAbstractProxyModel *>(proxy->sourceModel());
0982     }
0983 
0984     qCWarning(KADDRESSBOOK_LOG) << "Couldn't find EntityTreeModel";
0985     return nullptr;
0986 }
0987 
0988 void MainWidget::slotCheckNewCalendar(const QModelIndex &parent, int begin, int end)
0989 {
0990     // HACK: Check newly created calendars
0991 
0992     if (begin < end) {
0993         return;
0994     }
0995 
0996     Akonadi::EntityTreeModel *etm = entityTreeModel();
0997     QAbstractItemModel *model = mCollectionView->model();
0998     if (etm && etm->isCollectionTreeFetched()) {
0999         for (int row = begin; row <= end; ++row) {
1000             QModelIndex index = model->index(row, 0, parent);
1001             if (index.isValid()) {
1002                 model->setData(index, Qt::Checked, Qt::CheckStateRole);
1003                 slotCheckNewCalendar(index, 0, model->rowCount(index) - 1);
1004             }
1005         }
1006         if (parent.isValid()) {
1007             mCollectionView->setExpanded(parent, true);
1008         }
1009     }
1010 }
1011 
1012 const Akonadi::Item::List MainWidget::collectSelectedAllContactsItem()
1013 {
1014     return collectSelectedAllContactsItem(mItemView->selectionModel());
1015 }
1016 
1017 void MainWidget::slotDebugAkonadiSearch()
1018 {
1019 #ifndef FORCE_DISABLE_AKONADI_SEARCH
1020     const Akonadi::Item::List lst = collectSelectedAllContactsItem(mItemView->selectionModel());
1021     if (lst.isEmpty()) {
1022         return;
1023     }
1024     QPointer<Akonadi::Search::AkonadiSearchDebugDialog> dlg = new Akonadi::Search::AkonadiSearchDebugDialog;
1025     dlg->setAkonadiId(lst.at(0).id());
1026     dlg->setAttribute(Qt::WA_DeleteOnClose);
1027     dlg->setSearchType(Akonadi::Search::AkonadiSearchDebugSearchPathComboBox::Contacts);
1028     dlg->doSearch();
1029     dlg->show();
1030 #endif
1031 }
1032 
1033 const Akonadi::Item::List MainWidget::collectSelectedAllContactsItem(QItemSelectionModel *model)
1034 {
1035     Akonadi::Item::List lst;
1036 
1037     const QModelIndexList indexes = model->selectedRows(0);
1038     for (int i = 0; i < indexes.count(); ++i) {
1039         const QModelIndex index = indexes.at(i);
1040         if (index.isValid()) {
1041             const auto item = index.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
1042             if (item.isValid()) {
1043                 if (item.hasPayload<KContacts::Addressee>() || item.hasPayload<KContacts::ContactGroup>()) {
1044                     lst.append(item);
1045                 }
1046             }
1047         }
1048     }
1049     return lst;
1050 }
1051 
1052 void MainWidget::slotServerSideSubscription()
1053 {
1054     Akonadi::Collection collection = currentAddressBook();
1055     if (collection.isValid()) {
1056         auto job = new PimCommon::ManageServerSideSubscriptionJob(this);
1057         job->setCurrentCollection(collection);
1058         job->setParentWidget(this);
1059         job->start();
1060     }
1061 }
1062 
1063 void MainWidget::slotCurrentCollectionChanged(const Akonadi::Collection &col)
1064 {
1065     for (auto interface : std::as_const(mImportExportPluginInterfaceList)) {
1066         interface->setDefaultCollection(col);
1067     }
1068     bool isOnline;
1069     mServerSideSubscription->setEnabled(PimCommon::MailUtil::isImapFolder(col, isOnline));
1070 }
1071 
1072 #include "moc_mainwidget.cpp"