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