diff --git a/kmail/kmmainwidget.cpp b/kmail/kmmainwidget.cpp index 66c2c91b07..4270079582 100644 --- a/kmail/kmmainwidget.cpp +++ b/kmail/kmmainwidget.cpp @@ -1,4574 +1,4561 @@ /* This file is part of KMail, the KDE mail client. Copyright (c) 2002 Don Sanders Copyright (c) 2009-2015 Montel Laurent Based on the work of Stefan Taferner KMail is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2, as published by the Free Software Foundation. KMail is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ // KMail includes #include "kmreadermainwin.h" #include "editor/composer.h" #include "searchdialog/searchwindow.h" #include "antispam-virus/antispamwizard.h" #include "widgets/vacationscriptindicatorwidget.h" #include "undostack.h" #include "kmcommands.h" #include "kmmainwin.h" #include "kmsystemtray.h" #include #include "MailCommon/FolderSelectionDialog" #include "MailCommon/FolderTreeWidget" #include "util.h" #include "mailcommon/mailutil.h" #include "mailcommon/mailkernel.h" #include "dialog/archivefolderdialog.h" #include "settings/kmailsettings.h" #include "MailCommon/FolderTreeView" #include "tag/tagactionmanager.h" #include "foldershortcutactionmanager.h" #include "widgets/collectionpane.h" #include "manageshowcollectionproperties.h" #include "widgets/kactionmenutransport.h" #include "widgets/kactionmenuaccount.h" #include "mailcommon/searchrulestatus.h" #include "PimCommon/NetworkUtil" #include "kpimtextedit/texttospeech.h" #include "job/markallmessagesasreadinfolderandsubfolderjob.h" #if !defined(NDEBUG) #include using KSieveUi::SieveDebugDialog; #endif #include "collectionpage/collectionmaintenancepage.h" #include "collectionpage/collectionquotapage.h" #include "collectionpage/collectiontemplatespage.h" #include "collectionpage/collectionshortcutpage.h" #include "collectionpage/collectionviewpage.h" #include "collectionpage/collectionmailinglistpage.h" #include "tag/tagselectdialog.h" #include "job/createnewcontactjob.h" #include "folderarchive/folderarchiveutil.h" #include "folderarchive/folderarchivemanager.h" #include "PimCommon/CollectionAclPage" #include "PimCommon/PimUtil" #include "MailCommon/CollectionGeneralPage" #include "MailCommon/CollectionExpiryPage" #include "MailCommon/ExpireCollectionAttribute" #include "MailCommon/FilterManager" #include "MailCommon/MailFilter" #include "MailCommon/FavoriteCollectionWidget" #include "MailCommon/FolderTreeWidget" #include "MailCommon/FolderTreeView" #include "mailcommonsettings_base.h" #include "kmmainwidget.h" // Other PIM includes #include "kdepim-version.h" #include "messageviewer/messageviewersettings.h" #include "messageviewer/viewer.h" #include "messageviewer/attachmentstrategy.h" #ifndef QT_NO_CURSOR #include "Libkdepim/KCursorSaver" #endif #include "MessageComposer/MessageSender" #include "MessageComposer/MessageHelper" #include "TemplateParser/TemplateParser" #include "MessageCore/MessageCoreSettings" #include "MessageCore/MailingList" #include "messagecore/messagehelpers.h" #include "dialog/kmknotify.h" #include "widgets/displaymessageformatactionmenu.h" #include "ksieveui/vacationmanager.h" #include "kmlaunchexternalcomponent.h" // LIBKDEPIM includes #include "libkdepim/progressmanager.h" #include "libkdepim/broadcaststatus.h" // KDEPIMLIBS includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDELIBS includes #include #include #include #include #include #include #include #include "kmail_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include #include // System includes #include #include #include #include "PimCommon/ManageServerSideSubscriptionJob" #include #include using namespace KMime; using namespace Akonadi; using namespace MailCommon; using KPIM::ProgressManager; using KPIM::BroadcastStatus; using KMail::SearchWindow; using KMail::AntiSpamWizard; using KMime::Types::AddrSpecList; using MessageViewer::AttachmentStrategy; Q_GLOBAL_STATIC(KMMainWidget::PtrList, theMainWidgetList) //----------------------------------------------------------------------------- KMMainWidget::KMMainWidget(QWidget *parent, KXMLGUIClient *aGUIClient, KActionCollection *actionCollection, KSharedConfig::Ptr config) : QWidget(parent), mMoveMsgToFolderAction(Q_NULLPTR), mCollectionProperties(Q_NULLPTR), mFavoriteCollectionsView(Q_NULLPTR), mMsgView(Q_NULLPTR), mSplitter1(Q_NULLPTR), mSplitter2(Q_NULLPTR), mFolderViewSplitter(Q_NULLPTR), mArchiveFolderAction(Q_NULLPTR), mShowBusySplashTimer(Q_NULLPTR), mMsgActions(Q_NULLPTR), mCurrentFolder(Q_NULLPTR), mVacationIndicatorActive(false), mGoToFirstUnreadMessageInSelectedFolder(false), mDisplayMessageFormatMenu(Q_NULLPTR), mFolderDisplayFormatPreference(MessageViewer::Viewer::UseGlobalSetting), mSearchMessages(Q_NULLPTR), mManageShowCollectionProperties(new ManageShowCollectionProperties(this, this)), mShowIntroductionAction(Q_NULLPTR), mMarkAllMessageAsReadAndInAllSubFolder(Q_NULLPTR), mAccountActionMenu(Q_NULLPTR) { mLaunchExternalComponent = new KMLaunchExternalComponent(this, this); // must be the first line of the constructor: mStartupDone = false; mWasEverShown = false; mReaderWindowActive = true; mReaderWindowBelow = true; mFolderHtmlLoadExtPreference = false; mDestructed = false; mActionCollection = actionCollection; mTopLayout = new QVBoxLayout(this); mTopLayout->setMargin(0); mConfig = config; mGUIClient = aGUIClient; mFolderTreeWidget = Q_NULLPTR; mPreferHtmlLoadExtAction = Q_NULLPTR; Akonadi::ControlGui::widgetNeedsAkonadi(this); mFavoritesModel = Q_NULLPTR; mVacationManager = new KSieveUi::VacationManager(this); mToolbarActionSeparator = new QAction(this); mToolbarActionSeparator->setSeparator(true); theMainWidgetList->append(this); readPreConfig(); createWidgets(); setupActions(); readConfig(); if (!kmkernel->isOffline()) { //kmail is set to online mode, make sure the agents are also online kmkernel->setAccountStatus(true); } QTimer::singleShot(0, this, &KMMainWidget::slotShowStartupFolder); connect(kmkernel, &KMKernel::startCheckMail, this, &KMMainWidget::slotStartCheckMail); connect(kmkernel, &KMKernel::endCheckMail, this, &KMMainWidget::slotEndCheckMail); connect(kmkernel, &KMKernel::configChanged, this, &KMMainWidget::slotConfigChanged); connect(kmkernel, &KMKernel::onlineStatusChanged, this, &KMMainWidget::slotUpdateOnlineStatus); connect(mTagActionManager, &KMail::TagActionManager::tagActionTriggered, this, &KMMainWidget::slotUpdateMessageTagList); connect(mTagActionManager, &KMail::TagActionManager::tagMoreActionClicked, this, &KMMainWidget::slotSelectMoreMessageTagList); kmkernel->toggleSystemTray(); { // make sure the pages are registered only once, since there can be multiple instances of KMMainWidget static bool pagesRegistered = false; if (!pagesRegistered) { Akonadi::CollectionPropertiesDialog::registerPage(new PimCommon::CollectionAclPageFactory); Akonadi::CollectionPropertiesDialog::registerPage(new MailCommon::CollectionGeneralPageFactory); Akonadi::CollectionPropertiesDialog::registerPage(new CollectionMaintenancePageFactory); Akonadi::CollectionPropertiesDialog::registerPage(new CollectionQuotaPageFactory); Akonadi::CollectionPropertiesDialog::registerPage(new CollectionTemplatesPageFactory); Akonadi::CollectionPropertiesDialog::registerPage(new MailCommon::CollectionExpiryPageFactory); Akonadi::CollectionPropertiesDialog::registerPage(new CollectionViewPageFactory); Akonadi::CollectionPropertiesDialog::registerPage(new CollectionMailingListPageFactory); Akonadi::CollectionPropertiesDialog::registerPage(new CollectionShortcutPageFactory); pagesRegistered = true; } } KMainWindow *mainWin = dynamic_cast(window()); QStatusBar *sb = mainWin ? mainWin->statusBar() : Q_NULLPTR; mVacationScriptIndicator = new KMail::VacationScriptIndicatorWidget(sb); mVacationScriptIndicator->hide(); connect(mVacationScriptIndicator, &KMail::VacationScriptIndicatorWidget::clicked, this, &KMMainWidget::slotEditVacation); if (KSieveUi::Util::checkOutOfOfficeOnStartup()) { QTimer::singleShot(0, this, &KMMainWidget::slotCheckVacation); } connect(mFolderTreeWidget->folderTreeView()->model(), &QAbstractItemModel::modelReset, this, &KMMainWidget::restoreCollectionFolderViewConfig); restoreCollectionFolderViewConfig(); if (kmkernel->firstStart()) { if (MailCommon::Util::foundMailer()) { if (KMessageBox::questionYesNo(this, i18n("Another mailer was found on system. Do you want to import data from it?")) == KMessageBox::Yes) { const QString path = QStandardPaths::findExecutable(QStringLiteral("importwizard")); if (!QProcess::startDetached(path)) { KMessageBox::error(this, i18n("Could not start the import wizard. " "Please check your installation."), i18n("Unable to start import wizard")); } } else { mLaunchExternalComponent->slotAccountWizard(); } } else { mLaunchExternalComponent->slotAccountWizard(); } } // must be the last line of the constructor: mStartupDone = true; mCheckMailTimer.setInterval(3 * 1000); mCheckMailTimer.setSingleShot(true); connect(&mCheckMailTimer, &QTimer::timeout, this, &KMMainWidget::slotUpdateActionsAfterMailChecking); } void KMMainWidget::restoreCollectionFolderViewConfig() { ETMViewStateSaver *saver = new ETMViewStateSaver; saver->setView(mFolderTreeWidget->folderTreeView()); const KConfigGroup cfg(KMKernel::self()->config(), "CollectionFolderView"); mFolderTreeWidget->restoreHeaderState(cfg.readEntry("HeaderState", QByteArray())); saver->restoreState(cfg); //Restore startup folder Akonadi::Collection::Id id = -1; if (mCurrentFolder && mCurrentFolder->collection().isValid()) { id = mCurrentFolder->collection().id(); } if (id == -1) { if (KMailSettings::self()->startSpecificFolderAtStartup()) { Akonadi::Collection::Id startupFolder = KMailSettings::self()->startupFolder(); if (startupFolder > 0) { saver->restoreCurrentItem(QStringLiteral("c%1").arg(startupFolder)); } } } else { saver->restoreCurrentItem(QStringLiteral("c%1").arg(id)); } } //----------------------------------------------------------------------------- //The kernel may have already been deleted when this method is called, //perform all cleanup that requires the kernel in destruct() KMMainWidget::~KMMainWidget() { theMainWidgetList->removeAll(this); qDeleteAll(mFilterCommands); destruct(); } //----------------------------------------------------------------------------- //This method performs all cleanup that requires the kernel to exist. void KMMainWidget::destruct() { if (mDestructed) { return; } if (mSearchWin) { mSearchWin->close(); } disconnect(mFolderTreeWidget->folderTreeView()->selectionModel(), &QItemSelectionModel::selectionChanged, this, &KMMainWidget::updateFolderMenu); writeConfig(false); /* don't force kmkernel sync when close BUG: 289287 */ writeFolderConfig(); deleteWidgets(); mCurrentFolder.clear(); delete mMoveOrCopyToDialog; delete mSelectFromAllFoldersDialog; disconnect(kmkernel->folderCollectionMonitor(), SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)), this, Q_NULLPTR); disconnect(kmkernel->folderCollectionMonitor(), SIGNAL(itemRemoved(Akonadi::Item)), this, Q_NULLPTR); disconnect(kmkernel->folderCollectionMonitor(), SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)), this, Q_NULLPTR); disconnect(kmkernel->folderCollectionMonitor(), SIGNAL(collectionChanged(Akonadi::Collection,QSet)), this, Q_NULLPTR); disconnect(kmkernel->folderCollectionMonitor(), SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), this, Q_NULLPTR); mDestructed = true; } void KMMainWidget::slotStartCheckMail() { if (mCheckMailTimer.isActive()) { mCheckMailTimer.stop(); } } void KMMainWidget::slotEndCheckMail() { if (!mCheckMailTimer.isActive()) { mCheckMailTimer.start(); } } void KMMainWidget::slotUpdateActionsAfterMailChecking() { const bool sendOnAll = KMailSettings::self()->sendOnCheck() == KMailSettings::EnumSendOnCheck::SendOnAllChecks; const bool sendOnManual = KMailSettings::self()->sendOnCheck() == KMailSettings::EnumSendOnCheck::SendOnManualChecks; if (!kmkernel->isOffline() && (sendOnAll || sendOnManual)) { slotSendQueued(); } // update folder menus in case some mail got filtered to trash/current folder // and we can enable "empty trash/move all to trash" action etc. updateFolderMenu(); } void KMMainWidget::slotCollectionFetched(int collectionId) { // Called when a collection is fetched for the first time by the ETM. // This is the right time to update the caption (which still says "Loading...") // and to update the actions that depend on the number of mails in the folder. if (mCurrentFolder && collectionId == mCurrentFolder->collection().id()) { mCurrentFolder->setCollection(MailCommon::Util::updatedCollection(mCurrentFolder->collection())); updateMessageActions(); updateFolderMenu(); } // We call this for any collection, it could be one of our parents... if (mCurrentFolder) { Q_EMIT captionChangeRequest(MailCommon::Util::fullCollectionPath(mCurrentFolder->collection())); } } void KMMainWidget::slotFolderChanged(const Akonadi::Collection &collection) { folderSelected(collection); if (collection.cachePolicy().syncOnDemand()) { AgentManager::self()->synchronizeCollection(collection, false); } mMsgActions->setCurrentMessage(Akonadi::Item()); Q_EMIT captionChangeRequest(MailCommon::Util::fullCollectionPath(collection)); } void KMMainWidget::folderSelected(const Akonadi::Collection &col) { // This is connected to the MainFolderView signal triggering when a folder is selected if (mGoToFirstUnreadMessageInSelectedFolder) { // the default action has been overridden from outside mPreSelectionMode = MessageList::Core::PreSelectFirstUnreadCentered; } else { // use the default action switch (KMailSettings::self()->actionEnterFolder()) { case KMailSettings::EnumActionEnterFolder::SelectFirstUnread: mPreSelectionMode = MessageList::Core::PreSelectFirstUnreadCentered; break; case KMailSettings::EnumActionEnterFolder::SelectLastSelected: mPreSelectionMode = MessageList::Core::PreSelectLastSelected; break; case KMailSettings::EnumActionEnterFolder::SelectNewest: mPreSelectionMode = MessageList::Core::PreSelectNewestCentered; break; case KMailSettings::EnumActionEnterFolder::SelectOldest: mPreSelectionMode = MessageList::Core::PreSelectOldestCentered; break; default: mPreSelectionMode = MessageList::Core::PreSelectNone; break; } } mGoToFirstUnreadMessageInSelectedFolder = false; #ifndef QT_NO_CURSOR KPIM::KCursorSaver busy(KPIM::KBusyPtr::busy()); #endif if (mMsgView) { mMsgView->clear(true); } const bool newFolder = mCurrentFolder && (mCurrentFolder->collection() != col); // Delete any pending timer, if needed it will be recreated below delete mShowBusySplashTimer; mShowBusySplashTimer = Q_NULLPTR; if (newFolder) { // We're changing folder: write configuration for the old one writeFolderConfig(); } mCurrentFolder = FolderCollection::forCollection(col); readFolderConfig(); if (mMsgView) { mMsgView->setDisplayFormatMessageOverwrite(mFolderDisplayFormatPreference); mMsgView->setHtmlLoadExtOverride(mFolderHtmlLoadExtPreference); } if (!mCurrentFolder->isValid() && (mMessagePane->count() < 2)) { slotIntro(); } updateMessageActions(); updateFolderMenu(); // The message pane uses the selection model of the folder view to load the correct aggregation model and theme // settings. At this point the selection model hasn't been updated yet to the user's new choice, so it would load // the old folder settings instead. QTimer::singleShot(0, this, &KMMainWidget::slotShowSelectedFolderInPane); } void KMMainWidget::slotShowSelectedFolderInPane() { if (mCurrentFolder && mCurrentFolder->collection().isValid()) { mMessagePane->setCurrentFolder(mCurrentFolder->collection(), false, mPreSelectionMode); } } void KMMainWidget::clearViewer() { if (mMsgView) { mMsgView->clear(true); mMsgView->displayAboutPage(); } } //----------------------------------------------------------------------------- void KMMainWidget::readPreConfig() { mLongFolderList = KMailSettings::self()->folderList() == KMailSettings::EnumFolderList::longlist; mReaderWindowActive = KMailSettings::self()->readerWindowMode() != KMailSettings::EnumReaderWindowMode::hide; mReaderWindowBelow = KMailSettings::self()->readerWindowMode() == KMailSettings::EnumReaderWindowMode::below; mHtmlGlobalSetting = MessageViewer::MessageViewerSettings::self()->htmlMail(); mHtmlLoadExtGlobalSetting = MessageViewer::MessageViewerSettings::self()->htmlLoadExternal(); mEnableFavoriteFolderView = (MailCommon::MailCommonSettings::self()->favoriteCollectionViewMode() != MailCommon::MailCommonSettings::EnumFavoriteCollectionViewMode::HiddenMode); mEnableFolderQuickSearch = KMailSettings::self()->enableFolderQuickSearch(); readFolderConfig(); updateHtmlMenuEntry(); if (mMsgView) { mMsgView->setDisplayFormatMessageOverwrite(mFolderDisplayFormatPreference); mMsgView->update(true); } } //----------------------------------------------------------------------------- void KMMainWidget::readFolderConfig() { if (!mCurrentFolder || !mCurrentFolder->isValid()) { return; } KSharedConfig::Ptr config = KMKernel::self()->config(); KConfigGroup group(config, MailCommon::FolderCollection::configGroupName(mCurrentFolder->collection())); if (group.hasKey("htmlMailOverride")) { const bool useHtml = group.readEntry("htmlMailOverride", false); mFolderDisplayFormatPreference = useHtml ? MessageViewer::Viewer::Html : MessageViewer::Viewer::Text; group.deleteEntry("htmlMailOverride"); group.sync(); } else { mFolderDisplayFormatPreference = static_cast(group.readEntry("displayFormatOverride", static_cast(MessageViewer::Viewer::UseGlobalSetting))); } mFolderHtmlLoadExtPreference = group.readEntry("htmlLoadExternalOverride", false); } //----------------------------------------------------------------------------- void KMMainWidget::writeFolderConfig() { if (mCurrentFolder && mCurrentFolder->isValid()) { KSharedConfig::Ptr config = KMKernel::self()->config(); KConfigGroup group(config, MailCommon::FolderCollection::configGroupName(mCurrentFolder->collection())); group.writeEntry("htmlLoadExternalOverride", mFolderHtmlLoadExtPreference); if (mFolderDisplayFormatPreference == MessageViewer::Viewer::UseGlobalSetting) { group.deleteEntry("displayFormatOverride"); } else { group.writeEntry("displayFormatOverride", static_cast(mFolderDisplayFormatPreference)); } } } //----------------------------------------------------------------------------- void KMMainWidget::layoutSplitters() { // This function can only be called when the old splitters are already deleted Q_ASSERT(!mSplitter1); Q_ASSERT(!mSplitter2); // For some reason, this is necessary here so that the copy action still // works after changing the folder layout. if (mMsgView) disconnect(mMsgView->copyAction(), &QAction::triggered, mMsgView, &KMReaderWin::slotCopySelectedText); // If long folder list is enabled, the splitters are: // Splitter 1: FolderView vs (HeaderAndSearch vs MessageViewer) // Splitter 2: HeaderAndSearch vs MessageViewer // // If long folder list is disabled, the splitters are: // Splitter 1: (FolderView vs HeaderAndSearch) vs MessageViewer // Splitter 2: FolderView vs HeaderAndSearch // The folder view is both the folder tree and the favorite folder view, if // enabled const bool readerWindowAtSide = !mReaderWindowBelow && mReaderWindowActive; const bool readerWindowBelow = mReaderWindowBelow && mReaderWindowActive; mSplitter1 = new QSplitter(this); mSplitter2 = new QSplitter(mSplitter1); QWidget *folderTreeWidget = mSearchAndTree; if (mFavoriteCollectionsView) { mFolderViewSplitter = new QSplitter(Qt::Vertical); //mFolderViewSplitter->setChildrenCollapsible( false ); mFolderViewSplitter->addWidget(mFavoriteCollectionsView); mFavoriteCollectionsView->setParent(mFolderViewSplitter); mFolderViewSplitter->addWidget(mSearchAndTree); folderTreeWidget = mFolderViewSplitter; } if (mLongFolderList) { // add folder tree mSplitter1->setOrientation(Qt::Horizontal); mSplitter1->addWidget(folderTreeWidget); // and the rest to the right mSplitter1->addWidget(mSplitter2); // add the message list to the right or below if (readerWindowAtSide) { mSplitter2->setOrientation(Qt::Horizontal); } else { mSplitter2->setOrientation(Qt::Vertical); } mSplitter2->addWidget(mMessagePane); // add the preview window, if there is one if (mMsgView) { mSplitter2->addWidget(mMsgView); } } else { // short folder list if (mReaderWindowBelow) { mSplitter1->setOrientation(Qt::Vertical); mSplitter2->setOrientation(Qt::Horizontal); } else { // at side or none mSplitter1->setOrientation(Qt::Horizontal); mSplitter2->setOrientation(Qt::Vertical); } mSplitter1->addWidget(mSplitter2); // add folder tree mSplitter2->addWidget(folderTreeWidget); // add message list to splitter 2 mSplitter2->addWidget(mMessagePane); // add the preview window, if there is one if (mMsgView) { mSplitter1->addWidget(mMsgView); } } // // Set splitter properties // mSplitter1->setObjectName(QStringLiteral("splitter1")); //mSplitter1->setChildrenCollapsible( false ); mSplitter2->setObjectName(QStringLiteral("splitter2")); //mSplitter2->setChildrenCollapsible( false ); // // Set the stretch factors // mSplitter1->setStretchFactor(0, 0); mSplitter2->setStretchFactor(0, 0); mSplitter1->setStretchFactor(1, 1); mSplitter2->setStretchFactor(1, 1); if (mFavoriteCollectionsView) { mFolderViewSplitter->setStretchFactor(0, 0); mFolderViewSplitter->setStretchFactor(1, 1); } // Because the reader windows's width increases a tiny bit after each // restart in short folder list mode with message window at side, disable // the stretching as a workaround here if (readerWindowAtSide && !mLongFolderList) { mSplitter1->setStretchFactor(0, 1); mSplitter1->setStretchFactor(1, 0); } // // Set the sizes of the splitters to the values stored in the config // QList splitter1Sizes; QList splitter2Sizes; const int folderViewWidth = KMailSettings::self()->folderViewWidth(); int ftHeight = KMailSettings::self()->folderTreeHeight(); int headerHeight = KMailSettings::self()->searchAndHeaderHeight(); const int messageViewerWidth = KMailSettings::self()->readerWindowWidth(); int headerWidth = KMailSettings::self()->searchAndHeaderWidth(); int messageViewerHeight = KMailSettings::self()->readerWindowHeight(); int ffvHeight = mFolderViewSplitter ? MailCommon::MailCommonSettings::self()->favoriteCollectionViewHeight() : 0; // If the message viewer was hidden before, make sure it is not zero height if (messageViewerHeight < 10 && readerWindowBelow) { headerHeight /= 2; messageViewerHeight = headerHeight; } if (mLongFolderList) { if (!readerWindowAtSide) { splitter1Sizes << folderViewWidth << headerWidth; splitter2Sizes << headerHeight << messageViewerHeight; } else { splitter1Sizes << folderViewWidth << (headerWidth + messageViewerWidth); splitter2Sizes << headerWidth << messageViewerWidth; } } else { if (!readerWindowAtSide) { splitter1Sizes << headerHeight << messageViewerHeight; splitter2Sizes << folderViewWidth << headerWidth; } else { splitter1Sizes << headerWidth << messageViewerWidth; splitter2Sizes << ftHeight + ffvHeight << messageViewerHeight; } } mSplitter1->setSizes(splitter1Sizes); mSplitter2->setSizes(splitter2Sizes); if (mFolderViewSplitter) { QList splitterSizes; splitterSizes << ffvHeight << ftHeight; mFolderViewSplitter->setSizes(splitterSizes); } // // Now add the splitters to the main layout // mTopLayout->addWidget(mSplitter1); // Make sure the focus is on the view, and not on the quick search line edit, because otherwise // shortcuts like + or j go to the wrong place. // This would normally be done in the message list itself, but apparently something resets the focus // again, probably all the reparenting we do here. mMessagePane->focusView(); // By default hide th unread and size columns on first run. if (kmkernel->firstStart()) { mFolderTreeWidget->folderTreeView()->hideColumn(1); mFolderTreeWidget->folderTreeView()->hideColumn(3); mFolderTreeWidget->folderTreeView()->header()->resizeSection(0, folderViewWidth * 0.8); } // Make the copy action work, see disconnect comment above if (mMsgView) connect(mMsgView->copyAction(), &QAction::triggered, mMsgView, &KMReaderWin::slotCopySelectedText); } //----------------------------------------------------------------------------- void KMMainWidget::refreshFavoriteFoldersViewProperties() { if (mFavoriteCollectionsView) { if (MailCommon::MailCommonSettings::self()->favoriteCollectionViewMode() == MailCommon::MailCommonSettings::EnumFavoriteCollectionViewMode::IconMode) { mFavoriteCollectionsView->changeViewMode(QListView::IconMode); } else if (MailCommon::MailCommonSettings::self()->favoriteCollectionViewMode() == MailCommon::MailCommonSettings::EnumFavoriteCollectionViewMode::ListMode) { mFavoriteCollectionsView->changeViewMode(QListView::ListMode); } else { Q_ASSERT(false); // we should never get here in hidden mode } mFavoriteCollectionsView->setDropActionMenuEnabled(kmkernel->showPopupAfterDnD()); mFavoriteCollectionsView->setWordWrap(true); mFavoriteCollectionsView->updateMode(); } } //----------------------------------------------------------------------------- void KMMainWidget::readConfig() { const bool oldLongFolderList = mLongFolderList; const bool oldReaderWindowActive = mReaderWindowActive; const bool oldReaderWindowBelow = mReaderWindowBelow; const bool oldFavoriteFolderView = mEnableFavoriteFolderView; const bool oldFolderQuickSearch = mEnableFolderQuickSearch; // on startup, the layout is always new and we need to relayout the widgets bool layoutChanged = !mStartupDone; if (mStartupDone) { readPreConfig(); layoutChanged = (oldLongFolderList != mLongFolderList) || (oldReaderWindowActive != mReaderWindowActive) || (oldReaderWindowBelow != mReaderWindowBelow) || (oldFavoriteFolderView != mEnableFavoriteFolderView); if (layoutChanged) { deleteWidgets(); createWidgets(); restoreCollectionFolderViewConfig(); Q_EMIT recreateGui(); } else if (oldFolderQuickSearch != mEnableFolderQuickSearch) { if (mEnableFolderQuickSearch) { mFolderTreeWidget->filterFolderLineEdit()->show(); } else { mFolderTreeWidget->filterFolderLineEdit()->hide(); } } } { // Read the config of the folder views and the header if (mMsgView) { mMsgView->readConfig(); } mMessagePane->reloadGlobalConfiguration(); mFolderTreeWidget->readConfig(); if (mFavoriteCollectionsView) { mFavoriteCollectionsView->readConfig(); } refreshFavoriteFoldersViewProperties(); } { // area for config group "General" if (!mStartupDone) { // check mail on startup // do it after building the kmmainwin, so that the progressdialog is available QTimer::singleShot(0, this, &KMMainWidget::slotCheckMailOnStartup); } } if (layoutChanged) { layoutSplitters(); } updateMessageMenu(); updateFileMenu(); kmkernel->toggleSystemTray(); mAccountActionMenu->setAccountOrder(MailCommon::MailCommonSettings::self()->order()); connect(Akonadi::AgentManager::self(), &AgentManager::instanceAdded, this, &KMMainWidget::updateFileMenu); connect(Akonadi::AgentManager::self(), &AgentManager::instanceRemoved, this, &KMMainWidget::updateFileMenu); } //----------------------------------------------------------------------------- void KMMainWidget::writeConfig(bool force) { // Don't save the sizes of all the widgets when we were never shown. // This can happen in Kontact, where the KMail plugin is automatically // loaded, but not necessarily shown. // This prevents invalid sizes from being saved if (mWasEverShown) { // The height of the header widget can be 0, this happens when the user // did not switch to the header widget onced and the "Welcome to KMail" // HTML widget was shown the whole time int headersHeight = mMessagePane->height(); if (headersHeight == 0) { headersHeight = height() / 2; } KMailSettings::self()->setSearchAndHeaderHeight(headersHeight); KMailSettings::self()->setSearchAndHeaderWidth(mMessagePane->width()); if (mFavoriteCollectionsView) { MailCommon::MailCommonSettings::self()->setFavoriteCollectionViewHeight(mFavoriteCollectionsView->height()); KMailSettings::self()->setFolderTreeHeight(mFolderTreeWidget->height()); if (!mLongFolderList) { KMailSettings::self()->setFolderViewHeight(mFolderViewSplitter->height()); } } else if (!mLongFolderList && mFolderTreeWidget) { KMailSettings::self()->setFolderTreeHeight(mFolderTreeWidget->height()); } if (mFolderTreeWidget) { KMailSettings::self()->setFolderViewWidth(mFolderTreeWidget->width()); KSharedConfig::Ptr config = KMKernel::self()->config(); KConfigGroup group(config, "CollectionFolderView"); ETMViewStateSaver saver; saver.setView(mFolderTreeWidget->folderTreeView()); saver.saveState(group); group.writeEntry("HeaderState", mFolderTreeWidget->folderTreeView()->header()->saveState()); //Work around from startup folder group.deleteEntry("Selection"); #if 0 if (!KMailSettings::self()->startSpecificFolderAtStartup()) { group.deleteEntry("Current"); } #endif group.sync(); } if (mMsgView) { if (!mReaderWindowBelow) { KMailSettings::self()->setReaderWindowWidth(mMsgView->width()); } mMsgView->viewer()->writeConfig(force); KMailSettings::self()->setReaderWindowHeight(mMsgView->height()); } } } void KMMainWidget::writeReaderConfig() { if (mWasEverShown) { if (mMsgView) { mMsgView->viewer()->writeConfig(); } } } KMReaderWin *KMMainWidget::messageView() const { return mMsgView; } CollectionPane *KMMainWidget::messageListPane() const { return mMessagePane; } //----------------------------------------------------------------------------- void KMMainWidget::deleteWidgets() { // Simply delete the top splitter, which always is mSplitter1, regardless // of the layout. This deletes all children. // akonadi action manager is created in createWidgets(), parented to this // so not autocleaned up. delete mAkonadiStandardActionManager; mAkonadiStandardActionManager = Q_NULLPTR; delete mSplitter1; mMsgView = Q_NULLPTR; mSearchAndTree = Q_NULLPTR; mFolderViewSplitter = Q_NULLPTR; mFavoriteCollectionsView = Q_NULLPTR; mSplitter1 = Q_NULLPTR; mSplitter2 = Q_NULLPTR; mFavoritesModel = Q_NULLPTR; } //----------------------------------------------------------------------------- void KMMainWidget::createWidgets() { // Note that all widgets we create in this function have the parent 'this'. // They will be properly reparented in layoutSplitters() // // Create header view and search bar // FolderTreeWidget::TreeViewOptions opt = FolderTreeWidget::ShowUnreadCount; opt |= FolderTreeWidget::UseLineEditForFiltering; opt |= FolderTreeWidget::ShowCollectionStatisticAnimation; opt |= FolderTreeWidget::DontKeyFilter; mFolderTreeWidget = new FolderTreeWidget(this, mGUIClient, opt); connect(mFolderTreeWidget->folderTreeView(), SIGNAL(currentChanged(Akonadi::Collection)), this, SLOT(slotFolderChanged(Akonadi::Collection))); connect(mFolderTreeWidget->folderTreeView()->selectionModel(), &QItemSelectionModel::selectionChanged, this, &KMMainWidget::updateFolderMenu); connect(mFolderTreeWidget->folderTreeView(), &FolderTreeView::prefereCreateNewTab, this, &KMMainWidget::slotCreateNewTab); mFolderTreeWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); mMessagePane = new CollectionPane(!KMailSettings::self()->startSpecificFolderAtStartup(), KMKernel::self()->entityTreeModel(), mFolderTreeWidget->folderTreeView()->selectionModel(), this); connect(KMKernel::self()->entityTreeModel(), &Akonadi::EntityTreeModel::collectionFetched, this, &KMMainWidget::slotCollectionFetched); mMessagePane->hide();// hide the search bar mMessagePane->setXmlGuiClient(mGUIClient); connect(mMessagePane, &MessageList::Pane::messageSelected, this, &KMMainWidget::slotMessageSelected); connect(mMessagePane, &MessageList::Pane::selectionChanged, this, &KMMainWidget::startUpdateMessageActionsTimer); connect(mMessagePane, &CollectionPane::currentTabChanged, this, &KMMainWidget::refreshMessageListSelection); connect(mMessagePane, &MessageList::Pane::messageActivated, this, &KMMainWidget::slotMessageActivated); connect(mMessagePane, &MessageList::Pane::messageStatusChangeRequest, this, &KMMainWidget::slotMessageStatusChangeRequest); connect(mMessagePane, &MessageList::Pane::statusMessage, BroadcastStatus::instance(), &KPIM::BroadcastStatus::setStatusMsg); // // Create the reader window // if (mReaderWindowActive) { mMsgView = new KMReaderWin(this, this, actionCollection(), Q_NULLPTR); if (mMsgActions) { mMsgActions->setMessageView(mMsgView); } connect(mMsgView->viewer(), &MessageViewer::Viewer::replaceMsgByUnencryptedVersion, this, &KMMainWidget::slotReplaceMsgByUnencryptedVersion); connect(mMsgView->viewer(), &MessageViewer::Viewer::popupMenu, this, &KMMainWidget::slotMessagePopup); connect(mMsgView->viewer(), &MessageViewer::Viewer::moveMessageToTrash, this, &KMMainWidget::slotMoveMessageToTrash); if (mShowIntroductionAction) { mShowIntroductionAction->setEnabled(true); } } else { if (mMsgActions) { mMsgActions->setMessageView(Q_NULLPTR); } if (mShowIntroductionAction) { mShowIntroductionAction->setEnabled(false); } } // // Create the folder tree // the "folder tree" consists of a quicksearch input field and the tree itself // mSearchAndTree = new QWidget(this); QVBoxLayout *vboxlayout = new QVBoxLayout; vboxlayout->setMargin(0); mSearchAndTree->setLayout(vboxlayout); vboxlayout->addWidget(mFolderTreeWidget); if (!KMailSettings::self()->enableFolderQuickSearch()) { mFolderTreeWidget->filterFolderLineEdit()->hide(); } // // Create the favorite folder view // mAkonadiStandardActionManager = new Akonadi::StandardMailActionManager(mGUIClient->actionCollection(), this); connect(mAkonadiStandardActionManager, &Akonadi::StandardMailActionManager::actionStateUpdated, this, &KMMainWidget::slotAkonadiStandardActionUpdated); mAkonadiStandardActionManager->setCollectionSelectionModel(mFolderTreeWidget->folderTreeView()->selectionModel()); mAkonadiStandardActionManager->setItemSelectionModel(mMessagePane->currentItemSelectionModel()); if (mEnableFavoriteFolderView) { mFavoriteCollectionsView = new FavoriteCollectionWidget(mGUIClient, this); refreshFavoriteFoldersViewProperties(); connect(mFavoriteCollectionsView, SIGNAL(currentChanged(Akonadi::Collection)), this, SLOT(slotFolderChanged(Akonadi::Collection))); mFavoritesModel = new Akonadi::FavoriteCollectionsModel( mFolderTreeWidget->folderTreeView()->model(), KMKernel::self()->config()->group("FavoriteCollections"), this); mFavoriteCollectionsView->setModel(mFavoritesModel); mAkonadiStandardActionManager->setFavoriteCollectionsModel(mFavoritesModel); mAkonadiStandardActionManager->setFavoriteSelectionModel(mFavoriteCollectionsView->selectionModel()); } //Don't use mMailActionManager->createAllActions() to save memory by not //creating actions that doesn't make sense. QList standardActions; standardActions << StandardActionManager::CreateCollection << StandardActionManager::CopyCollections << StandardActionManager::DeleteCollections << StandardActionManager::SynchronizeCollections << StandardActionManager::CollectionProperties << StandardActionManager::CopyItems << StandardActionManager::Paste << StandardActionManager::DeleteItems << StandardActionManager::ManageLocalSubscriptions << StandardActionManager::CopyCollectionToMenu << StandardActionManager::CopyItemToMenu << StandardActionManager::MoveItemToMenu << StandardActionManager::MoveCollectionToMenu << StandardActionManager::CutItems << StandardActionManager::CutCollections << StandardActionManager::CreateResource << StandardActionManager::DeleteResources << StandardActionManager::ResourceProperties << StandardActionManager::SynchronizeResources << StandardActionManager::ToggleWorkOffline << StandardActionManager::SynchronizeCollectionsRecursive; Q_FOREACH (StandardActionManager::Type standardAction, standardActions) { mAkonadiStandardActionManager->createAction(standardAction); } if (mEnableFavoriteFolderView) { QList favoriteActions; favoriteActions << StandardActionManager::AddToFavoriteCollections << StandardActionManager::RemoveFromFavoriteCollections << StandardActionManager::RenameFavoriteCollection << StandardActionManager::SynchronizeFavoriteCollections; Q_FOREACH (StandardActionManager::Type favoriteAction, favoriteActions) { mAkonadiStandardActionManager->createAction(favoriteAction); } } QList mailActions; mailActions << StandardMailActionManager::MarkAllMailAsRead << StandardMailActionManager::MoveToTrash << StandardMailActionManager::MoveAllToTrash << StandardMailActionManager::RemoveDuplicates << StandardMailActionManager::EmptyAllTrash << StandardMailActionManager::MarkMailAsRead << StandardMailActionManager::MarkMailAsUnread << StandardMailActionManager::MarkMailAsImportant << StandardMailActionManager::MarkMailAsActionItem; Q_FOREACH (StandardMailActionManager::Type mailAction, mailActions) { mAkonadiStandardActionManager->createAction(mailAction); } mAkonadiStandardActionManager->interceptAction(Akonadi::StandardActionManager::CollectionProperties); connect(mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::CollectionProperties), &QAction::triggered, mManageShowCollectionProperties, &ManageShowCollectionProperties::slotCollectionProperties); // // Create all kinds of actions // mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::RemoveDuplicates)->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Asterisk)); mAkonadiStandardActionManager->interceptAction(Akonadi::StandardMailActionManager::RemoveDuplicates); connect(mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::RemoveDuplicates), &QAction::triggered, this, &KMMainWidget::slotRemoveDuplicates); { mCollectionProperties = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::CollectionProperties); } connect(kmkernel->folderCollectionMonitor(), &Monitor::collectionRemoved, this, &KMMainWidget::slotCollectionRemoved); connect(kmkernel->folderCollectionMonitor(), &Monitor::itemAdded, this, &KMMainWidget::slotItemAdded); connect(kmkernel->folderCollectionMonitor(), &Monitor::itemRemoved, this, &KMMainWidget::slotItemRemoved); connect(kmkernel->folderCollectionMonitor(), &Monitor::itemMoved, this, &KMMainWidget::slotItemMoved); connect(kmkernel->folderCollectionMonitor(), SIGNAL(collectionChanged(Akonadi::Collection,QSet)), SLOT(slotCollectionChanged(Akonadi::Collection,QSet))); connect(kmkernel->folderCollectionMonitor(), &Monitor::collectionStatisticsChanged, this, &KMMainWidget::slotCollectionStatisticsChanged); } void KMMainWidget::updateMoveAction(const Akonadi::CollectionStatistics &statistic) { const bool hasUnreadMails = (statistic.unreadCount() > 0); const bool hasMails = (statistic.count() > 0); updateMoveAction(hasUnreadMails, hasMails); } void KMMainWidget::updateMoveAction(bool hasUnreadMails, bool hasMails) { const bool enable_goto_unread = hasUnreadMails || (KMailSettings::self()->loopOnGotoUnread() == KMailSettings::EnumLoopOnGotoUnread::LoopInAllFolders) || (KMailSettings::self()->loopOnGotoUnread() == KMailSettings::EnumLoopOnGotoUnread::LoopInAllMarkedFolders); actionCollection()->action(QStringLiteral("go_next_message"))->setEnabled(hasMails); actionCollection()->action(QStringLiteral("go_next_unread_message"))->setEnabled(enable_goto_unread); actionCollection()->action(QStringLiteral("go_prev_message"))->setEnabled(hasMails); actionCollection()->action(QStringLiteral("go_prev_unread_message"))->setEnabled(enable_goto_unread); if (mAkonadiStandardActionManager && mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::MarkAllMailAsRead)) { mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::MarkAllMailAsRead)->setEnabled(hasUnreadMails); } } void KMMainWidget::updateAllToTrashAction(int statistics) { bool multiFolder = false; if (mFolderTreeWidget) { multiFolder = mFolderTreeWidget->selectedCollections().count() > 1; } if (mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::MoveAllToTrash)) { const bool folderWithContent = mCurrentFolder && !mCurrentFolder->isStructural(); mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::MoveAllToTrash)->setEnabled(folderWithContent && (statistics > 0) && mCurrentFolder->canDeleteMessages() && !multiFolder); } } void KMMainWidget::slotCollectionStatisticsChanged(Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistic) { if (id == CommonKernel->outboxCollectionFolder().id()) { const bool enableAction = (statistic.count() > 0); mSendQueued->setEnabled(enableAction); mSendActionMenu->setEnabled(enableAction); } else if (mCurrentFolder && (id == mCurrentFolder->collection().id())) { updateMoveAction(statistic); updateAllToTrashAction(statistic.count()); mCurrentFolder->setCollection(MailCommon::Util::updatedCollection(mCurrentFolder->collection())); } } void KMMainWidget::slotCreateNewTab(bool preferNewTab) { mMessagePane->setPreferEmptyTab(preferNewTab); } void KMMainWidget::slotCollectionChanged(const Akonadi::Collection &collection, const QSet &set) { if (mCurrentFolder && (collection == mCurrentFolder->collection()) && (set.contains("MESSAGEFOLDER") || set.contains("expirationcollectionattribute"))) { if (set.contains("MESSAGEFOLDER")) { mMessagePane->resetModelStorage(); } else { mCurrentFolder->setCollection(collection); } } else if (set.contains("ENTITYDISPLAY") || set.contains("NAME")) { const QModelIndex idx = Akonadi::EntityTreeModel::modelIndexForCollection(KMKernel::self()->collectionModel(), collection); if (idx.isValid()) { const QString text = idx.data().toString(); const QIcon icon = idx.data(Qt::DecorationRole).value(); mMessagePane->updateTabIconText(collection, text, icon); } } } void KMMainWidget::slotItemAdded(const Akonadi::Item &msg, const Akonadi::Collection &col) { Q_UNUSED(msg); if (col.isValid()) { if (col == CommonKernel->outboxCollectionFolder()) { startUpdateMessageActionsTimer(); } } } void KMMainWidget::slotItemRemoved(const Akonadi::Item &item) { if (item.isValid() && item.parentCollection().isValid() && (item.parentCollection() == CommonKernel->outboxCollectionFolder())) { startUpdateMessageActionsTimer(); } } void KMMainWidget::slotItemMoved(const Akonadi::Item &item, const Akonadi::Collection &from, const Akonadi::Collection &to) { if (item.isValid() && ((from.id() == CommonKernel->outboxCollectionFolder().id()) || to.id() == CommonKernel->outboxCollectionFolder().id())) { startUpdateMessageActionsTimer(); } } //------------------------------------------------------------------------- void KMMainWidget::slotFocusQuickSearch() { const QString text = mMsgView ? mMsgView->copyText() : QString(); mMessagePane->focusQuickSearch(text); } //------------------------------------------------------------------------- bool KMMainWidget::slotSearch() { if (!mSearchWin) { mSearchWin = new SearchWindow(this, mCurrentFolder ? mCurrentFolder->collection() : Akonadi::Collection()); mSearchWin->setModal(false); mSearchWin->setObjectName(QStringLiteral("Search")); } else { mSearchWin->activateFolder(mCurrentFolder ? mCurrentFolder->collection() : Akonadi::Collection()); } mSearchWin->show(); KWindowSystem::activateWindow(mSearchWin->winId()); return true; } //----------------------------------------------------------------------------- void KMMainWidget::slotHelp() { KHelpClient::invokeHelp(); } //----------------------------------------------------------------------------- void KMMainWidget::slotFilter() { FilterIf->openFilterDialog(true); } void KMMainWidget::slotManageSieveScripts() { if (!kmkernel->askToGoOnline()) { return; } if (mManageSieveDialog) { return; } mManageSieveDialog = new KSieveUi::ManageSieveScriptsDialog; connect(mManageSieveDialog.data(), &KSieveUi::ManageSieveScriptsDialog::finished, this, &KMMainWidget::slotCheckVacation); mManageSieveDialog->show(); } //----------------------------------------------------------------------------- void KMMainWidget::slotCheckMail() { kmkernel->checkMail(); } //----------------------------------------------------------------------------- void KMMainWidget::slotCheckMailOnStartup() { kmkernel->checkMailOnStartup(); } void KMMainWidget::slotCompose() { KMail::Composer *win; KMime::Message::Ptr msg(new KMime::Message()); bool forceCursorPosition = false; if (mCurrentFolder) { MessageHelper::initHeader(msg, KMKernel::self()->identityManager(), mCurrentFolder->identity()); //Laurent: bug 289905 /* if ( mCurrentFolder->collection().isValid() && mCurrentFolder->putRepliesInSameFolder() ) { KMime::Headers::Generic *header = new KMime::Headers::Generic( "X-KMail-Fcc", msg.get(), QString::number( mCurrentFolder->collection().id() ), "utf-8" ); msg->setHeader( header ); } */ TemplateParser::TemplateParser parser(msg, TemplateParser::TemplateParser::NewMessage); parser.setIdentityManager(KMKernel::self()->identityManager()); parser.process(msg, mCurrentFolder->collection()); win = KMail::makeComposer(msg, false, false, KMail::Composer::New, mCurrentFolder->identity()); win->setCollectionForNewMessage(mCurrentFolder->collection()); forceCursorPosition = parser.cursorPositionWasSet(); } else { MessageHelper::initHeader(msg, KMKernel::self()->identityManager()); TemplateParser::TemplateParser parser(msg, TemplateParser::TemplateParser::NewMessage); parser.setIdentityManager(KMKernel::self()->identityManager()); parser.process(KMime::Message::Ptr(), Akonadi::Collection()); win = KMail::makeComposer(msg, false, false, KMail::Composer::New); forceCursorPosition = parser.cursorPositionWasSet(); } if (forceCursorPosition) { win->setFocusToEditor(); } win->show(); } //----------------------------------------------------------------------------- // TODO: do we want the list sorted alphabetically? void KMMainWidget::slotShowNewFromTemplate() { if (mCurrentFolder) { const KIdentityManagement::Identity &ident = kmkernel->identityManager()->identityForUoidOrDefault(mCurrentFolder->identity()); mTemplateFolder = CommonKernel->collectionFromId(ident.templates().toLongLong()); } if (!mTemplateFolder.isValid()) { mTemplateFolder = CommonKernel->templatesCollectionFolder(); } if (!mTemplateFolder.isValid()) { return; } mTemplateMenu->menu()->clear(); Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(mTemplateFolder); job->fetchScope().setAncestorRetrieval(ItemFetchScope::Parent); job->fetchScope().fetchFullPayload(); connect(job, &Akonadi::ItemFetchJob::result, this, &KMMainWidget::slotDelayedShowNewFromTemplate); } void KMMainWidget::slotDelayedShowNewFromTemplate(KJob *job) { Akonadi::ItemFetchJob *fetchJob = qobject_cast(job); const Akonadi::Item::List items = fetchJob->items(); const int numberOfItems = items.count(); for (int idx = 0; idx < numberOfItems; ++idx) { KMime::Message::Ptr msg = MessageCore::Util::message(items.at(idx)); if (msg) { QString subj = msg->subject()->asUnicodeString(); if (subj.isEmpty()) { subj = i18n("No Subject"); } QAction *templateAction = mTemplateMenu->menu()->addAction(KStringHandler::rsqueeze(subj.replace(QLatin1Char('&'), QStringLiteral("&&")))); QVariant var; var.setValue(items.at(idx)); templateAction->setData(var); } } // If there are no templates available, add a menu entry which informs // the user about this. if (mTemplateMenu->menu()->actions().isEmpty()) { QAction *noAction = mTemplateMenu->menu()->addAction( i18n("(no templates)")); noAction->setEnabled(false); } } //----------------------------------------------------------------------------- void KMMainWidget::slotNewFromTemplate(QAction *action) { if (!mTemplateFolder.isValid()) { return; } const Akonadi::Item item = action->data().value(); newFromTemplate(item); } //----------------------------------------------------------------------------- void KMMainWidget::newFromTemplate(const Akonadi::Item &msg) { if (!msg.isValid()) { return; } KMCommand *command = new KMUseTemplateCommand(this, msg); command->start(); } //----------------------------------------------------------------------------- void KMMainWidget::slotPostToML() { if (mCurrentFolder && mCurrentFolder->isMailingListEnabled()) { if (KMail::Util::mailingListPost(mCurrentFolder)) { return; } } slotCompose(); } void KMMainWidget::slotExpireFolder() { if (!mCurrentFolder) { return; } bool mustDeleteExpirationAttribute = false; MailCommon::ExpireCollectionAttribute *attr = MailCommon::Util::expirationCollectionAttribute(mCurrentFolder->collection(), mustDeleteExpirationAttribute); bool canBeExpired = true; if (!attr->isAutoExpire()) { canBeExpired = false; } else if (attr->unreadExpireUnits() == MailCommon::ExpireCollectionAttribute::ExpireNever && attr->readExpireUnits() == MailCommon::ExpireCollectionAttribute::ExpireNever) { canBeExpired = false; } if (!canBeExpired) { const QString message = i18n("This folder does not have any expiry options set"); KMessageBox::information(this, message); if (mustDeleteExpirationAttribute) { delete attr; } return; } if (KMailSettings::self()->warnBeforeExpire()) { const QString message = i18n("Are you sure you want to expire the folder %1?", mCurrentFolder->name().toHtmlEscaped()); if (KMessageBox::warningContinueCancel(this, message, i18n("Expire Folder"), KGuiItem(i18n("&Expire"))) != KMessageBox::Continue) { if (mustDeleteExpirationAttribute) { delete attr; } return; } } MailCommon::Util::expireOldMessages(mCurrentFolder->collection(), true /*immediate*/); if (mustDeleteExpirationAttribute) { delete attr; } } //----------------------------------------------------------------------------- void KMMainWidget::slotEmptyFolder() { if (!mCurrentFolder) { return; } const bool isTrash = CommonKernel->folderIsTrash(mCurrentFolder->collection()); if (KMailSettings::self()->confirmBeforeEmpty()) { const QString title = (isTrash) ? i18n("Empty Trash") : i18n("Move to Trash"); const QString text = (isTrash) ? i18n("Are you sure you want to empty the trash folder?") : i18n("Are you sure you want to move all messages from " "folder %1 to the trash?", mCurrentFolder->name().toHtmlEscaped()); if (KMessageBox::warningContinueCancel(this, text, title, KGuiItem(title, QStringLiteral("user-trash"))) != KMessageBox::Continue) { return; } } #ifndef QT_NO_CURSOR KPIM::KCursorSaver busy(KPIM::KBusyPtr::busy()); #endif slotSelectAllMessages(); if (isTrash) { /* Don't ask for confirmation again when deleting, the user has already confirmed. */ slotDeleteMsg(false); } else { slotTrashSelectedMessages(); } if (mMsgView) { mMsgView->clearCache(); } if (!isTrash) { BroadcastStatus::instance()->setStatusMsg(i18n("Moved all messages to the trash")); } updateMessageActions(); // Disable empty trash/move all to trash action - we've just deleted/moved // all folder contents. mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::MoveAllToTrash)->setEnabled(false); } //----------------------------------------------------------------------------- void KMMainWidget::slotArchiveFolder() { if (mCurrentFolder && mCurrentFolder->collection().isValid()) { KMail::ArchiveFolderDialog archiveDialog; archiveDialog.setFolder(mCurrentFolder->collection()); archiveDialog.exec(); } } //----------------------------------------------------------------------------- void KMMainWidget::slotRemoveFolder() { if (!mCurrentFolder) { return; } if (!mCurrentFolder->collection().isValid()) { return; } if (mCurrentFolder->isSystemFolder()) { return; } if (mCurrentFolder->isReadOnly()) { return; } RemoveCollectionJob *job = new RemoveCollectionJob(this); connect(job, &RemoveCollectionJob::clearCurrentFolder, this, &KMMainWidget::slotClearCurrentFolder); job->setMainWidget(this); job->setCurrentFolder(mCurrentFolder->collection()); job->start(); } void KMMainWidget::slotClearCurrentFolder() { mCurrentFolder.clear(); } //----------------------------------------------------------------------------- void KMMainWidget::slotExpireAll() { if (KMailSettings::self()->warnBeforeExpire()) { const int ret = KMessageBox::warningContinueCancel(KMainWindow::memberList().first(), i18n("Are you sure you want to expire all old messages?"), i18n("Expire Old Messages?"), KGuiItem(i18n("Expire"))); if (ret != KMessageBox::Continue) { return; } } kmkernel->expireAllFoldersNow(); } //----------------------------------------------------------------------------- void KMMainWidget::slotOverrideHtmlLoadExt() { if (mHtmlLoadExtGlobalSetting == mFolderHtmlLoadExtPreference) { int result = KMessageBox::warningContinueCancel(this, // the warning text is taken from configuredialog.cpp: i18n("Loading external references in html mail will make you more vulnerable to " "\"spam\" and may increase the likelihood that your system will be " "compromised by other present and anticipated security exploits."), i18n("Security Warning"), KGuiItem(i18n("Load External References")), KStandardGuiItem::cancel(), QStringLiteral("OverrideHtmlLoadExtWarning"), Q_NULLPTR); if (result == KMessageBox::Cancel) { mPreferHtmlLoadExtAction->setChecked(false); return; } } mFolderHtmlLoadExtPreference = !mFolderHtmlLoadExtPreference; if (mMsgView) { mMsgView->setHtmlLoadExtOverride(mFolderHtmlLoadExtPreference); mMsgView->update(true); } } //----------------------------------------------------------------------------- void KMMainWidget::slotMessageQueuedOrDrafted() { if (!CommonKernel->folderIsDraftOrOutbox(mCurrentFolder->collection())) { return; } if (mMsgView) { mMsgView->update(true); } } //----------------------------------------------------------------------------- void KMMainWidget::slotForwardInlineMsg() { if (!mCurrentFolder) { return; } const Akonadi::Item::List selectedMessages = mMessagePane->selectionAsMessageItemList(); if (selectedMessages.isEmpty()) { return; } KMForwardCommand *command = new KMForwardCommand( this, selectedMessages, mCurrentFolder->identity() ); command->start(); } //----------------------------------------------------------------------------- void KMMainWidget::slotForwardAttachedMsg() { if (!mCurrentFolder) { return; } const Akonadi::Item::List selectedMessages = mMessagePane->selectionAsMessageItemList(); if (selectedMessages.isEmpty()) { return; } KMForwardAttachedCommand *command = new KMForwardAttachedCommand( this, selectedMessages, mCurrentFolder->identity() ); command->start(); } //----------------------------------------------------------------------------- void KMMainWidget::slotUseTemplate() { newFromTemplate(mMessagePane->currentItem()); } //----------------------------------------------------------------------------- void KMMainWidget::slotResendMsg() { const Akonadi::Item msg = mMessagePane->currentItem(); if (!msg.isValid()) { return; } KMCommand *command = new KMResendMessageCommand(this, msg); command->start(); } //----------------------------------------------------------------------------- // Message moving and permanent deletion // void KMMainWidget::moveMessageSelected(MessageList::Core::MessageItemSetReference ref, const Akonadi::Collection &dest, bool confirmOnDeletion) { Akonadi::Item::List selectMsg = mMessagePane->itemListFromPersistentSet(ref); // If this is a deletion, ask for confirmation if (confirmOnDeletion) { int ret = KMessageBox::warningContinueCancel( this, i18np( "Do you really want to delete the selected message?
" "Once deleted, it cannot be restored.
", "Do you really want to delete the %1 selected messages?
" "Once deleted, they cannot be restored.
", selectMsg.count() ), selectMsg.count() > 1 ? i18n("Delete Messages") : i18n("Delete Message"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), QStringLiteral("NoConfirmDelete") ); if (ret == KMessageBox::Cancel) { mMessagePane->deletePersistentSet(ref); return; // user canceled the action } } mMessagePane->markMessageItemsAsAboutToBeRemoved(ref, true); // And stuff them into a KMMoveCommand :) KMMoveCommand *command = new KMMoveCommand(dest, selectMsg, ref); QObject::connect( command, &KMMoveCommand::moveDone, this, &KMMainWidget::slotMoveMessagesCompleted ); command->start(); if (dest.isValid()) { BroadcastStatus::instance()->setStatusMsg(i18n("Moving messages...")); } else { BroadcastStatus::instance()->setStatusMsg(i18n("Deleting messages...")); } } void KMMainWidget::slotMoveMessagesCompleted(KMMoveCommand *command) { Q_ASSERT(command); mMessagePane->markMessageItemsAsAboutToBeRemoved(command->refSet(), false); mMessagePane->deletePersistentSet(command->refSet()); // Bleah :D const bool moveWasReallyADelete = !command->destFolder().isValid(); if (command->result() == KMCommand::OK) { if (moveWasReallyADelete) { BroadcastStatus::instance()->setStatusMsg(i18n("Messages deleted successfully.")); } else { BroadcastStatus::instance()->setStatusMsg(i18n("Messages moved successfully.")); } } else { if (moveWasReallyADelete) { if (command->result() == KMCommand::Failed) { BroadcastStatus::instance()->setStatusMsg(i18n("Deleting messages failed.")); } else { BroadcastStatus::instance()->setStatusMsg(i18n("Deleting messages canceled.")); } } else { if (command->result() == KMCommand::Failed) { BroadcastStatus::instance()->setStatusMsg(i18n("Moving messages failed.")); } else { BroadcastStatus::instance()->setStatusMsg(i18n("Moving messages canceled.")); } } } // The command will autodelete itself and will also kill the set. } void KMMainWidget::slotDeleteMessages() { slotDeleteMsg(true); } void KMMainWidget::slotDeleteMsg(bool confirmDelete) { // Create a persistent message set from the current selection MessageList::Core::MessageItemSetReference ref = mMessagePane->selectionAsPersistentSet(); if (ref != -1) { moveMessageSelected(ref, Akonadi::Collection(), confirmDelete); } } void KMMainWidget::slotDeleteThread(bool confirmDelete) { // Create a persistent set from the current thread. MessageList::Core::MessageItemSetReference ref = mMessagePane->currentThreadAsPersistentSet(); if (ref != -1) { moveMessageSelected(ref, Akonadi::Collection(), confirmDelete); } } FolderSelectionDialog *KMMainWidget::moveOrCopyToDialog() { if (!mMoveOrCopyToDialog) { FolderSelectionDialog::SelectionFolderOption options = FolderSelectionDialog::HideVirtualFolder; mMoveOrCopyToDialog = new FolderSelectionDialog(this, options); mMoveOrCopyToDialog->setModal(true); } return mMoveOrCopyToDialog; } FolderSelectionDialog *KMMainWidget::selectFromAllFoldersDialog() { if (!mSelectFromAllFoldersDialog) { FolderSelectionDialog::SelectionFolderOptions options = FolderSelectionDialog::None; options |= FolderSelectionDialog::NotAllowToCreateNewFolder; mSelectFromAllFoldersDialog = new FolderSelectionDialog(this, options); mSelectFromAllFoldersDialog->setModal(true); } return mSelectFromAllFoldersDialog; } void KMMainWidget::slotMoveSelectedMessageToFolder() { QPointer dialog(moveOrCopyToDialog()); dialog->setWindowTitle(i18n("Move Messages to Folder")); if (dialog->exec() && dialog) { const Akonadi::Collection dest = dialog->selectedCollection(); if (dest.isValid()) { moveSelectedMessagesToFolder(dest); } } } void KMMainWidget::moveSelectedMessagesToFolder(const Akonadi::Collection &dest) { MessageList::Core::MessageItemSetReference ref = mMessagePane->selectionAsPersistentSet(); if (ref != -1) { //Need to verify if dest == src ??? akonadi do it for us. moveMessageSelected(ref, dest, false); } } void KMMainWidget::copyMessageSelected(const Akonadi::Item::List &selectMsg, const Akonadi::Collection &dest) { if (selectMsg.isEmpty()) { return; } // And stuff them into a KMCopyCommand :) KMCommand *command = new KMCopyCommand(dest, selectMsg); QObject::connect( command, &KMCommand::completed, this, &KMMainWidget::slotCopyMessagesCompleted ); command->start(); BroadcastStatus::instance()->setStatusMsg(i18n("Copying messages...")); } void KMMainWidget::slotCopyMessagesCompleted(KMCommand *command) { Q_ASSERT(command); if (command->result() == KMCommand::OK) { BroadcastStatus::instance()->setStatusMsg(i18n("Messages copied successfully.")); } else { if (command->result() == KMCommand::Failed) { BroadcastStatus::instance()->setStatusMsg(i18n("Copying messages failed.")); } else { BroadcastStatus::instance()->setStatusMsg(i18n("Copying messages canceled.")); } } // The command will autodelete itself and will also kill the set. } void KMMainWidget::slotCopySelectedMessagesToFolder() { QPointer dialog(moveOrCopyToDialog()); dialog->setWindowTitle(i18n("Copy Messages to Folder")); if (dialog->exec() && dialog) { const Akonadi::Collection dest = dialog->selectedCollection(); if (dest.isValid()) { copySelectedMessagesToFolder(dest); } } } void KMMainWidget::copySelectedMessagesToFolder(const Akonadi::Collection &dest) { const Akonadi::Item::List lstMsg = mMessagePane->selectionAsMessageItemList(); if (!lstMsg.isEmpty()) { copyMessageSelected(lstMsg, dest); } } //----------------------------------------------------------------------------- // Message trashing // void KMMainWidget::trashMessageSelected(MessageList::Core::MessageItemSetReference ref) { if (!mCurrentFolder) { return; } const Akonadi::Item::List select = mMessagePane->itemListFromPersistentSet(ref); mMessagePane->markMessageItemsAsAboutToBeRemoved(ref, true); // FIXME: Why we don't use KMMoveCommand( trashFolder(), selectedMessages ); ? // And stuff them into a KMTrashMsgCommand :) KMCommand *command = new KMTrashMsgCommand(mCurrentFolder->collection(), select, ref); QObject::connect( command, SIGNAL(moveDone(KMMoveCommand*)), this, SLOT(slotTrashMessagesCompleted(KMMoveCommand*)) ); command->start(); BroadcastStatus::instance()->setStatusMsg(i18n("Moving messages to trash...")); } void KMMainWidget::slotTrashMessagesCompleted(KMMoveCommand *command) { Q_ASSERT(command); mMessagePane->markMessageItemsAsAboutToBeRemoved(command->refSet(), false); mMessagePane->deletePersistentSet(command->refSet()); if (command->result() == KMCommand::OK) { BroadcastStatus::instance()->setStatusMsg(i18n("Messages moved to trash successfully.")); } else { if (command->result() == KMCommand::Failed) { BroadcastStatus::instance()->setStatusMsg(i18n("Moving messages to trash failed.")); } else { BroadcastStatus::instance()->setStatusMsg(i18n("Moving messages to trash canceled.")); } } // The command will autodelete itself and will also kill the set. } void KMMainWidget::slotTrashSelectedMessages() { MessageList::Core::MessageItemSetReference ref = mMessagePane->selectionAsPersistentSet(); if (ref != -1) { trashMessageSelected(ref); } } void KMMainWidget::slotTrashThread() { MessageList::Core::MessageItemSetReference ref = mMessagePane->currentThreadAsPersistentSet(); if (ref != -1) { trashMessageSelected(ref); } } //----------------------------------------------------------------------------- // Message tag setting for messages // // FIXME: The "selection" version of these functions is in MessageActions. // We should probably move everything there.... void KMMainWidget::toggleMessageSetTag(const Akonadi::Item::List &select, const Akonadi::Tag &tag) { if (select.isEmpty()) { return; } KMCommand *command = new KMSetTagCommand(Akonadi::Tag::List() << tag, select, KMSetTagCommand::Toggle); command->start(); } void KMMainWidget::slotSelectMoreMessageTagList() { const Akonadi::Item::List selectedMessages = mMessagePane->selectionAsMessageItemList(); if (selectedMessages.isEmpty()) { return; } TagSelectDialog dlg(this, selectedMessages.count(), selectedMessages.first()); dlg.setActionCollection(QList() << actionCollection()); if (dlg.exec()) { const Akonadi::Tag::List lst = dlg.selectedTag(); KMCommand *command = new KMSetTagCommand(lst, selectedMessages, KMSetTagCommand::CleanExistingAndAddNew); command->start(); } } void KMMainWidget::slotUpdateMessageTagList(const Akonadi::Tag &tag) { // Create a persistent set from the current thread. const Akonadi::Item::List selectedMessages = mMessagePane->selectionAsMessageItemList(); if (selectedMessages.isEmpty()) { return; } toggleMessageSetTag(selectedMessages, tag); } void KMMainWidget::refreshMessageListSelection() { mAkonadiStandardActionManager->setItemSelectionModel(mMessagePane->currentItemSelectionModel()); slotMessageSelected(mMessagePane->currentItem()); } //----------------------------------------------------------------------------- // Status setting for threads // // FIXME: The "selection" version of these functions is in MessageActions. // We should probably move everything there.... void KMMainWidget::setMessageSetStatus(const Akonadi::Item::List &select, const Akonadi::MessageStatus &status, bool toggle) { KMCommand *command = new KMSetStatusCommand(status, select, toggle); command->start(); } void KMMainWidget::setCurrentThreadStatus(const Akonadi::MessageStatus &status, bool toggle) { const Akonadi::Item::List select = mMessagePane->currentThreadAsMessageList(); if (select.isEmpty()) { return; } setMessageSetStatus(select, status, toggle); } void KMMainWidget::slotSetThreadStatusUnread() { setCurrentThreadStatus(MessageStatus::statusRead(), true); } void KMMainWidget::slotSetThreadStatusImportant() { setCurrentThreadStatus(MessageStatus::statusImportant(), true); } void KMMainWidget::slotSetThreadStatusRead() { setCurrentThreadStatus(MessageStatus::statusRead(), false); } void KMMainWidget::slotSetThreadStatusToAct() { setCurrentThreadStatus(MessageStatus::statusToAct(), true); } void KMMainWidget::slotSetThreadStatusWatched() { setCurrentThreadStatus(MessageStatus::statusWatched(), true); if (mWatchThreadAction->isChecked()) { mIgnoreThreadAction->setChecked(false); } } void KMMainWidget::slotSetThreadStatusIgnored() { setCurrentThreadStatus(MessageStatus::statusIgnored(), true); if (mIgnoreThreadAction->isChecked()) { mWatchThreadAction->setChecked(false); } } //----------------------------------------------------------------------------- void KMMainWidget::slotRedirectMsg() { const Akonadi::Item::List selectedMessages = mMessagePane->selectionAsMessageItemList(); if (selectedMessages.isEmpty()) { return; } KMCommand *command = new KMRedirectCommand(this, selectedMessages); command->start(); } //----------------------------------------------------------------------------- void KMMainWidget::slotCustomReplyToMsg(const QString &tmpl) { const Akonadi::Item msg = mMessagePane->currentItem(); if (!msg.isValid()) { return; } const QString text = mMsgView ? mMsgView->copyText() : QString(); qCDebug(KMAIL_LOG) << "Reply with template:" << tmpl; KMCommand *command = new KMReplyCommand(this, msg, MessageComposer::ReplySmart, text, false, tmpl); command->start(); } //----------------------------------------------------------------------------- void KMMainWidget::slotCustomReplyAllToMsg(const QString &tmpl) { const Akonadi::Item msg = mMessagePane->currentItem(); if (!msg.isValid()) { return; } const QString text = mMsgView ? mMsgView->copyText() : QString(); qCDebug(KMAIL_LOG) << "Reply to All with template:" << tmpl; KMCommand *command = new KMReplyCommand(this, msg, MessageComposer::ReplyAll, text, false, tmpl ); command->start(); } //----------------------------------------------------------------------------- void KMMainWidget::slotCustomForwardMsg(const QString &tmpl) { if (!mCurrentFolder) { return; } const Akonadi::Item::List selectedMessages = mMessagePane->selectionAsMessageItemList(); if (selectedMessages.isEmpty()) { return; } qCDebug(KMAIL_LOG) << "Forward with template:" << tmpl; KMForwardCommand *command = new KMForwardCommand( this, selectedMessages, mCurrentFolder->identity(), tmpl ); command->start(); } void KMMainWidget::openFilterDialog(const QByteArray &field, const QString &value) { FilterIf->openFilterDialog(false); FilterIf->createFilter(field, value); } //----------------------------------------------------------------------------- void KMMainWidget::slotSubjectFilter() { const KMime::Message::Ptr msg = mMessagePane->currentMessage(); if (!msg) { return; } openFilterDialog("Subject", msg->subject()->asUnicodeString()); } //----------------------------------------------------------------------------- void KMMainWidget::slotFromFilter() { KMime::Message::Ptr msg = mMessagePane->currentMessage(); if (!msg) { return; } AddrSpecList al = MessageHelper::extractAddrSpecs(msg, "From"); if (al.empty()) { openFilterDialog("From", msg->from()->asUnicodeString()); } else { openFilterDialog("From", al.front().asString()); } } //----------------------------------------------------------------------------- void KMMainWidget::slotToFilter() { KMime::Message::Ptr msg = mMessagePane->currentMessage(); if (!msg) { return; } openFilterDialog("To", msg->to()->asUnicodeString()); } void KMMainWidget::slotCcFilter() { KMime::Message::Ptr msg = mMessagePane->currentMessage(); if (!msg) { return; } openFilterDialog("Cc", msg->cc()->asUnicodeString()); } void KMMainWidget::slotBandwidth(bool b) { PimCommon::NetworkUtil::self()->setLowBandwidth(b); } //----------------------------------------------------------------------------- void KMMainWidget::slotUndo() { kmkernel->undoStack()->undo(); updateMessageActions(); updateFolderMenu(); } //----------------------------------------------------------------------------- void KMMainWidget::slotJumpToFolder() { QPointer dialog(selectFromAllFoldersDialog()); dialog->setWindowTitle(i18n("Jump to Folder")); if (dialog->exec() && dialog) { Akonadi::Collection collection = dialog->selectedCollection(); if (collection.isValid()) { slotSelectCollectionFolder(collection); } } } void KMMainWidget::slotSelectCollectionFolder(const Akonadi::Collection &col) { if (mFolderTreeWidget) { mFolderTreeWidget->selectCollectionFolder(col); slotFolderChanged(col); } } void KMMainWidget::slotApplyFilters() { const Akonadi::Item::List selectedMessages = mMessagePane->selectionAsMessageItemList(); if (selectedMessages.isEmpty()) { return; } applyFilters(selectedMessages); } void KMMainWidget::slotApplyFiltersOnFolder() { if (mCurrentFolder && mCurrentFolder->collection().isValid()) { Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(mCurrentFolder->collection(), this); connect(job, &Akonadi::ItemFetchJob::result, this, &KMMainWidget::slotFetchItemsForFolderDone); } } void KMMainWidget::slotFetchItemsForFolderDone(KJob *job) { Akonadi::ItemFetchJob *fjob = dynamic_cast(job); Q_ASSERT(fjob); Akonadi::Item::List items = fjob->items(); applyFilters(items); } void KMMainWidget::applyFilters(const Akonadi::Item::List &selectedMessages) { #ifndef QT_NO_CURSOR KPIM::KCursorSaver busy(KPIM::KBusyPtr::busy()); #endif MailCommon::FilterManager::instance()->filter(selectedMessages); } //----------------------------------------------------------------------------- void KMMainWidget::slotCheckVacation() { updateVacationScriptStatus(false); if (!kmkernel->askToGoOnline()) { return; } mVacationManager->checkVacation(); connect(mVacationManager, SIGNAL(updateVacationScriptStatus(bool,QString)), SLOT(updateVacationScriptStatus(bool,QString))); connect(mVacationManager, SIGNAL(editVacation()), SLOT(slotEditVacation())); } void KMMainWidget::slotEditVacation(const QString &serverName) { if (!kmkernel->askToGoOnline()) { return; } mVacationManager->slotEditVacation(serverName); } //----------------------------------------------------------------------------- void KMMainWidget::slotDebugSieve() { #if !defined(NDEBUG) if (mSieveDebugDialog) { return; } mSieveDebugDialog = new KSieveUi::SieveDebugDialog(this); mSieveDebugDialog->exec(); delete mSieveDebugDialog; #endif } void KMMainWidget::slotConfigChanged() { readConfig(); mMsgActions->setupForwardActions(actionCollection()); mMsgActions->setupForwardingActionsList(mGUIClient); } //----------------------------------------------------------------------------- void KMMainWidget::slotSaveMsg() { const Akonadi::Item::List selectedMessages = mMessagePane->selectionAsMessageItemList(); if (selectedMessages.isEmpty()) { return; } KMSaveMsgCommand *saveCommand = new KMSaveMsgCommand(this, selectedMessages); saveCommand->start(); } //----------------------------------------------------------------------------- void KMMainWidget::slotOpenMsg() { KMOpenMsgCommand *openCommand = new KMOpenMsgCommand(this, QUrl(), overrideEncoding(), this); openCommand->start(); } //----------------------------------------------------------------------------- void KMMainWidget::slotSaveAttachments() { const Akonadi::Item::List selectedMessages = mMessagePane->selectionAsMessageItemList(); if (selectedMessages.isEmpty()) { return; } // Avoid re-downloading in the common case that only one message is selected, and the message // is also displayed in the viewer. For this, create a dummy item without a parent collection / item id, // so that KMCommand doesn't download it. KMSaveAttachmentsCommand *saveCommand = Q_NULLPTR; if (mMsgView && selectedMessages.size() == 1 && mMsgView->message().hasPayload() && selectedMessages.first().id() == mMsgView->message().id()) { Akonadi::Item dummyItem; dummyItem.setPayload(mMsgView->message().payload()); saveCommand = new KMSaveAttachmentsCommand(this, dummyItem, mMsgView->viewer()); } else { saveCommand = new KMSaveAttachmentsCommand(this, selectedMessages); } saveCommand->start(); } void KMMainWidget::slotOnlineStatus() { // KMKernel will Q_EMIT a signal when we toggle the network state that is caught by // KMMainWidget::slotUpdateOnlineStatus to update our GUI if (KMailSettings::self()->networkState() == KMailSettings::EnumNetworkState::Online) { // if online; then toggle and set it offline. kmkernel->stopNetworkJobs(); } else { kmkernel->resumeNetworkJobs(); slotCheckVacation(); } } void KMMainWidget::slotUpdateOnlineStatus(KMailSettings::EnumNetworkState::type) { if (!mAkonadiStandardActionManager) { return; } QAction *action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::ToggleWorkOffline); if (KMailSettings::self()->networkState() == KMailSettings::EnumNetworkState::Online) { action->setText(i18n("Work Offline")); action->setIcon(QIcon::fromTheme(QStringLiteral("user-offline"))); } else { action->setText(i18n("Work Online")); action->setIcon(QIcon::fromTheme(QStringLiteral("user-online"))); } } //----------------------------------------------------------------------------- void KMMainWidget::slotSendQueued() { if (kmkernel->msgSender()) { kmkernel->msgSender()->sendQueued(); } } //----------------------------------------------------------------------------- void KMMainWidget::slotSendQueuedVia(MailTransport::Transport *transport) { if (transport) { if (kmkernel->msgSender()) { kmkernel->msgSender()->sendQueued(transport->id()); } } } //----------------------------------------------------------------------------- void KMMainWidget::slotShowBusySplash() { if (mReaderWindowActive) { mMsgView->displayBusyPage(); } } void KMMainWidget::showOfflinePage() { if (!mReaderWindowActive) { return; } mMsgView->displayOfflinePage(); } void KMMainWidget::showResourceOfflinePage() { if (!mReaderWindowActive) { return; } mMsgView->displayResourceOfflinePage(); } //----------------------------------------------------------------------------- void KMMainWidget::slotReplaceMsgByUnencryptedVersion() { qCDebug(KMAIL_LOG); Akonadi::Item oldMsg = mMessagePane->currentItem(); if (oldMsg.isValid()) { #if 0 qCDebug(KMAIL_LOG) << "Old message found"; if (oldMsg->hasUnencryptedMsg()) { qCDebug(KMAIL_LOG) << "Extra unencrypted message found"; KMime::Message *newMsg = oldMsg->unencryptedMsg(); // adjust the message id { QString msgId(oldMsg->msgId()); QString prefix("DecryptedMsg."); int oldIdx = msgId.indexOf(prefix, 0, Qt::CaseInsensitive); if (-1 == oldIdx) { int leftAngle = msgId.lastIndexOf('<'); msgId = msgId.insert((-1 == leftAngle) ? 0 : ++leftAngle, prefix); } else { // toggle between "DecryptedMsg." and "DeCryptedMsg." // to avoid same message id QCharRef c = msgId[ oldIdx + 2 ]; if ('C' == c) { c = 'c'; } else { c = 'C'; } } newMsg->setMsgId(msgId); mMsgView->setIdOfLastViewedMessage(msgId); } // insert the unencrypted message qCDebug(KMAIL_LOG) << "Adding unencrypted message to folder"; mFolder->addMsg(newMsg); /* Figure out its index in the folder for selecting. This must be count()-1, * since we append. Be safe and do find, though, just in case. */ int newMsgIdx = mFolder->find(newMsg); Q_ASSERT(newMsgIdx != -1); /* we need this unget, to have the message displayed correctly initially */ mFolder->unGetMsg(newMsgIdx); int idx = mFolder->find(oldMsg); Q_ASSERT(idx != -1); /* only select here, so the old one is not un-Gotten before, which would * render the pointer we hold invalid so that find would fail */ #if 0 // FIXME (Pragma) mHeaders->setCurrentItemByIndex(newMsgIdx); #endif // remove the old one if (idx != -1) { qCDebug(KMAIL_LOG) << "Deleting encrypted message"; mFolder->take(idx); } qCDebug(KMAIL_LOG) << "Updating message actions"; updateMessageActions(); qCDebug(KMAIL_LOG) << "Done."; } else { qCDebug(KMAIL_LOG) << "NO EXTRA UNENCRYPTED MESSAGE FOUND"; } #else qCDebug(KMAIL_LOG) << "AKONADI PORT: Disabled code in " << Q_FUNC_INFO; #endif } else { qCDebug(KMAIL_LOG) << "PANIC: NO OLD MESSAGE FOUND"; } } void KMMainWidget::slotFocusOnNextMessage() { mMessagePane->focusNextMessageItem(MessageList::Core::MessageTypeAny, true, false); } void KMMainWidget::slotFocusOnPrevMessage() { mMessagePane->focusPreviousMessageItem(MessageList::Core::MessageTypeAny, true, false); } void KMMainWidget::slotSelectFirstMessage() { mMessagePane->selectFirstMessageItem(MessageList::Core::MessageTypeAny, true); } void KMMainWidget::slotSelectLastMessage() { mMessagePane->selectLastMessageItem(MessageList::Core::MessageTypeAny, true); } void KMMainWidget::slotSelectFocusedMessage() { mMessagePane->selectFocusedMessageItem(true); } void KMMainWidget::slotSelectNextMessage() { mMessagePane->selectNextMessageItem(MessageList::Core::MessageTypeAny, MessageList::Core::ClearExistingSelection, true, false); } void KMMainWidget::slotExtendSelectionToNextMessage() { mMessagePane->selectNextMessageItem( MessageList::Core::MessageTypeAny, MessageList::Core::GrowOrShrinkExistingSelection, true, // center item false // don't loop in folder ); } void KMMainWidget::slotSelectNextUnreadMessage() { // The looping logic is: "Don't loop" just never loops, "Loop in current folder" // loops just in current folder, "Loop in all folders" loops in the current folder // first and then after confirmation jumps to the next folder. // A bad point here is that if you answer "No, and don't ask me again" to the confirmation // dialog then you have "Loop in current folder" and "Loop in all folders" that do // the same thing and no way to get the old behaviour. However, after a consultation on #kontact, // for bug-to-bug backward compatibility, the masters decided to keep it b0rken :D // If nobody complains, it stays like it is: if you complain enough maybe the masters will // decide to reconsider :) if (!mMessagePane->selectNextMessageItem( MessageList::Core::MessageTypeUnreadOnly, MessageList::Core::ClearExistingSelection, true, // center item KMailSettings::self()->loopOnGotoUnread() != KMailSettings::EnumLoopOnGotoUnread::DontLoop )) { // no next unread message was found in the current folder if ((KMailSettings::self()->loopOnGotoUnread() == KMailSettings::EnumLoopOnGotoUnread::LoopInAllFolders) || (KMailSettings::self()->loopOnGotoUnread() == KMailSettings::EnumLoopOnGotoUnread::LoopInAllMarkedFolders)) { mGoToFirstUnreadMessageInSelectedFolder = true; mFolderTreeWidget->folderTreeView()->selectNextUnreadFolder(true); mGoToFirstUnreadMessageInSelectedFolder = false; } } } void KMMainWidget::slotSelectPreviousMessage() { mMessagePane->selectPreviousMessageItem(MessageList::Core::MessageTypeAny, MessageList::Core::ClearExistingSelection, true, false); } void KMMainWidget::slotExtendSelectionToPreviousMessage() { mMessagePane->selectPreviousMessageItem( MessageList::Core::MessageTypeAny, MessageList::Core::GrowOrShrinkExistingSelection, true, // center item false // don't loop in folder ); } void KMMainWidget::slotSearchButton() { if(mHideShowSearchBarAction->isChecked()) mMessagePane->show(); else mMessagePane->hide(); } void KMMainWidget::slotSelectPreviousUnreadMessage() { if (!mMessagePane->selectPreviousMessageItem( MessageList::Core::MessageTypeUnreadOnly, MessageList::Core::ClearExistingSelection, true, // center item KMailSettings::self()->loopOnGotoUnread() == KMailSettings::EnumLoopOnGotoUnread::LoopInCurrentFolder )) { // no next unread message was found in the current folder if ((KMailSettings::self()->loopOnGotoUnread() == KMailSettings::EnumLoopOnGotoUnread::LoopInAllFolders) || (KMailSettings::self()->loopOnGotoUnread() == KMailSettings::EnumLoopOnGotoUnread::LoopInAllMarkedFolders)) { mGoToFirstUnreadMessageInSelectedFolder = true; mFolderTreeWidget->folderTreeView()->selectPrevUnreadFolder(); mGoToFirstUnreadMessageInSelectedFolder = false; } } } void KMMainWidget::slotDisplayCurrentMessage() { if (mMessagePane->currentItem().isValid() && !mMessagePane->searchEditHasFocus()) { slotMessageActivated(mMessagePane->currentItem()); } } // Called by double-clicked or 'Enter' in the messagelist -> pop up reader window void KMMainWidget::slotMessageActivated(const Akonadi::Item &msg) { if (!mCurrentFolder || !msg.isValid()) { return; } if (CommonKernel->folderIsDraftOrOutbox(mCurrentFolder->collection())) { mMsgActions->setCurrentMessage(msg); mMsgActions->editCurrentMessage(); return; } if (CommonKernel->folderIsTemplates(mCurrentFolder->collection())) { slotUseTemplate(); return; } // Try to fetch the mail, even in offline mode, it might be cached KMFetchMessageCommand *cmd = new KMFetchMessageCommand(this, msg); connect(cmd, &KMCommand::completed, this, &KMMainWidget::slotItemsFetchedForActivation); cmd->start(); } void KMMainWidget::slotItemsFetchedForActivation(KMCommand *command) { KMCommand::Result result = command->result(); if (result != KMCommand::OK) { qCDebug(KMAIL_LOG) << "Result:" << result; return; } KMFetchMessageCommand *fetchCmd = qobject_cast(command); const Item msg = fetchCmd->item(); KMReaderMainWin *win = new KMReaderMainWin(mFolderDisplayFormatPreference, mFolderHtmlLoadExtPreference); const bool useFixedFont = mMsgView ? mMsgView->isFixedFont() : MessageViewer::MessageViewerSettings::self()->useFixedFont(); win->setUseFixedFont(useFixedFont); const Akonadi::Collection parentCollection = MailCommon::Util::parentCollectionFromItem(msg); win->showMessage(overrideEncoding(), msg, parentCollection); win->show(); } void KMMainWidget::slotMessageStatusChangeRequest(const Akonadi::Item &item, const Akonadi::MessageStatus &set, const Akonadi::MessageStatus &clear) { if (!item.isValid()) { return; } if (clear.toQInt32() != Akonadi::MessageStatus().toQInt32()) { KMCommand *command = new KMSetStatusCommand(clear, Akonadi::Item::List() << item, true); command->start(); } if (set.toQInt32() != Akonadi::MessageStatus().toQInt32()) { KMCommand *command = new KMSetStatusCommand(set, Akonadi::Item::List() << item, false); command->start(); } } //----------------------------------------------------------------------------- void KMMainWidget::slotSelectAllMessages() { mMessagePane->selectAll(); updateMessageActions(); } void KMMainWidget::slotMessagePopup(const Akonadi::Item &msg, const QUrl &aUrl, const QUrl &imageUrl, const QPoint &aPoint) { updateMessageMenu(); const QString email = KEmailAddress::firstEmailAddress(aUrl.path()).toLower(); if (aUrl.scheme() == QLatin1String("mailto") && !email.isEmpty()) { Akonadi::ContactSearchJob *job = new Akonadi::ContactSearchJob(this); job->setLimit(1); job->setQuery(Akonadi::ContactSearchJob::Email, email, Akonadi::ContactSearchJob::ExactMatch); job->setProperty("msg", QVariant::fromValue(msg)); job->setProperty("point", aPoint); job->setProperty("imageUrl", imageUrl); job->setProperty("url", aUrl); connect(job, &Akonadi::ContactSearchJob::result, this, &KMMainWidget::slotContactSearchJobForMessagePopupDone); } else { showMessagePopup(msg, aUrl, imageUrl, aPoint, false, false); } } void KMMainWidget::slotContactSearchJobForMessagePopupDone(KJob *job) { const Akonadi::ContactSearchJob *searchJob = qobject_cast(job); const bool contactAlreadyExists = !searchJob->contacts().isEmpty(); const Akonadi::Item::List listContact = searchJob->items(); const bool uniqueContactFound = (listContact.count() == 1); if (uniqueContactFound) { mMsgView->setContactItem(listContact.first(), searchJob->contacts().at(0)); } else { mMsgView->clearContactItem(); } const Akonadi::Item msg = job->property("msg").value(); const QPoint aPoint = job->property("point").toPoint(); const QUrl imageUrl = job->property("imageUrl").toUrl(); const QUrl url = job->property("url").toUrl(); showMessagePopup(msg, url, imageUrl, aPoint, contactAlreadyExists, uniqueContactFound); } void KMMainWidget::showMessagePopup(const Akonadi::Item &msg, const QUrl &url, const QUrl &imageUrl, const QPoint &aPoint, bool contactAlreadyExists, bool uniqueContactFound) { QMenu *menu = new QMenu; bool urlMenuAdded = false; if (!url.isEmpty()) { if (url.scheme() == QLatin1String("mailto")) { // popup on a mailto URL menu->addAction(mMsgView->mailToComposeAction()); menu->addAction(mMsgView->mailToReplyAction()); menu->addAction(mMsgView->mailToForwardAction()); menu->addSeparator(); if (contactAlreadyExists) { if (uniqueContactFound) { menu->addAction(mMsgView->editContactAction()); } else { menu->addAction(mMsgView->openAddrBookAction()); } } else { menu->addAction(mMsgView->addAddrBookAction()); menu->addAction(mMsgView->addToExistingContactAction()); } menu->addSeparator(); menu->addMenu(mMsgView->viewHtmlOption()); menu->addSeparator(); menu->addAction(mMsgView->copyURLAction()); urlMenuAdded = true; } else if (url.scheme() != QLatin1String("attachment")) { // popup on a not-mailto URL menu->addAction(mMsgView->urlOpenAction()); menu->addAction(mMsgView->addBookmarksAction()); menu->addAction(mMsgView->urlSaveAsAction()); menu->addAction(mMsgView->copyURLAction()); menu->addSeparator(); menu->addAction(mMsgView->shareServiceUrlMenu()); if (mMsgView->isAShortUrl(url)) { menu->addSeparator(); menu->addAction(mMsgView->expandShortUrlAction()); } if (!imageUrl.isEmpty()) { menu->addSeparator(); menu->addAction(mMsgView->copyImageLocation()); menu->addAction(mMsgView->downloadImageToDiskAction()); menu->addAction(mMsgView->shareImage()); if (mMsgView->adblockEnabled()) { menu->addSeparator(); menu->addAction(mMsgView->blockImage()); } } urlMenuAdded = true; } qCDebug(KMAIL_LOG) << "URL is:" << url; } const QString selectedText = mMsgView ? mMsgView->copyText() : QString(); if (mMsgView && !selectedText.isEmpty()) { if (urlMenuAdded) { menu->addSeparator(); } menu->addAction(mMsgActions->replyMenu()); menu->addSeparator(); menu->addAction(mMsgView->copyAction()); menu->addAction(mMsgView->selectAllAction()); menu->addSeparator(); mMsgActions->addWebShortcutsMenu(menu, selectedText); menu->addSeparator(); menu->addActions(mMsgView->viewerPluginActionList(MessageViewer::ViewerPluginInterface::NeedSelection)); if (KPIMTextEdit::TextToSpeech::self()->isReady()) { menu->addSeparator(); menu->addAction(mMsgView->speakTextAction()); } } else if (!urlMenuAdded) { // popup somewhere else (i.e., not a URL) on the message if (!mMessagePane->currentMessage()) { // no messages delete menu; return; } Akonadi::Collection parentCol = msg.parentCollection(); if (parentCol.isValid() && CommonKernel->folderIsTemplates(parentCol)) { menu->addAction(mUseAction); } else { menu->addAction(mMsgActions->replyMenu()); menu->addAction(mMsgActions->forwardMenu()); } if (parentCol.isValid() && CommonKernel->folderIsSentMailFolder(parentCol)) { menu->addAction(sendAgainAction()); } else { menu->addAction(editAction()); } menu->addAction(mailingListActionMenu()); menu->addSeparator(); menu->addAction(mCopyActionMenu); menu->addAction(mMoveActionMenu); menu->addSeparator(); menu->addAction(mMsgActions->messageStatusMenu()); menu->addSeparator(); if (mMsgView) { if (!imageUrl.isEmpty()) { menu->addSeparator(); menu->addAction(mMsgView->copyImageLocation()); menu->addAction(mMsgView->downloadImageToDiskAction()); menu->addAction(mMsgView->shareImage()); menu->addSeparator(); if (mMsgView->adblockEnabled()) { menu->addAction(mMsgView->blockImage()); menu->addSeparator(); } } menu->addAction(mMsgView->viewSourceAction()); menu->addAction(mMsgView->toggleFixFontAction()); menu->addAction(mMsgView->toggleMimePartTreeAction()); } menu->addSeparator(); if (mMsgActions->printPreviewAction()) { menu->addAction(mMsgActions->printPreviewAction()); } menu->addAction(mMsgActions->printAction()); menu->addAction(mSaveAsAction); menu->addAction(mSaveAttachmentsAction); menu->addSeparator(); if (parentCol.isValid() && CommonKernel->folderIsTrash(parentCol)) { menu->addAction(mDeleteAction); } else { menu->addAction(akonadiStandardAction(Akonadi::StandardMailActionManager::MoveToTrash)); } menu->addSeparator(); if (mMsgView) { menu->addActions(mMsgView->viewerPluginActionList(MessageViewer::ViewerPluginInterface::NeedMessage)); menu->addSeparator(); menu->addAction(mMsgView->saveMessageDisplayFormatAction()); menu->addAction(mMsgView->resetMessageDisplayFormatAction()); menu->addSeparator(); } menu->addAction(mMsgActions->annotateAction()); if (mMsgView && mMsgView->adblockEnabled()) { menu->addSeparator(); menu->addAction(mMsgView->openBlockableItems()); } menu->addSeparator(); menu->addAction(mMsgActions->addFollowupReminderAction()); if (kmkernel->allowToDebugBalooSupport()) { menu->addSeparator(); menu->addAction(mMsgActions->debugBalooAction()); } } KAcceleratorManager::manage(menu); menu->exec(aPoint, Q_NULLPTR); delete menu; } void KMMainWidget::setupActions() { mMsgActions = new KMail::MessageActions(actionCollection(), this); mMsgActions->setMessageView(mMsgView); //----- File Menu mSaveAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save &As..."), this); actionCollection()->addAction(QStringLiteral("file_save_as"), mSaveAsAction); connect(mSaveAsAction, &QAction::triggered, this, &KMMainWidget::slotSaveMsg); actionCollection()->setDefaultShortcut(mSaveAsAction, KStandardShortcut::save().first()); mOpenAction = KStandardAction::open(this, SLOT(slotOpenMsg()), actionCollection()); mOpenRecentAction = KStandardAction::openRecent(this, SLOT(slotOpenRecentMsg(QUrl)), actionCollection()); KConfigGroup grp = mConfig->group(QStringLiteral("Recent Files")); mOpenRecentAction->loadEntries(grp); { QAction *action = new QAction(i18n("&Expire All Folders"), this); actionCollection()->addAction(QStringLiteral("expire_all_folders"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotExpireAll); } { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("mail-receive")), i18n("Check &Mail"), this); actionCollection()->addAction(QStringLiteral("check_mail"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotCheckMail); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_L)); } mAccountActionMenu = new KActionMenuAccount(this); mAccountActionMenu->setIcon(QIcon::fromTheme(QStringLiteral("mail-receive"))); mAccountActionMenu->setText(i18n("Check Mail In")); mAccountActionMenu->setIconText(i18n("Check Mail")); mAccountActionMenu->setToolTip(i18n("Check Mail")); actionCollection()->addAction(QStringLiteral("check_mail_in"), mAccountActionMenu); connect(mAccountActionMenu, &KActionMenu::triggered, this, &KMMainWidget::slotCheckMail); mSendQueued = new QAction(QIcon::fromTheme(QStringLiteral("mail-send")), i18n("&Send Queued Messages"), this); actionCollection()->addAction(QStringLiteral("send_queued"), mSendQueued); connect(mSendQueued, &QAction::triggered, this, &KMMainWidget::slotSendQueued); { QAction *action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::ToggleWorkOffline); mAkonadiStandardActionManager->interceptAction(Akonadi::StandardActionManager::ToggleWorkOffline); action->setCheckable(false); connect(action, &QAction::triggered, this, &KMMainWidget::slotOnlineStatus); action->setText(i18n("Online status (unknown)")); } mSendActionMenu = new KActionMenuTransport(this); mSendActionMenu->setIcon(QIcon::fromTheme(QStringLiteral("mail-send-via"))); mSendActionMenu->setText(i18n("Send Queued Messages Via")); actionCollection()->addAction(QStringLiteral("send_queued_via"), mSendActionMenu); connect(mSendActionMenu, &KActionMenuTransport::transportSelected, this, &KMMainWidget::slotSendQueuedVia); //----- Tools menu if (parent()->inherits("KMMainWin")) { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("x-office-address-book")), i18n("&Address Book"), this); actionCollection()->addAction(QStringLiteral("addressbook"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotAddrBook); if (QStandardPaths::findExecutable(QStringLiteral("kaddressbook")).isEmpty()) { action->setEnabled(false); } } { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("pgp-keys")), i18n("Certificate Manager"), this); actionCollection()->addAction(QStringLiteral("tools_start_certman"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotStartCertManager); // disable action if no certman binary is around if (QStandardPaths::findExecutable(QStringLiteral("kleopatra")).isEmpty()) { action->setEnabled(false); } } { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("pgp-keys")), i18n("GnuPG Log Viewer"), this); actionCollection()->addAction(QStringLiteral("tools_start_kwatchgnupg"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotStartWatchGnuPG); #ifdef Q_OS_WIN32 // not ported yet, underlying infrastructure missing on Windows const bool usableKWatchGnupg = false; #else // disable action if no kwatchgnupg binary is around bool usableKWatchGnupg = !QStandardPaths::findExecutable(QStringLiteral("kwatchgnupg")).isEmpty(); #endif action->setEnabled(usableKWatchGnupg); } { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("document-import")), i18n("&Import Messages..."), this); actionCollection()->addAction(QStringLiteral("import"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotImport); if (QStandardPaths::findExecutable(QStringLiteral("importwizard")).isEmpty()) { action->setEnabled(false); } } #if !defined(NDEBUG) { QAction *action = new QAction(i18n("&Debug Sieve..."), this); actionCollection()->addAction(QStringLiteral("tools_debug_sieve"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotDebugSieve); } #endif { QAction *action = new QAction(i18n("Filter &Log Viewer..."), this); actionCollection()->addAction(QStringLiteral("filter_log_viewer"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotFilterLogViewer); } { QAction *action = new QAction(i18n("&Anti-Spam Wizard..."), this); actionCollection()->addAction(QStringLiteral("antiSpamWizard"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotAntiSpamWizard); } { QAction *action = new QAction(i18n("&Anti-Virus Wizard..."), this); actionCollection()->addAction(QStringLiteral("antiVirusWizard"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotAntiVirusWizard); } { QAction *action = new QAction(i18n("&Account Wizard..."), this); actionCollection()->addAction(QStringLiteral("accountWizard"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotAccountWizard); } { QAction *action = new QAction(i18n("&Import Wizard..."), this); actionCollection()->addAction(QStringLiteral("importWizard"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotImportWizard); } if (KSieveUi::Util::allowOutOfOfficeSettings()) { QAction *action = new QAction(i18n("Edit \"Out of Office\" Replies..."), this); actionCollection()->addAction(QStringLiteral("tools_edit_vacation"), action); connect(action, SIGNAL(triggered(bool)), SLOT(slotEditVacation())); } { QAction *action = new QAction(i18n("&Configure Automatic Archiving..."), this); actionCollection()->addAction(QStringLiteral("tools_automatic_archiving"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotConfigureAutomaticArchiving); } { QAction *action = new QAction(i18n("Delayed Messages..."), this); actionCollection()->addAction(QStringLiteral("message_delayed"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotConfigureSendLater); } { QAction *action = new QAction(i18n("Followup Reminder Messages..."), this); actionCollection()->addAction(QStringLiteral("followup_reminder_messages"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotConfigureFollowupReminder); } // Disable the standard action delete key sortcut. QAction *const standardDelAction = akonadiStandardAction(Akonadi::StandardActionManager::DeleteItems); standardDelAction->setShortcut(QKeySequence()); //----- Edit Menu /* The delete action is nowhere in the gui, by default, so we need to make * sure it is plugged into the KAccel now, since that won't happen on * XMLGui construction or manual ->plug(). This is only a problem when run * as a part, though. */ mDeleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action Hard delete, bypassing trash", "&Delete"), this); actionCollection()->addAction(QStringLiteral("delete"), mDeleteAction); connect(mDeleteAction, &QAction::triggered, this, &KMMainWidget::slotDeleteMessages); actionCollection()->setDefaultShortcut(mDeleteAction, QKeySequence(Qt::SHIFT + Qt::Key_Delete)); mTrashThreadAction = new QAction(i18n("M&ove Thread to Trash"), this); actionCollection()->addAction(QStringLiteral("move_thread_to_trash"), mTrashThreadAction); actionCollection()->setDefaultShortcut(mTrashThreadAction, QKeySequence(Qt::CTRL + Qt::Key_Delete)); mTrashThreadAction->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); KMail::Util::addQActionHelpText(mTrashThreadAction, i18n("Move thread to trashcan")); connect(mTrashThreadAction, &QAction::triggered, this, &KMMainWidget::slotTrashThread); mDeleteThreadAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete T&hread"), this); actionCollection()->addAction(QStringLiteral("delete_thread"), mDeleteThreadAction); //Don't use new connect api. connect(mDeleteThreadAction, SIGNAL(triggered(bool)), this, SLOT(slotDeleteThread())); actionCollection()->setDefaultShortcut(mDeleteThreadAction, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Delete)); mSearchMessages = new QAction(QIcon::fromTheme(QStringLiteral("edit-find-mail")), i18n("&Find Messages..."), this); actionCollection()->addAction(QStringLiteral("search_messages"), mSearchMessages); connect(mSearchMessages, &QAction::triggered, this, &KMMainWidget::slotRequestFullSearchFromQuickSearch); actionCollection()->setDefaultShortcut(mSearchMessages, QKeySequence(Qt::Key_S)); { QAction *action = new QAction(i18n("Select &All Messages"), this); actionCollection()->addAction(QStringLiteral("mark_all_messages"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotSelectAllMessages); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_A)); } //----- Folder Menu mFolderMailingListPropertiesAction = new QAction(i18n("&Mailing List Management..."), this); actionCollection()->addAction(QStringLiteral("folder_mailinglist_properties"), mFolderMailingListPropertiesAction); connect(mFolderMailingListPropertiesAction, &QAction::triggered, mManageShowCollectionProperties, &ManageShowCollectionProperties::slotFolderMailingListProperties); // mFolderMailingListPropertiesAction->setIcon(QIcon::fromTheme("document-properties-mailing-list")); mShowFolderShortcutDialogAction = new QAction(QIcon::fromTheme(QStringLiteral("configure-shortcuts")), i18n("&Assign Shortcut..."), this); actionCollection()->addAction(QStringLiteral("folder_shortcut_command"), mShowFolderShortcutDialogAction); connect(mShowFolderShortcutDialogAction, &QAction::triggered, mManageShowCollectionProperties, &ManageShowCollectionProperties::slotShowFolderShortcutDialog); // FIXME: this action is not currently enabled in the rc file, but even if // it were there is inconsistency between the action name and action. // "Expiration Settings" implies that this will lead to a settings dialogue // and it should be followed by a "...", but slotExpireFolder() performs // an immediate expiry. // // TODO: expire action should be disabled if there is no content or if // the folder can't delete messages. // // Leaving the action here for the moment, it and the "Expire" option in the // folder popup menu should be combined or at least made consistent. Same for // slotExpireFolder() and FolderViewItem::slotShowExpiryProperties(). mExpireFolderAction = new QAction(i18n("&Expiration Settings"), this); actionCollection()->addAction(QStringLiteral("expire"), mExpireFolderAction); connect(mExpireFolderAction, &QAction::triggered, this, &KMMainWidget::slotExpireFolder); mAkonadiStandardActionManager->interceptAction(Akonadi::StandardMailActionManager::MoveToTrash); connect(mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::MoveToTrash), &QAction::triggered, this, &KMMainWidget::slotTrashSelectedMessages); mAkonadiStandardActionManager->interceptAction(Akonadi::StandardMailActionManager::MoveAllToTrash); connect(mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::MoveAllToTrash), &QAction::triggered, this, &KMMainWidget::slotEmptyFolder); mAkonadiStandardActionManager->interceptAction(Akonadi::StandardActionManager::DeleteCollections); connect(mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::DeleteCollections), &QAction::triggered, this, &KMMainWidget::slotRemoveFolder); // ### PORT ME: Add this to the context menu. Not possible right now because // the context menu uses XMLGUI, and that would add the entry to // all collection context menus mArchiveFolderAction = new QAction(i18n("&Archive Folder..."), this); actionCollection()->addAction(QStringLiteral("archive_folder"), mArchiveFolderAction); connect(mArchiveFolderAction, &QAction::triggered, this, &KMMainWidget::slotArchiveFolder); mDisplayMessageFormatMenu = new DisplayMessageFormatActionMenu(this); connect(mDisplayMessageFormatMenu, &DisplayMessageFormatActionMenu::changeDisplayMessageFormat, this, &KMMainWidget::slotChangeDisplayMessageFormat); actionCollection()->addAction(QStringLiteral("display_format_message"), mDisplayMessageFormatMenu); mPreferHtmlLoadExtAction = new KToggleAction(i18n("Load E&xternal References"), this); actionCollection()->addAction(QStringLiteral("prefer_html_external_refs"), mPreferHtmlLoadExtAction); connect(mPreferHtmlLoadExtAction, &KToggleAction::triggered, this, &KMMainWidget::slotOverrideHtmlLoadExt); { QAction *action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::CopyCollections); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_C)); } { QAction *action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::Paste); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_V)); } { QAction *action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::CopyItems); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::CTRL + Qt::Key_C)); } { QAction *action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::CutItems); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::CTRL + Qt::Key_X)); } { QAction *action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::CopyItemToMenu); action->setText(i18n("Copy Message To...")); action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::MoveItemToMenu); action->setText(i18n("Move Message To...")); } //----- Message Menu { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("mail-message-new")), i18n("&New Message..."), this); actionCollection()->addAction(QStringLiteral("new_message"), action); action->setIconText(i18nc("@action:intoolbar New Empty Message", "New")); connect(action, &QAction::triggered, this, &KMMainWidget::slotCompose); // do not set a New shortcut if kmail is a component if (!kmkernel->xmlGuiInstanceName().isEmpty()) { actionCollection()->setDefaultShortcut(action, KStandardShortcut::openNew().first()); } } mTemplateMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Message From &Template"), actionCollection()); mTemplateMenu->setDelayed(true); actionCollection()->addAction(QStringLiteral("new_from_template"), mTemplateMenu); connect(mTemplateMenu->menu(), &QMenu::aboutToShow, this, &KMMainWidget::slotShowNewFromTemplate); connect(mTemplateMenu->menu(), &QMenu::triggered, this, &KMMainWidget::slotNewFromTemplate); mMessageNewList = new QAction(QIcon::fromTheme(QStringLiteral("mail-message-new-list")), i18n("New Message t&o Mailing-List..."), this); actionCollection()->addAction(QStringLiteral("post_message"), mMessageNewList); connect(mMessageNewList, &QAction::triggered, this, &KMMainWidget::slotPostToML); actionCollection()->setDefaultShortcut(mMessageNewList, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_N)); mSendAgainAction = new QAction(i18n("Send A&gain..."), this); actionCollection()->addAction(QStringLiteral("send_again"), mSendAgainAction); connect(mSendAgainAction, &QAction::triggered, this, &KMMainWidget::slotResendMsg); //----- Create filter actions mFilterMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("view-filter")), i18n("&Create Filter"), this); actionCollection()->addAction(QStringLiteral("create_filter"), mFilterMenu); connect(mFilterMenu, &QAction::triggered, this, &KMMainWidget::slotFilter); { QAction *action = new QAction(i18n("Filter on &Subject..."), this); actionCollection()->addAction(QStringLiteral("subject_filter"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotSubjectFilter); mFilterMenu->addAction(action); } { QAction *action = new QAction(i18n("Filter on &From..."), this); actionCollection()->addAction(QStringLiteral("from_filter"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotFromFilter); mFilterMenu->addAction(action); } { QAction *action = new QAction(i18n("Filter on &To..."), this); actionCollection()->addAction(QStringLiteral("to_filter"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotToFilter); mFilterMenu->addAction(action); } { QAction *action = new QAction(i18n("Filter on &Cc..."), this); actionCollection()->addAction(QStringLiteral("cc_filter"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotCcFilter); mFilterMenu->addAction(action); } mFilterMenu->addAction(mMsgActions->listFilterAction()); mUseAction = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("New Message From &Template"), this); actionCollection()->addAction(QStringLiteral("use_template"), mUseAction); connect(mUseAction, &QAction::triggered, this, &KMMainWidget::slotUseTemplate); actionCollection()->setDefaultShortcut(mUseAction, QKeySequence(Qt::SHIFT + Qt::Key_N)); //----- "Mark Thread" submenu mThreadStatusMenu = new KActionMenu(i18n("Mark &Thread"), this); actionCollection()->addAction(QStringLiteral("thread_status"), mThreadStatusMenu); mMarkThreadAsReadAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-mark-read")), i18n("Mark Thread as &Read"), this); actionCollection()->addAction(QStringLiteral("thread_read"), mMarkThreadAsReadAction); connect(mMarkThreadAsReadAction, &QAction::triggered, this, &KMMainWidget::slotSetThreadStatusRead); KMail::Util::addQActionHelpText(mMarkThreadAsReadAction, i18n("Mark all messages in the selected thread as read")); mThreadStatusMenu->addAction(mMarkThreadAsReadAction); mMarkThreadAsUnreadAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-mark-unread")), i18n("Mark Thread as &Unread"), this); actionCollection()->addAction(QStringLiteral("thread_unread"), mMarkThreadAsUnreadAction); connect(mMarkThreadAsUnreadAction, &QAction::triggered, this, &KMMainWidget::slotSetThreadStatusUnread); KMail::Util::addQActionHelpText(mMarkThreadAsUnreadAction, i18n("Mark all messages in the selected thread as unread")); mThreadStatusMenu->addAction(mMarkThreadAsUnreadAction); mThreadStatusMenu->addSeparator(); //----- "Mark Thread" toggle actions mToggleThreadImportantAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("mail-mark-important")), i18n("Mark Thread as &Important"), this); actionCollection()->addAction(QStringLiteral("thread_flag"), mToggleThreadImportantAction); connect(mToggleThreadImportantAction, &KToggleAction::triggered, this, &KMMainWidget::slotSetThreadStatusImportant); mToggleThreadImportantAction->setCheckedState(KGuiItem(i18n("Remove &Important Thread Mark"))); mThreadStatusMenu->addAction(mToggleThreadImportantAction); mToggleThreadToActAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("mail-mark-task")), i18n("Mark Thread as &Action Item"), this); actionCollection()->addAction(QStringLiteral("thread_toact"), mToggleThreadToActAction); connect(mToggleThreadToActAction, &KToggleAction::triggered, this, &KMMainWidget::slotSetThreadStatusToAct); mToggleThreadToActAction->setCheckedState(KGuiItem(i18n("Remove &Action Item Thread Mark"))); mThreadStatusMenu->addAction(mToggleThreadToActAction); //------- "Watch and ignore thread" actions mWatchThreadAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("mail-thread-watch")), i18n("&Watch Thread"), this); actionCollection()->addAction(QStringLiteral("thread_watched"), mWatchThreadAction); connect(mWatchThreadAction, &KToggleAction::triggered, this, &KMMainWidget::slotSetThreadStatusWatched); mIgnoreThreadAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("mail-thread-ignored")), i18n("&Ignore Thread"), this); actionCollection()->addAction(QStringLiteral("thread_ignored"), mIgnoreThreadAction); connect(mIgnoreThreadAction, &KToggleAction::triggered, this, &KMMainWidget::slotSetThreadStatusIgnored); mThreadStatusMenu->addSeparator(); mThreadStatusMenu->addAction(mWatchThreadAction); mThreadStatusMenu->addAction(mIgnoreThreadAction); mSaveAttachmentsAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Save A&ttachments..."), this); actionCollection()->addAction(QStringLiteral("file_save_attachments"), mSaveAttachmentsAction); connect(mSaveAttachmentsAction, &QAction::triggered, this, &KMMainWidget::slotSaveAttachments); mMoveActionMenu = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::MoveItemToMenu); mCopyActionMenu = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::CopyItemToMenu); mApplyAllFiltersAction = new QAction(QIcon::fromTheme(QStringLiteral("view-filter")), i18n("Appl&y All Filters"), this); actionCollection()->addAction(QStringLiteral("apply_filters"), mApplyAllFiltersAction); connect(mApplyAllFiltersAction, &QAction::triggered, this, &KMMainWidget::slotApplyFilters); actionCollection()->setDefaultShortcut(mApplyAllFiltersAction, QKeySequence(Qt::CTRL + Qt::Key_J)); mApplyFilterActionsMenu = new KActionMenu(i18n("A&pply Filter"), this); actionCollection()->addAction(QStringLiteral("apply_filter_actions"), mApplyFilterActionsMenu); { QAction *action = new QAction(i18nc("View->", "&Expand Thread / Group"), this); actionCollection()->addAction(QStringLiteral("expand_thread"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::Key_Period)); KMail::Util::addQActionHelpText(action, i18n("Expand the current thread or group")); connect(action, &QAction::triggered, this, &KMMainWidget::slotExpandThread); } { QAction *action = new QAction(i18nc("View->", "&Collapse Thread / Group"), this); actionCollection()->addAction(QStringLiteral("collapse_thread"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::Key_Comma)); KMail::Util::addQActionHelpText(action, i18n("Collapse the current thread or group")); connect(action, &QAction::triggered, this, &KMMainWidget::slotCollapseThread); } { QAction *action = new QAction(i18nc("View->", "Ex&pand All Threads"), this); actionCollection()->addAction(QStringLiteral("expand_all_threads"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Period)); KMail::Util::addQActionHelpText(action, i18n("Expand all threads in the current folder")); connect(action, &QAction::triggered, this, &KMMainWidget::slotExpandAllThreads); } { QAction *action = new QAction(i18nc("View->", "C&ollapse All Threads"), this); actionCollection()->addAction(QStringLiteral("collapse_all_threads"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Comma)); KMail::Util::addQActionHelpText(action, i18n("Collapse all threads in the current folder")); connect(action, &QAction::triggered, this, &KMMainWidget::slotCollapseAllThreads); } QAction *dukeOfMonmoth = new QAction(i18n("&Display Message"), this); actionCollection()->addAction(QStringLiteral("display_message"), dukeOfMonmoth); connect(dukeOfMonmoth, &QAction::triggered, this, &KMMainWidget::slotDisplayCurrentMessage); QList shortcuts; shortcuts << QKeySequence(Qt::Key_Enter) << QKeySequence(Qt::Key_Return); actionCollection()->setDefaultShortcuts(dukeOfMonmoth, shortcuts); //----- Go Menu { QAction *action = new QAction(i18n("&Next Message"), this); actionCollection()->addAction(QStringLiteral("go_next_message"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(QStringLiteral("N; Right"))); KMail::Util::addQActionHelpText(action, i18n("Go to the next message")); connect(action, &QAction::triggered, this, &KMMainWidget::slotSelectNextMessage); } { QAction *action = new QAction(i18n("Next &Unread Message"), this); actionCollection()->addAction(QStringLiteral("go_next_unread_message"), action); actionCollection()->setDefaultShortcuts(action, QList() << QKeySequence(Qt::Key_Plus) << QKeySequence(Qt::Key_Plus + Qt::KeypadModifier)); if (QApplication::isRightToLeft()) { action->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); } else { action->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); } action->setIconText(i18nc("@action:inmenu Goto next unread message", "Next")); KMail::Util::addQActionHelpText(action, i18n("Go to the next unread message")); connect(action, &QAction::triggered, this, &KMMainWidget::slotSelectNextUnreadMessage); } { QAction *action = new QAction(i18n("&Previous Message"), this); actionCollection()->addAction(QStringLiteral("go_prev_message"), action); KMail::Util::addQActionHelpText(action, i18n("Go to the previous message")); actionCollection()->setDefaultShortcut(action, QKeySequence(QStringLiteral("P; Left"))); connect(action, &QAction::triggered, this, &KMMainWidget::slotSelectPreviousMessage); } { mHideShowSearchBarAction = new QAction(i18n("Search"), this); actionCollection()->addAction(QStringLiteral("search_button"), mHideShowSearchBarAction); mHideShowSearchBarAction->setIcon(QIcon::fromTheme(QStringLiteral("go-search"))); mHideShowSearchBarAction->setCheckable(true); mHideShowSearchBarAction->setChecked(false); connect(mHideShowSearchBarAction, &QAction::triggered, this, &KMMainWidget::slotSearchButton); } { QAction *action = new QAction(i18n("Previous Unread &Message"), this); actionCollection()->addAction(QStringLiteral("go_prev_unread_message"), action); actionCollection()->setDefaultShortcuts(action, QList() << QKeySequence(Qt::Key_Minus) << QKeySequence(Qt::Key_Minus + Qt::KeypadModifier)); if (QApplication::isRightToLeft()) { action->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); } else { action->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); } action->setIconText(i18nc("@action:inmenu Goto previous unread message.", "Previous")); KMail::Util::addQActionHelpText(action, i18n("Go to the previous unread message")); connect(action, &QAction::triggered, this, &KMMainWidget::slotSelectPreviousUnreadMessage); } { QAction *action = new QAction(i18n("Next Unread &Folder"), this); actionCollection()->addAction(QStringLiteral("go_next_unread_folder"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotNextUnreadFolder); actionCollection()->setDefaultShortcuts(action, QList() << QKeySequence(Qt::ALT + Qt::Key_Plus) << QKeySequence(Qt::ALT + Qt::Key_Plus + Qt::KeypadModifier)); KMail::Util::addQActionHelpText(action, i18n("Go to the next folder with unread messages")); } { QAction *action = new QAction(i18n("Previous Unread F&older"), this); actionCollection()->addAction(QStringLiteral("go_prev_unread_folder"), action); actionCollection()->setDefaultShortcuts(action, QList() << QKeySequence(Qt::ALT + Qt::Key_Minus) << QKeySequence(Qt::ALT + Qt::Key_Minus + Qt::KeypadModifier)); KMail::Util::addQActionHelpText(action, i18n("Go to the previous folder with unread messages")); connect(action, &QAction::triggered, this, &KMMainWidget::slotPrevUnreadFolder); } { QAction *action = new QAction(i18nc("Go->", "Next Unread &Text"), this); actionCollection()->addAction(QStringLiteral("go_next_unread_text"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::Key_Space)); KMail::Util::addQActionHelpText(action, i18n("Go to the next unread text")); action->setWhatsThis(i18n("Scroll down current message. " "If at end of current message, " "go to next unread message.")); connect(action, &QAction::triggered, this, &KMMainWidget::slotReadOn); } //----- Settings Menu { QAction *action = new QAction(i18n("Configure &Filters..."), this); action->setMenuRole(QAction::NoRole); // do not move to application menu on OS X actionCollection()->addAction(QStringLiteral("filter"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotFilter); } { QAction *action = new QAction(i18n("Manage &Sieve Scripts..."), this); actionCollection()->addAction(QStringLiteral("sieveFilters"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotManageSieveScripts); } { mShowIntroductionAction = new QAction(QIcon::fromTheme(QStringLiteral("kmail")), i18n("KMail &Introduction"), this); actionCollection()->addAction(QStringLiteral("help_kmail_welcomepage"), mShowIntroductionAction); KMail::Util::addQActionHelpText(mShowIntroductionAction, i18n("Display KMail's Welcome Page")); connect(mShowIntroductionAction, &QAction::triggered, this, &KMMainWidget::slotIntro); mShowIntroductionAction->setEnabled(mMsgView != Q_NULLPTR); } // ----- Standard Actions // KStandardAction::configureNotifications(this, SLOT(slotEditNotifications()), actionCollection()); { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("preferences-desktop-notification")), i18n("Configure &Notifications..."), this); action->setMenuRole(QAction::NoRole); // do not move to application menu on OS X actionCollection()->addAction(QStringLiteral("kmail_configure_notifications"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotEditNotifications); } { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("&Configure KMail..."), this); action->setMenuRole(QAction::PreferencesRole); // this one should move to the application menu on OS X actionCollection()->addAction(QStringLiteral("kmail_configure_kmail"), action); connect(action, &QAction::triggered, kmkernel, &KMKernel::slotShowConfigurationDialog); } { mExpireConfigAction = new QAction(i18n("Expire..."), this); actionCollection()->addAction(QStringLiteral("expire_settings"), mExpireConfigAction); connect(mExpireConfigAction, &QAction::triggered, mManageShowCollectionProperties, &ManageShowCollectionProperties::slotShowExpiryProperties); } { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add Favorite Folder..."), this); actionCollection()->addAction(QStringLiteral("add_favorite_folder"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotAddFavoriteFolder); } { mServerSideSubscription = new QAction(QIcon::fromTheme(QStringLiteral("folder-bookmarks")), i18n("Serverside Subscription..."), this); actionCollection()->addAction(QStringLiteral("serverside_subscription"), mServerSideSubscription); connect(mServerSideSubscription, &QAction::triggered, this, &KMMainWidget::slotServerSideSubscription); } { mApplyFiltersOnFolder = new QAction(QIcon::fromTheme(QStringLiteral("view-filter")), i18n("Appl&y All Filters On Folder"), this); actionCollection()->addAction(QStringLiteral("apply_filters_on_folder"), mApplyFiltersOnFolder); connect(mApplyFiltersOnFolder, &QAction::triggered, this, &KMMainWidget::slotApplyFiltersOnFolder); } { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("kmail")), i18n("&Export KMail Data..."), this); actionCollection()->addAction(QStringLiteral("kmail_export_data"), action); connect(action, &QAction::triggered, mLaunchExternalComponent, &KMLaunchExternalComponent::slotExportData); } { QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("contact-new")), i18n("New AddressBook Contact..."), this); actionCollection()->addAction(QStringLiteral("kmail_new_addressbook_contact"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotCreateAddressBookContact); } actionCollection()->addAction(KStandardAction::Undo, QStringLiteral("kmail_undo"), this, SLOT(slotUndo())); menutimer = new QTimer(this); menutimer->setObjectName(QStringLiteral("menutimer")); menutimer->setSingleShot(true); connect(menutimer, &QTimer::timeout, this, &KMMainWidget::updateMessageActionsDelayed); connect(kmkernel->undoStack(), &KMail::UndoStack::undoStackChanged, this, &KMMainWidget::slotUpdateUndo); updateMessageActions(); updateFolderMenu(); mTagActionManager = new KMail::TagActionManager(this, actionCollection(), mMsgActions, mGUIClient); mFolderShortcutActionManager = new KMail::FolderShortcutActionManager(this, actionCollection()); { QAction *action = new QAction(i18n("Copy Message to Folder"), this); actionCollection()->addAction(QStringLiteral("copy_message_to_folder"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotCopySelectedMessagesToFolder); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::Key_C)); } { QAction *action = new QAction(i18n("Jump to Folder..."), this); actionCollection()->addAction(QStringLiteral("jump_to_folder"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotJumpToFolder); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::Key_J)); } { QAction *action = new QAction(i18n("Abort Current Operation"), this); actionCollection()->addAction(QStringLiteral("cancel"), action); connect(action, &QAction::triggered, ProgressManager::instance(), &KPIM::ProgressManager::slotAbortAll); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::Key_Escape)); } { QAction *action = new QAction(i18n("Focus on Next Folder"), this); actionCollection()->addAction(QStringLiteral("inc_current_folder"), action); connect(action, &QAction::triggered, mFolderTreeWidget->folderTreeView(), &FolderTreeView::slotFocusNextFolder); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Right)); } { QAction *action = new QAction(i18n("Focus on Previous Folder"), this); actionCollection()->addAction(QStringLiteral("dec_current_folder"), action); connect(action, &QAction::triggered, mFolderTreeWidget->folderTreeView(), &FolderTreeView::slotFocusPrevFolder); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Left)); } { QAction *action = new QAction(i18n("Select Folder with Focus"), this); actionCollection()->addAction(QStringLiteral("select_current_folder"), action); connect(action, &QAction::triggered, mFolderTreeWidget->folderTreeView(), &FolderTreeView::slotSelectFocusFolder); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Space)); } { QAction *action = new QAction(i18n("Focus on First Folder"), this); actionCollection()->addAction(QStringLiteral("focus_first_folder"), action); connect(action, &QAction::triggered, mFolderTreeWidget->folderTreeView(), &FolderTreeView::slotFocusFirstFolder); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Home)); } { QAction *action = new QAction(i18n("Focus on Last Folder"), this); actionCollection()->addAction(QStringLiteral("focus_last_folder"), action); connect(action, &QAction::triggered, mFolderTreeWidget->folderTreeView(), &FolderTreeView::slotFocusLastFolder); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_End)); } { QAction *action = new QAction(i18n("Focus on Next Message"), this); actionCollection()->addAction(QStringLiteral("inc_current_message"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotFocusOnNextMessage); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::Key_Right)); } { QAction *action = new QAction(i18n("Focus on Previous Message"), this); actionCollection()->addAction(QStringLiteral("dec_current_message"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotFocusOnPrevMessage); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::Key_Left)); } { QAction *action = new QAction(i18n("Select First Message"), this); actionCollection()->addAction(QStringLiteral("select_first_message"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotSelectFirstMessage); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::Key_Home)); } { QAction *action = new QAction(i18n("Select Last Message"), this); actionCollection()->addAction(QStringLiteral("select_last_message"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotSelectLastMessage); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::Key_End)); } { QAction *action = new QAction(i18n("Select Message with Focus"), this); actionCollection()->addAction(QStringLiteral("select_current_message"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotSelectFocusedMessage); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::Key_Space)); } { mQuickSearchAction = new QAction(i18n("Set Focus to Quick Search"), this); //If change shortcut change Panel::setQuickSearchClickMessage(...) message actionCollection()->setDefaultShortcut(mQuickSearchAction, QKeySequence(Qt::ALT + Qt::Key_Q)); actionCollection()->addAction(QStringLiteral("focus_to_quickseach"), mQuickSearchAction); connect(mQuickSearchAction, &QAction::triggered, this, &KMMainWidget::slotFocusQuickSearch); updateQuickSearchLineText(); } { QAction *action = new QAction(i18n("Extend Selection to Previous Message"), this); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::SHIFT + Qt::Key_Left)); actionCollection()->addAction(QStringLiteral("previous_message"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotExtendSelectionToPreviousMessage); } { QAction *action = new QAction(i18n("Extend Selection to Next Message"), this); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::SHIFT + Qt::Key_Right)); actionCollection()->addAction(QStringLiteral("next_message"), action); connect(action, &QAction::triggered, this, &KMMainWidget::slotExtendSelectionToNextMessage); } { mMoveMsgToFolderAction = new QAction(i18n("Move Message to Folder"), this); actionCollection()->setDefaultShortcut(mMoveMsgToFolderAction, QKeySequence(Qt::Key_M)); actionCollection()->addAction(QStringLiteral("move_message_to_folder"), mMoveMsgToFolderAction); connect(mMoveMsgToFolderAction, &QAction::triggered, this, &KMMainWidget::slotMoveSelectedMessageToFolder); } mArchiveAction = new QAction(i18nc("@action", "Archive"), this); actionCollection()->addAction(QStringLiteral("archive_mails"), mArchiveAction); connect(mArchiveAction, &QAction::triggered, this, &KMMainWidget::slotArchiveMails); mLowBandwithAction = new KToggleAction(i18n("Low Bandwidth"), this); actionCollection()->addAction(QStringLiteral("low_bandwidth"), mLowBandwithAction); connect(mLowBandwithAction, &KToggleAction::triggered, this, &KMMainWidget::slotBandwidth); mMarkAllMessageAsReadAndInAllSubFolder = new QAction(i18n("Mark All Messages As Read in This Folder and All its Subfolder"), this); mMarkAllMessageAsReadAndInAllSubFolder->setIcon(QIcon::fromTheme(QStringLiteral("mail-mark-read"))); actionCollection()->addAction(QStringLiteral("markallmessagereadcurentfolderandsubfolder"), mMarkAllMessageAsReadAndInAllSubFolder); connect(mMarkAllMessageAsReadAndInAllSubFolder, &KToggleAction::triggered, this, &KMMainWidget::slotMarkAllMessageAsReadInCurrentFolderAndSubfolder); } void KMMainWidget::slotAddFavoriteFolder() { if (!mFavoritesModel) { return; } QPointer dialog(selectFromAllFoldersDialog()); dialog->setWindowTitle(i18n("Add Favorite Folder")); if (dialog->exec() && dialog) { const Akonadi::Collection collection = dialog->selectedCollection(); if (collection.isValid()) { mFavoritesModel->addCollection(collection); } } } //----------------------------------------------------------------------------- void KMMainWidget::slotEditNotifications() { KMail::KMKnotify notifyDlg(this); notifyDlg.exec(); } //----------------------------------------------------------------------------- void KMMainWidget::slotReadOn() { if (!mMsgView) { return; } if (!mMsgView->viewer()->atBottom()) { mMsgView->viewer()->slotJumpDown(); return; } slotSelectNextUnreadMessage(); } void KMMainWidget::slotNextUnreadFolder() { if (!mFolderTreeWidget) { return; } mGoToFirstUnreadMessageInSelectedFolder = true; mFolderTreeWidget->folderTreeView()->selectNextUnreadFolder(); mGoToFirstUnreadMessageInSelectedFolder = false; } void KMMainWidget::slotPrevUnreadFolder() { if (!mFolderTreeWidget) { return; } mGoToFirstUnreadMessageInSelectedFolder = true; mFolderTreeWidget->folderTreeView()->selectPrevUnreadFolder(); mGoToFirstUnreadMessageInSelectedFolder = false; } void KMMainWidget::slotExpandThread() { mMessagePane->setCurrentThreadExpanded(true); } void KMMainWidget::slotCollapseThread() { mMessagePane->setCurrentThreadExpanded(false); } void KMMainWidget::slotExpandAllThreads() { // TODO: Make this asynchronous ? (if there is enough demand) #ifndef QT_NO_CURSOR KPIM::KCursorSaver busy(KPIM::KBusyPtr::busy()); #endif mMessagePane->setAllThreadsExpanded(true); } void KMMainWidget::slotCollapseAllThreads() { // TODO: Make this asynchronous ? (if there is enough demand) #ifndef QT_NO_CURSOR KPIM::KCursorSaver busy(KPIM::KBusyPtr::busy()); #endif mMessagePane->setAllThreadsExpanded(false); } //----------------------------------------------------------------------------- void KMMainWidget::updateMessageMenu() { updateMessageActions(); } void KMMainWidget::startUpdateMessageActionsTimer() { // FIXME: This delay effectively CAN make the actions to be in an incoherent state // Maybe we should mark actions as "dirty" here and check it in every action handler... updateMessageActions(true); menutimer->stop(); menutimer->start(500); } void KMMainWidget::updateMessageActions(bool fast) { Akonadi::Item::List selectedItems; Akonadi::Item::List selectedVisibleItems; bool allSelectedBelongToSameThread = false; if (mCurrentFolder && mCurrentFolder->isValid() && mMessagePane->getSelectionStats(selectedItems, selectedVisibleItems, &allSelectedBelongToSameThread) ) { mMsgActions->setCurrentMessage(mMessagePane->currentItem(), selectedVisibleItems); } else { mMsgActions->setCurrentMessage(Akonadi::Item()); } if (!fast) { updateMessageActionsDelayed(); } } void KMMainWidget::updateMessageActionsDelayed() { int count; Akonadi::Item::List selectedItems; Akonadi::Item::List selectedVisibleItems; bool allSelectedBelongToSameThread = false; Akonadi::Item currentMessage; if (mCurrentFolder && mCurrentFolder->isValid() && mMessagePane->getSelectionStats(selectedItems, selectedVisibleItems, &allSelectedBelongToSameThread) ) { count = selectedItems.count(); currentMessage = mMessagePane->currentItem(); } else { count = 0; currentMessage = Akonadi::Item(); } mApplyFiltersOnFolder->setEnabled(mCurrentFolder && mCurrentFolder->isValid()); // // Here we have: // // - A list of selected messages stored in selectedSernums. // The selected messages might contain some invisible ones as a selected // collapsed node "includes" all the children in the selection. // - A list of selected AND visible messages stored in selectedVisibleSernums. // This list does not contain children of selected and collapsed nodes. // // Now, actions can operate on: // - Any set of messages // These are called "mass actions" and are enabled whenever we have a message selected. // In fact we should differentiate between actions that operate on visible selection // and those that operate on the selection as a whole (without considering visibility)... // - A single thread // These are called "thread actions" and are enabled whenever all the selected messages belong // to the same thread. If the selection doesn't cover the whole thread then the action // will act on the whole thread anyway (thus will silently extend the selection) // - A single message // And we have two sub-cases: // - The selection must contain exactly one message // These actions can't ignore the hidden messages and thus must be disabled if // the selection contains any. // - The selection must contain exactly one visible message // These actions will ignore the hidden message and thus can be enabled if // the selection contains any. // bool readOnly = mCurrentFolder && mCurrentFolder->isValid() && (mCurrentFolder->rights() & Akonadi::Collection::ReadOnly); // can we apply strictly single message actions ? (this is false if the whole selection contains more than one message) bool single_actions = count == 1; // can we apply loosely single message actions ? (this is false if the VISIBLE selection contains more than one message) bool singleVisibleMessageSelected = selectedVisibleItems.count() == 1; // can we apply "mass" actions to the selection ? (this is actually always true if the selection is non-empty) bool mass_actions = count >= 1; // does the selection identify a single thread ? bool thread_actions = mass_actions && allSelectedBelongToSameThread && mMessagePane->isThreaded(); // can we apply flags to the selected messages ? bool flags_available = KMailSettings::self()->allowLocalFlags() || !(mCurrentFolder && mCurrentFolder->isValid() ? readOnly : true); mThreadStatusMenu->setEnabled(thread_actions); // these need to be handled individually, the user might have them // in the toolbar mWatchThreadAction->setEnabled(thread_actions && flags_available); mIgnoreThreadAction->setEnabled(thread_actions && flags_available); mMarkThreadAsReadAction->setEnabled(thread_actions); mMarkThreadAsUnreadAction->setEnabled(thread_actions); mToggleThreadToActAction->setEnabled(thread_actions && flags_available); mToggleThreadImportantAction->setEnabled(thread_actions && flags_available); bool canDeleteMessages = mCurrentFolder && mCurrentFolder->isValid() && (mCurrentFolder->rights() & Akonadi::Collection::CanDeleteItem); mTrashThreadAction->setEnabled(thread_actions && canDeleteMessages); mDeleteThreadAction->setEnabled(thread_actions && canDeleteMessages); if (currentMessage.isValid()) { MessageStatus status; status.setStatusFromFlags(currentMessage.flags()); mTagActionManager->updateActionStates(count, mMessagePane->currentItem()); if (thread_actions) { mToggleThreadToActAction->setChecked(status.isToAct()); mToggleThreadImportantAction->setChecked(status.isImportant()); mWatchThreadAction->setChecked(status.isWatched()); mIgnoreThreadAction->setChecked(status.isIgnored()); } } mMoveActionMenu->setEnabled(mass_actions && canDeleteMessages); if (mMoveMsgToFolderAction) { mMoveMsgToFolderAction->setEnabled(mass_actions && canDeleteMessages); } //mCopyActionMenu->setEnabled( mass_actions ); mDeleteAction->setEnabled(mass_actions && canDeleteMessages); mExpireConfigAction->setEnabled(canDeleteMessages); if (mMsgView) { mMsgView->findInMessageAction()->setEnabled(mass_actions && !CommonKernel->folderIsTemplates(mCurrentFolder->collection())); } mMsgActions->forwardInlineAction()->setEnabled(mass_actions && !CommonKernel->folderIsTemplates(mCurrentFolder->collection())); mMsgActions->forwardAttachedAction()->setEnabled(mass_actions && !CommonKernel->folderIsTemplates(mCurrentFolder->collection())); mMsgActions->forwardMenu()->setEnabled(mass_actions && !CommonKernel->folderIsTemplates(mCurrentFolder->collection())); mMsgActions->editAction()->setEnabled(single_actions); mUseAction->setEnabled(single_actions && CommonKernel->folderIsTemplates(mCurrentFolder->collection())); filterMenu()->setEnabled(single_actions); mMsgActions->redirectAction()->setEnabled(/*single_actions &&*/mass_actions && !CommonKernel->folderIsTemplates(mCurrentFolder->collection())); if (mMsgActions->customTemplatesMenu()) { mMsgActions->customTemplatesMenu()->forwardActionMenu()->setEnabled(mass_actions); mMsgActions->customTemplatesMenu()->replyActionMenu()->setEnabled(single_actions); mMsgActions->customTemplatesMenu()->replyAllActionMenu()->setEnabled(single_actions); } // "Print" will act on the current message: it will ignore any hidden selection mMsgActions->printAction()->setEnabled(singleVisibleMessageSelected); // "Print preview" will act on the current message: it will ignore any hidden selection QAction *printPreviewAction = mMsgActions->printPreviewAction(); if (printPreviewAction) { printPreviewAction->setEnabled(singleVisibleMessageSelected); } // "View Source" will act on the current message: it will ignore any hidden selection if (mMsgView) { mMsgView->viewSourceAction()->setEnabled(singleVisibleMessageSelected); } MessageStatus status; status.setStatusFromFlags(currentMessage.flags()); QList< QAction *> actionList; bool statusSendAgain = single_actions && ((currentMessage.isValid() && status.isSent()) || (currentMessage.isValid() && CommonKernel->folderIsSentMailFolder(mCurrentFolder->collection()))); if (statusSendAgain) { actionList << mSendAgainAction; } else if (single_actions) { actionList << messageActions()->editAction(); } actionList << mSaveAttachmentsAction; if (mCurrentFolder && FolderArchive::FolderArchiveUtil::resourceSupportArchiving(mCurrentFolder->collection().resource())) { actionList << mArchiveAction; } mGUIClient->unplugActionList(QStringLiteral("messagelist_actionlist")); mGUIClient->plugActionList(QStringLiteral("messagelist_actionlist"), actionList); mSendAgainAction->setEnabled(statusSendAgain); mSaveAsAction->setEnabled(mass_actions); if ((mCurrentFolder && mCurrentFolder->isValid())) { updateMoveAction(mCurrentFolder->statistics()); } else { updateMoveAction(false, false); } const qint64 nbMsgOutboxCollection = MailCommon::Util::updatedCollection(CommonKernel->outboxCollectionFolder()).statistics().count(); mSendQueued->setEnabled(nbMsgOutboxCollection > 0); mSendActionMenu->setEnabled(nbMsgOutboxCollection > 0); const bool newPostToMailingList = mCurrentFolder && mCurrentFolder->isMailingListEnabled(); mMessageNewList->setEnabled(newPostToMailingList); slotUpdateOnlineStatus(static_cast(KMailSettings::self()->networkState())); if (action(QStringLiteral("kmail_undo"))) { action(QStringLiteral("kmail_undo"))->setEnabled(kmkernel->undoStack()->size() > 0); } // Enable / disable all filters. foreach (QAction *filterAction, mFilterMenuActions) { filterAction->setEnabled(count > 0); } mApplyAllFiltersAction->setEnabled(count); mApplyFilterActionsMenu->setEnabled(count); } void KMMainWidget::slotAkonadiStandardActionUpdated() { bool multiFolder = false; if (mFolderTreeWidget) { multiFolder = mFolderTreeWidget->selectedCollections().count() > 1; } if (mCollectionProperties) { if (mCurrentFolder && mCurrentFolder->collection().isValid()) { const Akonadi::AgentInstance instance = Akonadi::AgentManager::self()->instance(mCurrentFolder->collection().resource()); mCollectionProperties->setEnabled(!multiFolder && !mCurrentFolder->isStructural() && (instance.status() != Akonadi::AgentInstance::Broken)); } else { mCollectionProperties->setEnabled(false); } QList< QAction * > collectionProperties; if (mCollectionProperties->isEnabled()) { collectionProperties << mCollectionProperties; } mGUIClient->unplugActionList(QStringLiteral("akonadi_collection_collectionproperties_actionlist")); mGUIClient->plugActionList(QStringLiteral("akonadi_collection_collectionproperties_actionlist"), collectionProperties); } const bool folderWithContent = mCurrentFolder && !mCurrentFolder->isStructural(); if (mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::DeleteCollections)) { mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::DeleteCollections)->setEnabled(mCurrentFolder && !multiFolder && (mCurrentFolder->collection().rights() & Collection::CanDeleteCollection) && !mCurrentFolder->isSystemFolder() && folderWithContent); } if (mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::MoveAllToTrash)) { mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::MoveAllToTrash)->setEnabled(folderWithContent && (mCurrentFolder->count() > 0) && mCurrentFolder->canDeleteMessages() && !multiFolder); mAkonadiStandardActionManager->action(Akonadi::StandardMailActionManager::MoveAllToTrash)->setText((mCurrentFolder && CommonKernel->folderIsTrash(mCurrentFolder->collection())) ? i18n("E&mpty Trash") : i18n("&Move All Messages to Trash")); } QList< QAction * > addToFavorite; QAction *actionAddToFavoriteCollections = akonadiStandardAction(Akonadi::StandardActionManager::AddToFavoriteCollections); if (actionAddToFavoriteCollections) { if (mEnableFavoriteFolderView && actionAddToFavoriteCollections->isEnabled()) { addToFavorite << actionAddToFavoriteCollections; } mGUIClient->unplugActionList(QStringLiteral("akonadi_collection_add_to_favorites_actionlist")); mGUIClient->plugActionList(QStringLiteral("akonadi_collection_add_to_favorites_actionlist"), addToFavorite); } QList< QAction * > syncActionList; QAction *actionSync = akonadiStandardAction(Akonadi::StandardActionManager::SynchronizeCollections); if (actionSync && actionSync->isEnabled()) { syncActionList << actionSync; } actionSync = akonadiStandardAction(Akonadi::StandardActionManager::SynchronizeCollectionsRecursive); if (actionSync && actionSync->isEnabled()) { syncActionList << actionSync; } mGUIClient->unplugActionList(QStringLiteral("akonadi_collection_sync_actionlist")); mGUIClient->plugActionList(QStringLiteral("akonadi_collection_sync_actionlist"), syncActionList); QList< QAction * > actionList; QAction *action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::CreateCollection); if (action && action->isEnabled()) { actionList << action; } action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::MoveCollectionToMenu); if (action && action->isEnabled()) { actionList << action; } action = mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::CopyCollectionToMenu); if (action && action->isEnabled()) { actionList << action; } mGUIClient->unplugActionList(QStringLiteral("akonadi_collection_move_copy_menu_actionlist")); mGUIClient->plugActionList(QStringLiteral("akonadi_collection_move_copy_menu_actionlist"), actionList); } void KMMainWidget::updateHtmlMenuEntry() { if (mDisplayMessageFormatMenu && mPreferHtmlLoadExtAction) { bool multiFolder = false; if (mFolderTreeWidget) { multiFolder = mFolderTreeWidget->selectedCollections().count() > 1; } // the visual ones only make sense if we are showing a message list const bool enabledAction = (mFolderTreeWidget && mFolderTreeWidget->folderTreeView()->currentFolder().isValid() && !multiFolder); mDisplayMessageFormatMenu->setEnabled(enabledAction); const bool isEnabled = (mFolderTreeWidget && mFolderTreeWidget->folderTreeView()->currentFolder().isValid() && !multiFolder); const bool useHtml = (mFolderDisplayFormatPreference == MessageViewer::Viewer::Html || (mHtmlGlobalSetting && mFolderDisplayFormatPreference == MessageViewer::Viewer::UseGlobalSetting)); mPreferHtmlLoadExtAction->setEnabled(isEnabled && useHtml); mDisplayMessageFormatMenu->setDisplayMessageFormat(mFolderDisplayFormatPreference); mPreferHtmlLoadExtAction->setChecked(!multiFolder && (mHtmlLoadExtGlobalSetting ? !mFolderHtmlLoadExtPreference : mFolderHtmlLoadExtPreference)); } } //----------------------------------------------------------------------------- void KMMainWidget::updateFolderMenu() { if (!CommonKernel->outboxCollectionFolder().isValid()) { QTimer::singleShot(1000, this, &KMMainWidget::updateFolderMenu); return; } const bool folderWithContent = mCurrentFolder && !mCurrentFolder->isStructural(); bool multiFolder = false; if (mFolderTreeWidget) { multiFolder = mFolderTreeWidget->selectedCollections().count() > 1; } mFolderMailingListPropertiesAction->setEnabled(folderWithContent && !multiFolder && !mCurrentFolder->isSystemFolder()); QList< QAction * > actionlist; if (mCurrentFolder && mCurrentFolder->collection().id() == CommonKernel->outboxCollectionFolder().id() && (mCurrentFolder->collection()).statistics().count() > 0) { qCDebug(KMAIL_LOG) << "Enabling send queued"; mSendQueued->setEnabled(true); actionlist << mSendQueued; } // if ( mCurrentFolder && mCurrentFolder->collection().id() != CommonKernel->trashCollectionFolder().id() ) { // actionlist << mTrashAction; // } mGUIClient->unplugActionList(QStringLiteral("outbox_folder_actionlist")); mGUIClient->plugActionList(QStringLiteral("outbox_folder_actionlist"), actionlist); actionlist.clear(); const bool isASearchFolder = mCurrentFolder && mCurrentFolder->collection().resource() == QLatin1String("akonadi_search_resource"); if (isASearchFolder) { mAkonadiStandardActionManager->action(Akonadi::StandardActionManager::DeleteCollections)->setText(i18n("&Delete Search")); } mArchiveFolderAction->setEnabled(mCurrentFolder && !multiFolder && folderWithContent); bool isInTrashFolder = (mCurrentFolder && CommonKernel->folderIsTrash(mCurrentFolder->collection())); QAction *moveToTrash = akonadiStandardAction(Akonadi::StandardMailActionManager::MoveToTrash); akonadiStandardAction(Akonadi::StandardMailActionManager::MoveToTrash)->setText(isInTrashFolder ? i18nc("@action Hard delete, bypassing trash", "&Delete") : i18n("&Move to Trash")); akonadiStandardAction(Akonadi::StandardMailActionManager::MoveToTrash)->setIcon(isInTrashFolder ? QIcon::fromTheme(QStringLiteral("edit-delete")) : QIcon::fromTheme(QStringLiteral("user-trash"))); //Use same text as in Text property. Change it in kf5 moveToTrash->setToolTip(isInTrashFolder ? i18nc("@action Hard delete, bypassing trash", "&Delete") : i18n("&Move to Trash")); mTrashThreadAction->setIcon(isInTrashFolder ? QIcon::fromTheme(QStringLiteral("edit-delete")) : QIcon::fromTheme(QStringLiteral("user-trash"))); mTrashThreadAction->setText(isInTrashFolder ? i18n("Delete T&hread") : i18n("M&ove Thread to Trash")); mSearchMessages->setText((mCurrentFolder && mCurrentFolder->collection().resource() == QLatin1String("akonadi_search_resource")) ? i18n("Edit Search...") : i18n("&Find Messages...")); mExpireConfigAction->setEnabled(mCurrentFolder && !mCurrentFolder->isStructural() && !multiFolder && mCurrentFolder->canDeleteMessages() && folderWithContent && !MailCommon::Util::isVirtualCollection(mCurrentFolder->collection())); updateHtmlMenuEntry(); mShowFolderShortcutDialogAction->setEnabled(!multiFolder && folderWithContent); actionlist << akonadiStandardAction(Akonadi::StandardActionManager::ManageLocalSubscriptions); bool imapFolderIsOnline = false; if (mCurrentFolder && PimCommon::Util::isImapFolder(mCurrentFolder->collection(), imapFolderIsOnline)) { if (imapFolderIsOnline) { actionlist << mServerSideSubscription; } } mGUIClient->unplugActionList(QStringLiteral("collectionview_actionlist")); mGUIClient->plugActionList(QStringLiteral("collectionview_actionlist"), actionlist); } //----------------------------------------------------------------------------- void KMMainWidget::slotIntro() { if (!mMsgView) { return; } mMsgView->clear(true); // hide widgets that are in the way: if (mMessagePane && mLongFolderList) { mMessagePane->hide(); } mMsgView->displayAboutPage(); mCurrentFolder.clear(); } void KMMainWidget::slotShowStartupFolder() { connect(MailCommon::FilterManager::instance(), &FilterManager::filtersChanged, this, &KMMainWidget::initializeFilterActions); // Plug various action lists. This can't be done in the constructor, as that is called before // the main window or Kontact calls createGUI(). // This function however is called with a single shot timer. checkAkonadiServerManagerState(); mFolderShortcutActionManager->createActions(); mTagActionManager->createActions(); messageActions()->setupForwardingActionsList(mGUIClient); QString newFeaturesMD5 = KMReaderWin::newFeaturesMD5(); if (kmkernel->firstStart() || KMailSettings::self()->previousNewFeaturesMD5() != newFeaturesMD5) { KMailSettings::self()->setPreviousNewFeaturesMD5(newFeaturesMD5); slotIntro(); return; } } void KMMainWidget::checkAkonadiServerManagerState() { Akonadi::ServerManager::State state = Akonadi::ServerManager::self()->state(); if (state == Akonadi::ServerManager::Running) { initializeFilterActions(); } else { connect(Akonadi::ServerManager::self(), &ServerManager::stateChanged, this, &KMMainWidget::slotServerStateChanged); } } void KMMainWidget::slotServerStateChanged(Akonadi::ServerManager::State state) { if (state == Akonadi::ServerManager::Running) { initializeFilterActions(); disconnect(Akonadi::ServerManager::self(), SIGNAL(stateChanged(Akonadi::ServerManager::State))); } } QList KMMainWidget::actionCollections() const { return QList() << actionCollection(); } //----------------------------------------------------------------------------- void KMMainWidget::slotUpdateUndo() { if (actionCollection()->action(QStringLiteral("kmail_undo"))) { QAction *act = actionCollection()->action(QStringLiteral("kmail_undo")); act->setEnabled(!kmkernel->undoStack()->isEmpty()); const QString infoStr = kmkernel->undoStack()->undoInfo(); if (infoStr.isEmpty()) { act->setText(i18n("&Undo")); } else { act->setText(i18n("&Undo: \"%1\"", kmkernel->undoStack()->undoInfo())); } } } //----------------------------------------------------------------------------- void KMMainWidget::clearFilterActions() { if (!mFilterTBarActions.isEmpty()) if (mGUIClient->factory()) { mGUIClient->unplugActionList(QStringLiteral("toolbar_filter_actions")); } if (!mFilterMenuActions.isEmpty()) if (mGUIClient->factory()) { mGUIClient->unplugActionList(QStringLiteral("menu_filter_actions")); } foreach (QAction *a, mFilterMenuActions) { actionCollection()->removeAction(a); } mApplyFilterActionsMenu->menu()->clear(); mFilterTBarActions.clear(); mFilterMenuActions.clear(); qDeleteAll(mFilterCommands); mFilterCommands.clear(); } //----------------------------------------------------------------------------- void KMMainWidget::initializeFilterActions() { clearFilterActions(); mApplyFilterActionsMenu->menu()->addAction(mApplyAllFiltersAction); bool addedSeparator = false; foreach (MailFilter *filter, MailCommon::FilterManager::instance()->filters()) { if (!filter->isEmpty() && filter->configureShortcut() && filter->isEnabled()) { QString filterName = QStringLiteral("Filter %1").arg(filter->name()); QString normalizedName = filterName.replace(QLatin1Char(' '), QLatin1Char('_')); if (action(normalizedName)) { continue; } KMMetaFilterActionCommand *filterCommand = new KMMetaFilterActionCommand(filter->identifier(), this); mFilterCommands.append(filterCommand); QString displayText = i18n("Filter %1", filter->name()); QString icon = filter->icon(); if (icon.isEmpty()) { icon = QStringLiteral("system-run"); } QAction *filterAction = new QAction(QIcon::fromTheme(icon), displayText, actionCollection()); filterAction->setIconText(filter->toolbarName()); // The shortcut configuration is done in the filter dialog. // The shortcut set in the shortcut dialog would not be saved back to // the filter settings correctly. actionCollection()->setShortcutsConfigurable(filterAction, false); actionCollection()->addAction(normalizedName, filterAction); connect(filterAction, &QAction::triggered, filterCommand, &KMMetaFilterActionCommand::start); actionCollection()->setDefaultShortcut(filterAction, filter->shortcut()); if (!addedSeparator) { QAction *a = mApplyFilterActionsMenu->menu()->addSeparator(); mFilterMenuActions.append(a); addedSeparator = true; } mApplyFilterActionsMenu->menu()->addAction(filterAction); mFilterMenuActions.append(filterAction); if (filter->configureToolbar()) { mFilterTBarActions.append(filterAction); } } } if (!mFilterMenuActions.isEmpty() && mGUIClient->factory()) { mGUIClient->plugActionList(QStringLiteral("menu_filter_actions"), mFilterMenuActions); } if (!mFilterTBarActions.isEmpty() && mGUIClient->factory()) { mFilterTBarActions.prepend(mToolbarActionSeparator); mGUIClient->plugActionList(QStringLiteral("toolbar_filter_actions"), mFilterTBarActions); } // Our filters have changed, now enable/disable them updateMessageActions(); } void KMMainWidget::updateFileMenu() { const bool isEmpty = MailCommon::Util::agentInstances().isEmpty(); actionCollection()->action(QStringLiteral("check_mail"))->setEnabled(!isEmpty); actionCollection()->action(QStringLiteral("check_mail_in"))->setEnabled(!isEmpty); } //----------------------------------------------------------------------------- const KMMainWidget::PtrList *KMMainWidget::mainWidgetList() { // better safe than sorry; check whether the global static has already been destroyed if (theMainWidgetList.isDestroyed()) { return Q_NULLPTR; } return theMainWidgetList; } QSharedPointer KMMainWidget::currentFolder() const { return mCurrentFolder; } //----------------------------------------------------------------------------- QString KMMainWidget::overrideEncoding() const { if (mMsgView) { return mMsgView->overrideEncoding(); } else { return MessageCore::MessageCoreSettings::self()->overrideCharacterEncoding(); } } void KMMainWidget::showEvent(QShowEvent *event) { QWidget::showEvent(event); mWasEverShown = true; } void KMMainWidget::slotRequestFullSearchFromQuickSearch() { // First, open the search window. If we are currently on a search folder, // the search associated with that will be loaded. if (!slotSearch()) { return; } assert(mSearchWin); // Now we look at the current state of the quick search, and if there's // something in there, we add the criteria to the existing search for // the search folder, if applicable, or make a new one from it. SearchPattern pattern; const QString searchString = mMessagePane->currentFilterSearchString(); if (!searchString.isEmpty()) { - MessageList::Core::QuickSearchLine::SearchOptions options = mMessagePane->currentOptions(); + QByteArray searchStringVal; - if (options & MessageList::Core::QuickSearchLine::SearchEveryWhere) { - searchStringVal = ""; - } else if (options & MessageList::Core::QuickSearchLine::SearchAgainstSubject) { - searchStringVal = "subject"; - } else if (options & MessageList::Core::QuickSearchLine::SearchAgainstBody) { - searchStringVal = ""; - } else if (options & MessageList::Core::QuickSearchLine::SearchAgainstFrom) { - searchStringVal = "from"; - } else if (options & MessageList::Core::QuickSearchLine::SearchAgainstBcc) { - searchStringVal = "bcc"; - } else if (options & MessageList::Core::QuickSearchLine::SearchAgainstTo) { - searchStringVal = "to"; - } else { - searchStringVal = ""; - } + searchStringVal = ""; + pattern.append(SearchRule::createInstance(searchStringVal, SearchRule::FuncContains, searchString)); QList statusList = mMessagePane->currentFilterStatus(); Q_FOREACH (MessageStatus status, statusList) { if (status.hasAttachment()) { pattern.append(SearchRule::createInstance(searchStringVal, SearchRule::FuncHasAttachment, searchString)); status.setHasAttachment(false); } if (!status.isOfUnknownStatus()) { pattern.append(SearchRule::Ptr(new SearchRuleStatus(status))); } } } if (!pattern.isEmpty()) { mSearchWin->addRulesToSearchPattern(pattern); } } void KMMainWidget::updateVacationScriptStatus(bool active, const QString &serverName) { mVacationScriptIndicator->setVacationScriptActive(active, serverName); mVacationIndicatorActive = mVacationScriptIndicator->hasVacationScriptActive(); } QWidget *KMMainWidget::vacationScriptIndicator() const { return mVacationScriptIndicator; } void KMMainWidget::updateVacationScriptStatus() { updateVacationScriptStatus(mVacationIndicatorActive); } KMail::TagActionManager *KMMainWidget::tagActionManager() const { return mTagActionManager; } KMail::FolderShortcutActionManager *KMMainWidget::folderShortcutActionManager() const { return mFolderShortcutActionManager; } void KMMainWidget::slotMessageSelected(const Akonadi::Item &item) { delete mShowBusySplashTimer; mShowBusySplashTimer = Q_NULLPTR; if (mMsgView) { // The current selection was cleared, so we'll remove the previously // selected message from the preview pane if (!item.isValid()) { mMsgView->clear(); } else { mShowBusySplashTimer = new QTimer(this); mShowBusySplashTimer->setSingleShot(true); connect(mShowBusySplashTimer, &QTimer::timeout, this, &KMMainWidget::slotShowBusySplash); mShowBusySplashTimer->start(1000); Akonadi::ItemFetchJob *itemFetchJob = MessageViewer::Viewer::createFetchJob(item); if (mCurrentFolder) { const QString resource = mCurrentFolder->collection().resource(); itemFetchJob->setProperty("_resource", QVariant::fromValue(resource)); connect(itemFetchJob, &ItemFetchJob::itemsReceived, this, &KMMainWidget::itemsReceived); connect(itemFetchJob, &Akonadi::ItemFetchJob::result, this, &KMMainWidget::itemsFetchDone); } } } } void KMMainWidget::itemsReceived(const Akonadi::Item::List &list) { Q_ASSERT(list.size() == 1); delete mShowBusySplashTimer; mShowBusySplashTimer = Q_NULLPTR; if (!mMsgView) { return; } Item item = list.first(); if (mMessagePane) { mMessagePane->show(); if (mMessagePane->currentItem() != item) { // The user has selected another email already, so don't render this one. // Mark it as read, though, if the user settings say so. if (MessageViewer::MessageViewerSettings::self()->delayedMarkAsRead() && MessageViewer::MessageViewerSettings::self()->delayedMarkTime() == 0) { item.setFlag(Akonadi::MessageFlags::Seen); Akonadi::ItemModifyJob *modifyJob = new Akonadi::ItemModifyJob(item, this); modifyJob->disableRevisionCheck(); modifyJob->setIgnorePayload(true); } return; } } mMsgView->setMessage(item); // reset HTML Q_DECL_OVERRIDE to the folder setting mMsgView->setDisplayFormatMessageOverwrite(mFolderDisplayFormatPreference); mMsgView->setHtmlLoadExtOverride(mFolderHtmlLoadExtPreference); mMsgView->setDecryptMessageOverwrite(false); mMsgActions->setCurrentMessage(item); } void KMMainWidget::itemsFetchDone(KJob *job) { delete mShowBusySplashTimer; mShowBusySplashTimer = Q_NULLPTR; if (job->error()) { // Unfortunately job->error() is Job::Unknown in many cases. // (see JobPrivate::handleResponse in akonadi/job.cpp) // So we show the "offline" page after checking the resource status. qCDebug(KMAIL_LOG) << job->error() << job->errorString(); const QString resource = job->property("_resource").toString(); const Akonadi::AgentInstance agentInstance = Akonadi::AgentManager::self()->instance(resource); if (!agentInstance.isOnline()) { // The resource is offline if (mMsgView) { mMsgView->viewer()->enableMessageDisplay(); mMsgView->clear(true); } mMessagePane->show(); if (kmkernel->isOffline()) { showOfflinePage(); } else { if (mMsgView) { showResourceOfflinePage(); } } } else { // Some other error BroadcastStatus::instance()->setStatusMsg(job->errorString()); } } } QAction *KMMainWidget::akonadiStandardAction(Akonadi::StandardActionManager::Type type) { return mAkonadiStandardActionManager->action(type); } QAction *KMMainWidget::akonadiStandardAction(Akonadi::StandardMailActionManager::Type type) { return mAkonadiStandardActionManager->action(type); } void KMMainWidget::slotRemoveDuplicates() { RemoveDuplicateMailJob *job = new RemoveDuplicateMailJob(mFolderTreeWidget->folderTreeView()->selectionModel(), this, this); job->start(); } void KMMainWidget::slotServerSideSubscription() { if (!mCurrentFolder) { return; } PimCommon::ManageServerSideSubscriptionJob *job = new PimCommon::ManageServerSideSubscriptionJob(this); job->setCurrentCollection(mCurrentFolder->collection()); job->setParentWidget(this); job->start(); } void KMMainWidget::savePaneSelection() { if (mMessagePane) { mMessagePane->saveCurrentSelection(); } } void KMMainWidget::updatePaneTagComboBox() { if (mMessagePane) { mMessagePane->updateTagComboBox(); } } void KMMainWidget::slotCreateAddressBookContact() { CreateNewContactJob *job = new CreateNewContactJob(this, this); job->start(); } void KMMainWidget::slotOpenRecentMsg(const QUrl &url) { KMOpenMsgCommand *openCommand = new KMOpenMsgCommand(this, url, overrideEncoding(), this); openCommand->start(); } void KMMainWidget::addRecentFile(const QUrl &mUrl) { mOpenRecentAction->addUrl(mUrl); KConfigGroup grp = mConfig->group(QStringLiteral("Recent Files")); mOpenRecentAction->saveEntries(grp); grp.sync(); } void KMMainWidget::slotMoveMessageToTrash() { if (messageView() && messageView()->viewer() && mCurrentFolder) { KMTrashMsgCommand *command = new KMTrashMsgCommand(mCurrentFolder->collection(), messageView()->viewer()->messageItem(), -1); command->start(); } } void KMMainWidget::slotArchiveMails() { if (mCurrentFolder && mCurrentFolder->collection().isValid()) { const Akonadi::Item::List selectedMessages = mMessagePane->selectionAsMessageItemList(); KMKernel::self()->folderArchiveManager()->setArchiveItems(selectedMessages, mCurrentFolder->collection().resource()); } } void KMMainWidget::updateQuickSearchLineText() { //If change change shortcut mMessagePane->setQuickSearchClickMessage(i18nc("Show shortcut for focus quick search. Don't change it", "Search...<%1>", mQuickSearchAction->shortcut().toString())); } void KMMainWidget::slotChangeDisplayMessageFormat(MessageViewer::Viewer::DisplayFormatMessage format) { if (format == MessageViewer::Viewer::Html) { const int result = KMessageBox::warningContinueCancel(this, // the warning text is taken from configuredialog.cpp: i18n("Use of HTML in mail will make you more vulnerable to " "\"spam\" and may increase the likelihood that your system will be " "compromised by other present and anticipated security exploits."), i18n("Security Warning"), KGuiItem(i18n("Use HTML")), KStandardGuiItem::cancel(), QStringLiteral("OverrideHtmlWarning"), Q_NULLPTR); if (result == KMessageBox::Cancel) { mDisplayMessageFormatMenu->setDisplayMessageFormat(MessageViewer::Viewer::Text); return; } } mFolderDisplayFormatPreference = format; //Update mPrefererHtmlLoadExtAction const bool useHtml = (mFolderDisplayFormatPreference == MessageViewer::Viewer::Html || (mHtmlGlobalSetting && mFolderDisplayFormatPreference == MessageViewer::Viewer::UseGlobalSetting)); mPreferHtmlLoadExtAction->setEnabled(useHtml); if (mMsgView) { mMsgView->setDisplayFormatMessageOverwrite(mFolderDisplayFormatPreference); mMsgView->update(true); } } void KMMainWidget::populateMessageListStatusFilterCombo() { mMessagePane->populateStatusFilterCombo(); } void KMMainWidget::slotCollectionRemoved(const Akonadi::Collection &col) { if (mFavoritesModel) { mFavoritesModel->removeCollection(col); } } void KMMainWidget::slotMarkAllMessageAsReadInCurrentFolderAndSubfolder() { if (mCurrentFolder && mCurrentFolder->collection().isValid()) { MarkAllMessagesAsReadInFolderAndSubFolderJob *job = new MarkAllMessagesAsReadInFolderAndSubFolderJob(this); job->setTopLevelCollection(mCurrentFolder->collection()); job->start(); } } diff --git a/messagelist/autotests/quicksearchlinetest.cpp b/messagelist/autotests/quicksearchlinetest.cpp index 91e97a6e86..e51d54a59d 100644 --- a/messagelist/autotests/quicksearchlinetest.cpp +++ b/messagelist/autotests/quicksearchlinetest.cpp @@ -1,387 +1,378 @@ /* Copyright (c) 2014-2015 Montel Laurent This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "quicksearchlinetest.h" #include #include "core/widgets/quicksearchline.h" #include #include #include #include #include #include #include using namespace MessageList::Core; QuickSearchLineTest::QuickSearchLineTest() { } void QuickSearchLineTest::shouldHaveDefaultValueOnCreation() { QuickSearchLine searchLine; QVERIFY(searchLine.searchEdit()->text().isEmpty()); QWidget *widget = searchLine.findChild(QStringLiteral("extraoptions")); QVERIFY(widget); QVERIFY(widget->isHidden()); QPushButton *moreButton = searchLine.findChild(QStringLiteral("moreoptions")); QVERIFY(moreButton); QCOMPARE(moreButton->icon().name(), QStringLiteral("arrow-down-double")); QWidget *quickSearchFilterWidget = searchLine.findChild(QStringLiteral("quicksearchfilterwidget")); QVERIFY(quickSearchFilterWidget); QVERIFY(quickSearchFilterWidget->isHidden()); QCOMPARE(searchLine.containsOutboundMessages(), false); QPushButton *fullMessageButton = searchLine.findChild(QStringLiteral("full_message")); QVERIFY(!fullMessageButton->isVisible()); QCOMPARE(fullMessageButton->isChecked(), true); QPushButton *bodyButton = searchLine.findChild(QStringLiteral("body")); QVERIFY(!bodyButton->isVisible()); QCOMPARE(bodyButton->isChecked(), false); QPushButton *subjectButton = searchLine.findChild(QStringLiteral("subject")); QVERIFY(!subjectButton->isVisible()); QCOMPARE(subjectButton->isChecked(), false); QPushButton *fromOrToButton = searchLine.findChild(QStringLiteral("fromorto")); QVERIFY(!fromOrToButton->isVisible()); QCOMPARE(fromOrToButton->isChecked(), false); QPushButton *bccButton = searchLine.findChild(QStringLiteral("bcc")); QVERIFY(!bccButton->isVisible()); QCOMPARE(bccButton->isChecked(), false); } void QuickSearchLineTest::shouldEmitTextChanged() { QuickSearchLine searchLine; searchLine.show(); QTest::qWaitForWindowExposed(&searchLine); QSignalSpy spy(&searchLine, SIGNAL(searchEditTextEdited(QString))); searchLine.searchEdit()->setText(QStringLiteral("F")); QCOMPARE(spy.count(), 0); searchLine.searchEdit()->clear(); searchLine.searchEdit()->setText(QStringLiteral("FO")); QCOMPARE(spy.count(), 0); searchLine.searchEdit()->clear(); searchLine.searchEdit()->setText(QStringLiteral("FOO")); QCOMPARE(spy.count(), 1); searchLine.searchEdit()->clear(); searchLine.searchEdit()->setText(QStringLiteral("FOOO")); QCOMPARE(spy.count(), 2); searchLine.searchEdit()->clear(); searchLine.searchEdit()->setText(QStringLiteral("FOOOO")); QCOMPARE(spy.count(), 3); } void QuickSearchLineTest::shouldShowExtraOptionWidget() { QuickSearchLine searchLine; searchLine.show(); QTest::keyClick(searchLine.searchEdit(), 'F'); QTest::qWaitForWindowExposed(&searchLine); QWidget *widget = searchLine.findChild(QStringLiteral("extraoptions")); QVERIFY(!widget->isVisible()); searchLine.searchEdit()->clear(); searchLine.searchEdit()->setText(QStringLiteral("FOOOO")); QTest::qWaitForWindowExposed(&searchLine); QVERIFY(widget->isVisible()); } void QuickSearchLineTest::shouldHideExtraOptionWidgetWhenClearLineEdit() { QuickSearchLine searchLine; searchLine.show(); QTest::keyClicks(searchLine.searchEdit(), QStringLiteral("FOOFOO")); QTest::qWaitForWindowExposed(&searchLine); QWidget *widget = searchLine.findChild(QStringLiteral("extraoptions")); searchLine.searchEdit()->clear(); QVERIFY(!widget->isVisible()); } void QuickSearchLineTest::shouldHideExtraOptionWidgetWhenResetFilter() { QuickSearchLine searchLine; searchLine.show(); QTest::keyClicks(searchLine.searchEdit(), QStringLiteral("FOOFOO")); QTest::qWaitForWindowExposed(&searchLine); QWidget *widget = searchLine.findChild(QStringLiteral("extraoptions")); searchLine.resetFilter(); QVERIFY(!widget->isVisible()); } void QuickSearchLineTest::shouldEmitSearchOptionChanged() { QuickSearchLine searchLine; searchLine.show(); QSignalSpy spy(&searchLine, SIGNAL(searchOptionChanged())); QPushButton *button = searchLine.findChild(QStringLiteral("subject")); QTest::mouseClick(button, Qt::LeftButton); QCOMPARE(spy.count(), 1); } void QuickSearchLineTest::shouldEmitSearchOptionChangedWhenUseTabPress() { QuickSearchLine searchLine; searchLine.show(); QTest::qWaitForWindowExposed(&searchLine); QPushButton *button = searchLine.findChild(QStringLiteral("full_message")); QTest::mouseClick(button, Qt::LeftButton); QTest::keyClick(button, Qt::Key_Right); QSignalSpy spy(&searchLine, SIGNAL(searchOptionChanged())); button = searchLine.findChild(QStringLiteral("body")); QTest::mouseClick(button, Qt::LeftButton); QCOMPARE(spy.count(), 1); } void QuickSearchLineTest::shouldResetAllWhenResetFilter() { QuickSearchLine searchLine; searchLine.show(); searchLine.resetFilter(); QCOMPARE(searchLine.status().count(), 0); QCOMPARE(searchLine.tagFilterComboBox()->currentIndex(), -1); - QuickSearchLine::SearchOptions options; - options = QuickSearchLine::SearchEveryWhere; - QCOMPARE(searchLine.searchOptions(), options); } void QuickSearchLineTest::shouldShowTagComboBox() { QuickSearchLine searchLine; searchLine.show(); QTest::qWaitForWindowExposed(&searchLine); QCOMPARE(searchLine.tagFilterComboBox()->isVisible(), false); searchLine.tagFilterComboBox()->addItems(QStringList() << QStringLiteral("1") << QStringLiteral("2")); searchLine.updateComboboxVisibility(); QCOMPARE(searchLine.tagFilterComboBox()->isVisible(), true); } void QuickSearchLineTest::shouldResetComboboxWhenResetFilter() { QuickSearchLine searchLine; searchLine.show(); QTest::qWaitForWindowExposed(&searchLine); QCOMPARE(searchLine.tagFilterComboBox()->isVisible(), false); searchLine.tagFilterComboBox()->addItems(QStringList() << QStringLiteral("1") << QStringLiteral("2")); searchLine.updateComboboxVisibility(); QCOMPARE(searchLine.tagFilterComboBox()->isVisible(), true); searchLine.tagFilterComboBox()->setCurrentIndex(1); QCOMPARE(searchLine.tagFilterComboBox()->currentIndex(), 1); searchLine.resetFilter(); QCOMPARE(searchLine.tagFilterComboBox()->currentIndex(), 0); } void QuickSearchLineTest::shouldNotEmitTextChangedWhenTextTrimmedIsEmpty() { QuickSearchLine searchLine; QSignalSpy spy(&searchLine, SIGNAL(searchEditTextEdited(QString))); searchLine.searchEdit()->setText(QStringLiteral(" ")); QCOMPARE(spy.count(), 0); searchLine.searchEdit()->setText(QStringLiteral(" FOO")); QCOMPARE(spy.count(), 1); } void QuickSearchLineTest::shouldShowExtraOptionWidgetWhenTextTrimmedIsNotEmpty() { QuickSearchLine searchLine; searchLine.show(); searchLine.searchEdit()->setText(QStringLiteral(" ")); QTest::qWaitForWindowExposed(&searchLine); QWidget *widget = searchLine.findChild(QStringLiteral("extraoptions")); QVERIFY(!widget->isVisible()); searchLine.searchEdit()->clear(); searchLine.searchEdit()->setText(QStringLiteral(" ")); QVERIFY(!widget->isVisible()); searchLine.searchEdit()->clear(); searchLine.searchEdit()->setText(QStringLiteral("")); QVERIFY(!widget->isVisible()); searchLine.searchEdit()->clear(); searchLine.searchEdit()->setText(QStringLiteral("FOO0 ")); QVERIFY(widget->isVisible()); searchLine.searchEdit()->clear(); searchLine.searchEdit()->setText(QStringLiteral("FOOO ")); QVERIFY(widget->isVisible()); } void QuickSearchLineTest::shouldShowMoreOptionWhenClickOnMoreButton() { QuickSearchLine searchLine; searchLine.show(); QTest::qWaitForWindowExposed(&searchLine); QPushButton *moreButton = searchLine.findChild(QStringLiteral("moreoptions")); QTest::mouseClick(moreButton, Qt::LeftButton); QWidget *quickSearchFilterWidget = searchLine.findChild(QStringLiteral("quicksearchfilterwidget")); QVERIFY(quickSearchFilterWidget->isVisible()); QCOMPARE(moreButton->icon().name(), QStringLiteral("arrow-up-double")); QTest::mouseClick(moreButton, Qt::LeftButton); QVERIFY(!quickSearchFilterWidget->isVisible()); QCOMPARE(moreButton->icon().name(), QStringLiteral("arrow-down-double")); } void QuickSearchLineTest::shouldChangeFromButtonLabelWhenChangeOutboundMessagesValue() { QuickSearchLine searchLine; QPushButton *button = searchLine.findChild(QStringLiteral("fromorto")); const QString buttonName = button->text(); searchLine.setContainsOutboundMessages(true); QVERIFY(button->text() != buttonName); searchLine.setContainsOutboundMessages(false); QCOMPARE(button->text(), buttonName); } void QuickSearchLineTest::shouldSearchToOrFrom() { QuickSearchLine searchLine; QPushButton *button = searchLine.findChild(QStringLiteral("fromorto")); QTest::mouseClick(button, Qt::LeftButton); searchLine.setContainsOutboundMessages(true); - QuickSearchLine::SearchOptions options; - options = QuickSearchLine::SearchAgainstTo; - QCOMPARE(searchLine.searchOptions(), options); - searchLine.setContainsOutboundMessages(false); - options = QuickSearchLine::SearchAgainstFrom; - QCOMPARE(searchLine.searchOptions(), options); } void QuickSearchLineTest::shouldHideShowWidgetWhenWeChangeVisibility() { QuickSearchLine searchLine; searchLine.show(); QWidget *widget = searchLine.findChild(QStringLiteral("extraoptions")); QPushButton *moreButton = searchLine.findChild(QStringLiteral("moreoptions")); QWidget *quickSearchFilterWidget = searchLine.findChild(QStringLiteral("quicksearchfilterwidget")); searchLine.changeQuicksearchVisibility(false); QCOMPARE(quickSearchFilterWidget->isVisible(), false); QCOMPARE(moreButton->isVisible(), false); QCOMPARE(widget->isVisible(), false); QCOMPARE(searchLine.searchEdit()->isVisible(), false); QCOMPARE(searchLine.tagFilterComboBox()->isVisible(), false); searchLine.changeQuicksearchVisibility(true); QCOMPARE(quickSearchFilterWidget->isVisible(), false); QCOMPARE(moreButton->isVisible(), true); QCOMPARE(widget->isVisible(), false); QCOMPARE(searchLine.searchEdit()->isVisible(), true); QCOMPARE(searchLine.tagFilterComboBox()->isVisible(), false); //Fill Combobox searchLine.tagFilterComboBox()->addItems(QStringList() << QStringLiteral("1") << QStringLiteral("2")); searchLine.changeQuicksearchVisibility(false); QCOMPARE(searchLine.tagFilterComboBox()->isVisible(), false); searchLine.changeQuicksearchVisibility(true); QCOMPARE(searchLine.tagFilterComboBox()->isVisible(), true); } void QuickSearchLineTest::shouldNotShowComboboxWhenWeAddNewItemWhenWeHiddedQuickSearchBarWidget() { QuickSearchLine searchLine; searchLine.show(); searchLine.tagFilterComboBox()->addItems(QStringList() << QStringLiteral("1") << QStringLiteral("2")); searchLine.updateComboboxVisibility(); QCOMPARE(searchLine.tagFilterComboBox()->isVisible(), true); searchLine.changeQuicksearchVisibility(false); searchLine.tagFilterComboBox()->addItems(QStringList() << QStringLiteral("1") << QStringLiteral("2")); searchLine.updateComboboxVisibility(); QCOMPARE(searchLine.tagFilterComboBox()->isVisible(), false); } void QuickSearchLineTest::shouldRestoreDefaultSearchOptionWhenTextIsEmpied() { QuickSearchLine searchLine; searchLine.show(); QPushButton *moreButton = searchLine.findChild(QStringLiteral("moreoptions")); QCOMPARE(moreButton->isVisible(), true); QTest::mouseClick(moreButton, Qt::LeftButton); QPushButton *fullMessageButton = searchLine.findChild(QStringLiteral("full_message")); QVERIFY(!fullMessageButton->isVisible()); QCOMPARE(fullMessageButton->isChecked(), true); QPushButton *bodyButton = searchLine.findChild(QStringLiteral("body")); QVERIFY(!bodyButton->isVisible()); QCOMPARE(bodyButton->isChecked(), false); QPushButton *subjectButton = searchLine.findChild(QStringLiteral("subject")); QVERIFY(!subjectButton->isVisible()); QCOMPARE(subjectButton->isChecked(), false); QPushButton *fromOrToButton = searchLine.findChild(QStringLiteral("fromorto")); QVERIFY(!fromOrToButton->isVisible()); QCOMPARE(fromOrToButton->isChecked(), false); QPushButton *bccButton = searchLine.findChild(QStringLiteral("bcc")); QVERIFY(!bccButton->isVisible()); QCOMPARE(bccButton->isChecked(), false); QTest::mouseClick(bccButton, Qt::LeftButton); QCOMPARE(fullMessageButton->isChecked(), false); QCOMPARE(bodyButton->isChecked(), false); QCOMPARE(subjectButton->isChecked(), false); QCOMPARE(fromOrToButton->isChecked(), false); QCOMPARE(bccButton->isChecked(), true); searchLine.resetFilter(); QCOMPARE(fullMessageButton->isChecked(), true); QCOMPARE(bodyButton->isChecked(), false); QCOMPARE(subjectButton->isChecked(), false); QCOMPARE(fromOrToButton->isChecked(), false); QCOMPARE(bccButton->isChecked(), false); } void QuickSearchLineTest::shouldHideExtraOptionWidgetWhenResetFilterWhenSetEmptyText() { QuickSearchLine searchLine; searchLine.show(); searchLine.searchEdit()->setText(QStringLiteral("FOOFOO")); QTest::qWaitForWindowExposed(&searchLine); QWidget *widget = searchLine.findChild(QStringLiteral("extraoptions")); QVERIFY(widget->isVisible()); searchLine.searchEdit()->clear(); QVERIFY(!widget->isVisible()); } QTEST_MAIN(QuickSearchLineTest) diff --git a/messagelist/src/core/filter.cpp b/messagelist/src/core/filter.cpp index b6ecd5439e..e4a33fb9f2 100644 --- a/messagelist/src/core/filter.cpp +++ b/messagelist/src/core/filter.cpp @@ -1,185 +1,165 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "core/filter.h" #include "core/messageitem.h" #include #include using namespace MessageList::Core; Filter::Filter(QObject *parent) : QObject(parent) { } bool Filter::containString(const QString &searchInString) const { bool found = false; Q_FOREACH (const QString &str, mSearchList) { if (searchInString.contains(str, Qt::CaseInsensitive)) { found = true; } else { found = false; break; } } return found; } bool Filter::match(const MessageItem *item) const { if (!mStatus.isEmpty()) { Q_FOREACH (Akonadi::MessageStatus status, mStatus) { if (!(status & item->status())) { return false; } } } if (!mSearchString.isEmpty()) { if (mMatchingItemIds.contains(item->itemId())) { return true; } bool searchMatches = false; if (containString(item->subject())) { searchMatches = true; } else if (containString(item->sender())) { searchMatches = true; } else if (containString(item->receiver())) { searchMatches = true; } if (!searchMatches) { return false; } } if (!mTagId.isEmpty()) { //mTagId is a Akonadi::Tag::url const bool tagMatches = item->findTag(mTagId) != Q_NULLPTR; if (!tagMatches) { return false; } } return true; } bool Filter::isEmpty() const { if (!mStatus.isEmpty()) { return false; } if (!mSearchString.isEmpty()) { return false; } if (!mTagId.isEmpty()) { return false; } return true; } void Filter::clear() { mStatus.clear(); mSearchString.clear(); mTagId.clear(); mMatchingItemIds.clear(); mSearchList.clear(); } void Filter::setCurrentFolder(const Akonadi::Collection &folder) { mCurrentFolder = folder; } -QuickSearchLine::SearchOptions Filter::currentOptions() const -{ - return mOptions; -} - -void Filter::setSearchString(const QString &search, QuickSearchLine::SearchOptions options) +void Filter::setSearchString(const QString &search) { const QString trimStr = search.trimmed(); - if ((mSearchString == trimStr) && (mOptions == options)) { + if (mSearchString == trimStr) { return; } - mOptions = options; mSearchString = trimStr; mMatchingItemIds.clear(); if (mSearchString.isEmpty()) { return; } bool needToSplitString = false; QString newStr = mSearchString; if (mSearchString.startsWith(QLatin1Char('"')) && mSearchString.startsWith(QLatin1Char('"'))) { newStr = newStr.remove(0, 1); newStr = newStr.remove(newStr.length() - 1, 1); mSearchList = QStringList() << newStr; } else { const QStringList searchListTmp = mSearchString.split(QLatin1Char(' '), QString::SkipEmptyParts); mSearchList.clear(); newStr.clear(); Q_FOREACH (const QString &text, searchListTmp) { if (text.size() >= 3) { mSearchList << text; if (!newStr.isEmpty()) { newStr += QLatin1Char(' '); } newStr += text; } } needToSplitString = true; } if (!newStr.trimmed().isEmpty()) { Akonadi::Search::PIM::EmailQuery query; - if (options & QuickSearchLine::SearchEveryWhere) { - query.matches(newStr); - query.setSplitSearchMatchString(needToSplitString); - } else if (options & QuickSearchLine::SearchAgainstSubject) { - query.subjectMatches(newStr); - } else if (options & QuickSearchLine::SearchAgainstBody) { - query.bodyMatches(newStr); - } else if (options & QuickSearchLine::SearchAgainstFrom) { - query.setFrom(newStr); - } else if (options & QuickSearchLine::SearchAgainstBcc) { - query.setBcc(QStringList() << newStr); - } else if (options & QuickSearchLine::SearchAgainstTo) { - query.setTo(QStringList() << newStr); - } //If the collection is virtual we're probably trying to filter the search collection, so we just search globally if (mCurrentFolder.isValid() && !mCurrentFolder.isVirtual()) { query.addCollection(mCurrentFolder.id()); } Akonadi::Search::PIM::ResultIterator it = query.exec(); while (it.next()) { mMatchingItemIds << it.id(); } } Q_EMIT finished(); } diff --git a/messagelist/src/core/filter.h b/messagelist/src/core/filter.h index 33fe717d4b..602d93e82a 100644 --- a/messagelist/src/core/filter.h +++ b/messagelist/src/core/filter.h @@ -1,138 +1,136 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #ifndef __MESSAGELIST_CORE_FILTER_H__ #define __MESSAGELIST_CORE_FILTER_H__ #include #include #include #include #include #include "widgets/quicksearchline.h" namespace MessageList { namespace Core { class MessageItem; /** * This class is responsable of matching messages that should be displayed * in the View. It's used mainly by Model and Widget. */ class Filter : public QObject { Q_OBJECT public: explicit Filter(QObject *parent = Q_NULLPTR); public: /** * Returns true if the specified parameters match this filter and false otherwise. * The msg pointer must not be null. */ bool match(const MessageItem *item) const; /** * Returns the currently set status mask */ QList status() const { return mStatus; } /** * Sets the status mask for this filter. */ void setStatus(const QList &lstStatus) { mStatus = lstStatus; } /** * Sets the current folder of this filter. */ void setCurrentFolder(const Akonadi::Collection &collection); /** * Returns the currently set search string. */ const QString &searchString() const { return mSearchString; } /** * Sets the search string for this filter. */ - void setSearchString(const QString &search, QuickSearchLine::SearchOptions options); + void setSearchString(const QString &search); /** * Returns the currently set MessageItem::Tag id */ const QString &tagId() const { return mTagId; } /** * Sets the id of a MessageItem::Tag that the matching messages must contain. */ void setTagId(const QString &tagId) { mTagId = tagId; } /** * Clears this filter (sets status to 0, search string and tag id to empty strings) */ void clear(); /** * Returns true if this filter is empty (0 status mask, empty search string and empty tag) * and it's useless to call match() that will always return true. */ bool isEmpty() const; - QuickSearchLine::SearchOptions currentOptions() const; Q_SIGNALS: void finished(); private: bool containString(const QString &searchInString) const; QList mStatus; ///< Messages must match theses status, if non 0 QString mSearchString; ///< Messages must match this search string, if not empty QString mTagId; ///< Messages must have this tag, if not empty. Contains a tag url. Akonadi::Collection mCurrentFolder; QSet mMatchingItemIds; - QuickSearchLine::SearchOptions mOptions; QStringList mSearchList; }; } // namespace Core } // namespace MessageList #endif //!__MESSAGELIST_CORE_FILTER_H__ diff --git a/messagelist/src/core/view.cpp b/messagelist/src/core/view.cpp index b2045bc266..e7a4a33c73 100644 --- a/messagelist/src/core/view.cpp +++ b/messagelist/src/core/view.cpp @@ -1,2772 +1,2767 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "core/view.h" #include "core/aggregation.h" #include "core/delegate.h" #include "core/groupheaderitem.h" #include "core/item.h" #include "core/manager.h" #include "core/messageitem.h" #include "core/model.h" #include "core/theme.h" #include "messagelistsettings.h" #include "core/storagemodelbase.h" #include "core/widgetbase.h" #include "messagelistutil.h" #include "MessageCore/StringUtil" #include // kdepimlibs #include #include #include #include #include #include #include #include #include #include #include #include "messagelist_debug.h" using namespace MessageList::Core; class Q_DECL_HIDDEN View::Private { public: Private(View *owner, Widget *parent) : q(owner), mWidget(parent), mModel(Q_NULLPTR), mDelegate(new Delegate(owner)), mAggregation(Q_NULLPTR), mTheme(Q_NULLPTR), mNeedToApplyThemeColumns(false), mLastCurrentItem(Q_NULLPTR), mFirstShow(true), mSaveThemeColumnStateOnSectionResize(true), mSaveThemeColumnStateTimer(Q_NULLPTR), mApplyThemeColumnsTimer(Q_NULLPTR), mIgnoreUpdateGeometries(false) { } void expandFullThread(const QModelIndex &index); View *const q; Widget *mWidget; Model *mModel; Delegate *mDelegate; const Aggregation *mAggregation; ///< The Aggregation we're using now, shallow pointer Theme *mTheme; ///< The Theme we're using now, shallow pointer bool mNeedToApplyThemeColumns; ///< Flag signaling a pending application of theme columns Item *mLastCurrentItem; QPoint mMousePressPosition; bool mFirstShow; bool mSaveThemeColumnStateOnSectionResize; ///< This is used to filter out programmatic column resizes in slotSectionResized(). QTimer *mSaveThemeColumnStateTimer; ///< Used to trigger a delayed "save theme state" QTimer *mApplyThemeColumnsTimer; ///< Used to trigger a delayed "apply theme columns" bool mIgnoreUpdateGeometries; ///< Shall we ignore the "update geometries" calls ? }; View::View(Widget *pParent) : QTreeView(pParent), d(new Private(this, pParent)) { d->mSaveThemeColumnStateTimer = new QTimer(); connect(d->mSaveThemeColumnStateTimer, &QTimer::timeout, this, &View::saveThemeColumnState); d->mApplyThemeColumnsTimer = new QTimer(); connect(d->mApplyThemeColumnsTimer, &QTimer::timeout, this, &View::applyThemeColumns); setItemDelegate(d->mDelegate); setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setAlternatingRowColors(true); setAllColumnsShowFocus(true); setSelectionMode(QAbstractItemView::ExtendedSelection); viewport()->setAcceptDrops(true); header()->setContextMenuPolicy(Qt::CustomContextMenu); connect(header(), &QWidget::customContextMenuRequested, this, &View::slotHeaderContextMenuRequested); connect(header(), &QHeaderView::sectionResized, this, &View::slotHeaderSectionResized); header()->setClickable(true); header()->setResizeMode(QHeaderView::Interactive); header()->setMinimumSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value header()->setDefaultSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value d->mModel = new Model(this); setModel(d->mModel); connect(d->mModel, &Model::statusMessage, pParent, &Widget::statusMessage); //connect( selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), // this, SLOT(slotCurrentIndexChanged(QModelIndex,QModelIndex)) ); connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection); // as in KDE3, when a root-item of a message thread is expanded, expand all children connect(this, SIGNAL(expanded(QModelIndex)), this, SLOT(expandFullThread(QModelIndex))); } View::~View() { if (d->mSaveThemeColumnStateTimer->isActive()) { d->mSaveThemeColumnStateTimer->stop(); } delete d->mSaveThemeColumnStateTimer; if (d->mApplyThemeColumnsTimer->isActive()) { d->mApplyThemeColumnsTimer->stop(); } delete d->mApplyThemeColumnsTimer; // Zero out the theme, aggregation and ApplyThemeColumnsTimer so Model will not cause accesses to them in its destruction process d->mApplyThemeColumnsTimer = Q_NULLPTR; d->mTheme = Q_NULLPTR; d->mAggregation = Q_NULLPTR; delete d; d = Q_NULLPTR; } Model *View::model() const { return d->mModel; } Delegate *View::delegate() const { return d->mDelegate; } void View::ignoreCurrentChanges(bool ignore) { if (ignore) { disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged); viewport()->setUpdatesEnabled(false); } else { connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection); viewport()->setUpdatesEnabled(true); } } void View::ignoreUpdateGeometries(bool ignore) { d->mIgnoreUpdateGeometries = ignore; } bool View::isScrollingLocked() const { // There is another popular requisite: people want the view to automatically // scroll in order to show new arriving mail. This actually makes sense // only when the view is sorted by date and the new mail is (usually) either // appended at the bottom or inserted at the top. It would be also confusing // when the user is browsing some other thread in the meantime. // // So here we make a simple guess: if the view is scrolled somewhere in the // middle then we assume that the user is browsing other threads and we // try to keep the currently selected item steady on the screen. // When the view is "locked" to the top (scrollbar value 0) or to the // bottom (scrollbar value == maximum) then we assume that the user // isn't browsing and we should attempt to show the incoming messages // by keeping the view "locked". // // The "locking" also doesn't make sense in the first big fill view job. // [Well this concept is pre-akonadi. Now the loading is all async anyway... // So all this code is actually triggered during the initial loading, too.] const int scrollBarPosition = verticalScrollBar()->value(); const int scrollBarMaximum = verticalScrollBar()->maximum(); const SortOrder *sortOrder = d->mModel->sortOrder(); const bool lockView = ( // not the first loading job !d->mModel->isLoading() ) && ( // messages sorted by date (sortOrder->messageSorting() == SortOrder::SortMessagesByDateTime) || (sortOrder->messageSorting() == SortOrder::SortMessagesByDateTimeOfMostRecent) ) && ( // scrollbar at top (Descending order) or bottom (Ascending order) (scrollBarPosition == 0 && sortOrder->messageSortDirection() == SortOrder::Descending) || (scrollBarPosition == scrollBarMaximum && sortOrder->messageSortDirection() == SortOrder::Ascending) ); return lockView; } void View::updateGeometries() { if (d->mIgnoreUpdateGeometries || !d->mModel) { return; } const int scrollBarPositionBefore = verticalScrollBar()->value(); const bool lockView = isScrollingLocked(); QTreeView::updateGeometries(); if (lockView) { // we prefer to keep the view locked to the top or bottom if (scrollBarPositionBefore != 0) { // we wanted the view to be locked to the bottom if (verticalScrollBar()->value() != verticalScrollBar()->maximum()) { verticalScrollBar()->setValue(verticalScrollBar()->maximum()); } } // else we wanted the view to be locked to top and we shouldn't need to do anything } } StorageModel *View::storageModel() const { return d->mModel->storageModel(); } void View::setAggregation(const Aggregation *aggregation) { d->mAggregation = aggregation; d->mModel->setAggregation(aggregation); // use uniform row heights to speed up, but only if there are no group headers used setUniformRowHeights(d->mAggregation->grouping() == Aggregation::NoGrouping); } void View::setTheme(Theme *theme) { d->mNeedToApplyThemeColumns = true; d->mTheme = theme; d->mDelegate->setTheme(theme); d->mModel->setTheme(theme); } void View::setSortOrder(const SortOrder *sortOrder) { d->mModel->setSortOrder(sortOrder); } void View::reload() { setStorageModel(storageModel()); } void View::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode) { // This will cause the model to be reset. d->mSaveThemeColumnStateOnSectionResize = false; d->mModel->setStorageModel(storageModel, preSelectionMode); d->mSaveThemeColumnStateOnSectionResize = true; } void View::modelJobBatchStarted() { // This is called by the model when the first job of a batch starts d->mWidget->viewJobBatchStarted(); } void View::modelJobBatchTerminated() { // This is called by the model when all the pending jobs have been processed d->mWidget->viewJobBatchTerminated(); } void View::modelHasBeenReset() { // This is called by Model when it has been reset. if (d && d->mNeedToApplyThemeColumns) { applyThemeColumns(); } } ////////////////////////////////////////////////////////////////////////////////////////////////////// // Theme column state machinery // // This is yet another beast to beat. The QHeaderView behaviour, at the time of writing, // is quite unpredictable. This is due to the complex interaction with the model, with the QTreeView // and due to its attempts to delay the layout jobs. The delayed layouts, especially, may // cause the widths of the columns to quickly change in an unexpected manner in a place // where previously they have been always settled to the values you set... // // So here we have the tools to: // // - Apply the saved state of the theme columns (applyThemeColumns()). // This function computes the "best fit" state of the visible columns and tries // to apply it to QHeaderView. It also saves the new computed state to the Theme object. // // - Explicitly save the column state, used when the user changes the widths or visibility manually. // This is called through a delayed timer after a column has been resized or used directly // when the visibility state of a column has been changed by toggling a popup menu entry. // // - Display the column state context popup menu and handle its actions // // - Apply the theme columns when the theme changes, when the model changes or in certain // ugly corner cases when the widget is resized or shown. // // - Avoid saving a corrupted column state in that QHeaderView can be found *very* frequently. // void View::applyThemeColumns() { if (!d->mApplyThemeColumnsTimer) { return; } if (d->mApplyThemeColumnsTimer->isActive()) { d->mApplyThemeColumnsTimer->stop(); } if (!d->mTheme) { return; } //qCDebug(MESSAGELIST_LOG) << "Apply theme columns"; const QList< Theme::Column * > &columns = d->mTheme->columns(); if (columns.isEmpty()) { return; // bad theme } if (!viewport()->isVisible()) { return; // invisible } if (viewport()->width() < 1) { return; // insane width } // Now we want to distribute the available width on all the visible columns. // // The rules: // - The visible columns will span the width of the view, if possible. // - The columns with a saved width should take that width. // - The columns on the left should take more space, if possible. // - The columns with no text take just slightly more than their size hint. // while the columns with text take possibly a lot more. // // Note that the first column is always shown (it can't be hidden at all) // The algorithm below is a sort of compromise between: // - Saving the user preferences for widths // - Using exactly the available view space // // It "tends to work" in all cases: // - When there are no user preferences saved and the column widths must be // automatically computed to make best use of available space // - When there are user preferences for only some of the columns // and that should be somewhat preserved while still using all the // available space. // - When all the columns have well defined saved widths QList< Theme::Column * >::ConstIterator it; int idx = 0; // Gather total size "hint" for visible sections: if the widths of the columns wers // all saved then the total hint is equal to the total saved width. int totalVisibleWidthHint = 0; QList< int > lColumnSizeHints; QList< Theme::Column * >::ConstIterator end(columns.end()); for (it = columns.constBegin(); it != end; ++it) { if ((*it)->currentlyVisible() || (idx == 0)) { //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be visible"; // Column visible const int savedWidth = (*it)->currentWidth(); const int hintWidth = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width(); totalVisibleWidthHint += savedWidth > 0 ? savedWidth : hintWidth; lColumnSizeHints.append(hintWidth); //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " size hint is " << hintWidth; } else { //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be not visible"; // The column is not visible lColumnSizeHints.append(-1); // dummy } idx++; } if (totalVisibleWidthHint < 16) { totalVisibleWidthHint = 16; // be reasonable } // Now compute somewhat "proportional" widths. idx = 0; QList< int > lColumnWidths; lColumnWidths.reserve(columns.count()); int totalVisibleWidth = 0; end = columns.constEnd(); for (it = columns.constBegin(); it != end; ++it) { int savedWidth = (*it)->currentWidth(); int hintWidth = savedWidth > 0 ? savedWidth : lColumnSizeHints[ idx ]; int realWidth; if ((*it)->currentlyVisible() || (idx == 0)) { if ((*it)->containsTextItems()) { // the column contains text items, it should get more space (if possible) realWidth = ((hintWidth * viewport()->width()) / totalVisibleWidthHint); } else { // the column contains no text items, it should get exactly its hint/saved width. realWidth = hintWidth; } if (realWidth < 2) { realWidth = 2; // don't allow very insane values } totalVisibleWidth += realWidth; } else { // Column not visible realWidth = -1; } lColumnWidths.append(realWidth); idx++; } // Now the algorithm above may be wrong for several reasons... // - We're using fixed widths for certain columns and proportional // for others... // - The user might have changed the width of the view from the // time in that the widths have been saved // - There are some (not well identified) issues with the QTreeView // scrollbar that make our view appear larger or shorter by 2-3 pixels // sometimes. // - ... // So we correct the previous estimates by trying to use exactly // the available space. idx = 0; if (totalVisibleWidth != viewport()->width()) { // The estimated widths were not using exactly the available space. if (totalVisibleWidth < viewport()->width()) { // We were using less space than available. // Give the additional space to the text columns // also give more space to the first ones and less space to the last ones int available = viewport()->width() - totalVisibleWidth; end = columns.end(); for (it = columns.begin(); it != end; ++it) { if (((*it)->currentlyVisible() || (idx == 0)) && (*it)->containsTextItems()) { // give more space to this column available >>= 1; // eat half of the available space lColumnWidths[ idx ] += available; // and give it to this column if (available < 1) { break; // no more space to give away } } idx++; } // if any space is still available, give it to the first column if (available) { lColumnWidths[ 0 ] += available; } } else { // We were using more space than available // If the columns span just a little bit more than the view then // try to squeeze them in order to make them fit if (totalVisibleWidth < (viewport()->width() + 100)) { int missing = totalVisibleWidth - viewport()->width(); int count = lColumnWidths.count(); if (missing > 0) { idx = count - 1; while (idx >= 0) { if (columns.at(idx)->currentlyVisible() || (idx == 0)) { int chop = lColumnWidths[ idx ] - lColumnSizeHints[ idx ]; if (chop > 0) { if (chop > missing) { chop = missing; } lColumnWidths[ idx ] -= chop; missing -= chop; if (missing < 1) { break; // no more space to recover } } } // else it's invisible idx--; } } } } } // We're ready to assign widths. bool oldSave = d->mSaveThemeColumnStateOnSectionResize; d->mSaveThemeColumnStateOnSectionResize = false; // A huge problem here is that QHeaderView goes quite nuts if we show or hide sections // while resizing them. This is because it has several machineries aimed to delay // the layout to the last possible moment. So if we show a column, it will tend to // screw up the layout of other ones. // We first loop showing/hiding columns then. idx = 0; //qCDebug(MESSAGELIST_LOG) << "Entering column show/hide loop"; end = columns.constEnd(); for (it = columns.constBegin(); it != end; ++it) { bool visible = (idx == 0) || (*it)->currentlyVisible(); //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " visible " << visible; (*it)->setCurrentlyVisible(visible); header()->setSectionHidden(idx, !visible); idx++; } // Then we loop assigning widths. This is still complicated since QHeaderView tries // very badly to stretch the last section and thus will resize it in the meantime. // But seems to work most of the times... idx = 0; end = columns.constEnd(); for (it = columns.constBegin(); it != end; ++it) { if ((*it)->currentlyVisible()) { //qCDebug(MESSAGELIST_LOG) << "Resize section " << idx << " to " << lColumnWidths[ idx ]; const int columnWidth(lColumnWidths[ idx ]); (*it)->setCurrentWidth(columnWidth); header()->resizeSection(idx, columnWidth); } else { (*it)->setCurrentWidth(-1); } idx++; } idx = 0; bool bTriggeredQtBug = false; end = columns.constEnd(); for (it = columns.constBegin(); it != end; ++it) { if (!header()->isSectionHidden(idx)) { if (!(*it)->currentlyVisible()) { bTriggeredQtBug = true; } } idx++; } setHeaderHidden(d->mTheme->viewHeaderPolicy() == Theme::NeverShowHeader); d->mSaveThemeColumnStateOnSectionResize = oldSave; d->mNeedToApplyThemeColumns = false; static bool bAllowRecursion = true; if (bTriggeredQtBug && bAllowRecursion) { bAllowRecursion = false; //qCDebug(MESSAGELIST_LOG) << "I've triggered the QHeaderView bug: trying to fix by calling myself again"; applyThemeColumns(); bAllowRecursion = true; } } void View::triggerDelayedApplyThemeColumns() { if (d->mApplyThemeColumnsTimer->isActive()) { d->mApplyThemeColumnsTimer->stop(); } d->mApplyThemeColumnsTimer->setSingleShot(true); d->mApplyThemeColumnsTimer->start(100); } void View::saveThemeColumnState() { if (d->mSaveThemeColumnStateTimer->isActive()) { d->mSaveThemeColumnStateTimer->stop(); } if (!d->mTheme) { return; } if (d->mNeedToApplyThemeColumns) { return; // don't save the state if it hasn't been applied at all } //qCDebug(MESSAGELIST_LOG) << "Save theme column state"; const QList< Theme::Column * > &columns = d->mTheme->columns(); if (columns.isEmpty()) { return; // bad theme } int idx = 0; QList< Theme::Column * >::ConstIterator end(columns.constEnd()); for (QList< Theme::Column * >::ConstIterator it = columns.constBegin(); it != end; ++it) { if (header()->isSectionHidden(idx)) { //qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is hidden"; (*it)->setCurrentlyVisible(false); (*it)->setCurrentWidth(-1); // reset (hmmm... we could use the "don't touch" policy here too...) } else { //qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is visible and has size " << header()->sectionSize( idx ); (*it)->setCurrentlyVisible(true); (*it)->setCurrentWidth(header()->sectionSize(idx)); } idx++; } } void View::triggerDelayedSaveThemeColumnState() { if (d->mSaveThemeColumnStateTimer->isActive()) { d->mSaveThemeColumnStateTimer->stop(); } d->mSaveThemeColumnStateTimer->setSingleShot(true); d->mSaveThemeColumnStateTimer->start(200); } void View::resizeEvent(QResizeEvent *e) { qCDebug(MESSAGELIST_LOG) << "Resize event enter (viewport width is " << viewport()->width() << ")"; QTreeView::resizeEvent(e); if (!isVisible()) { return; // don't play with } if ((!d->mFirstShow) && d->mNeedToApplyThemeColumns) { triggerDelayedApplyThemeColumns(); } if (header()->isVisible()) { return; } // header invisible bool oldSave = d->mSaveThemeColumnStateOnSectionResize; d->mSaveThemeColumnStateOnSectionResize = false; const int count = header()->count(); if ((count - header()->hiddenSectionCount()) < 2) { // a single column visible: resize it int visibleIndex; for (visibleIndex = 0; visibleIndex < count; visibleIndex++) { if (!header()->isSectionHidden(visibleIndex)) { break; } } if (visibleIndex < count) { header()->resizeSection(visibleIndex, viewport()->width() - 4); } } d->mSaveThemeColumnStateOnSectionResize = oldSave; triggerDelayedSaveThemeColumnState(); } void View::modelAboutToEmitLayoutChanged() { // QHeaderView goes totally NUTS with a layoutChanged() call d->mSaveThemeColumnStateOnSectionResize = false; } void View::modelEmittedLayoutChanged() { // This is after a first chunk of work has been done by the model: do apply column states d->mSaveThemeColumnStateOnSectionResize = true; applyThemeColumns(); } void View::slotHeaderSectionResized(int logicalIndex, int oldWidth, int newWidth) { Q_UNUSED(logicalIndex); Q_UNUSED(oldWidth); Q_UNUSED(newWidth); if (d->mSaveThemeColumnStateOnSectionResize) { triggerDelayedSaveThemeColumnState(); } } int View::sizeHintForColumn(int logicalColumnIndex) const { // QTreeView: please don't touch my column widths... int w = header()->sectionSize(logicalColumnIndex); if (w > 0) { return w; } if (!d->mDelegate) { return 32; // dummy } w = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, logicalColumnIndex).width(); return w; } void View::showEvent(QShowEvent *e) { QTreeView::showEvent(e); if (d->mFirstShow) { // If we're shown for the first time and the theme has been already set // then we need to reapply the theme column widths since the previous // application probably used invalid widths. // if (d->mTheme) { triggerDelayedApplyThemeColumns(); } d->mFirstShow = false; } } void View::slotHeaderContextMenuRequested(const QPoint &pnt) { if (!d->mTheme) { return; } const QList< Theme::Column * > &columns = d->mTheme->columns(); if (columns.isEmpty()) { return; // bad theme } // the menu for the columns QMenu menu; QSignalMapper *showColumnSignalMapper = new QSignalMapper(&menu); int idx = 0; QList< Theme::Column * >::ConstIterator end(columns.end()); for (QList< Theme::Column * >::ConstIterator it = columns.begin(); it != end; ++it) { QAction *act = menu.addAction((*it)->label()); act->setCheckable(true); act->setChecked(!header()->isSectionHidden(idx)); if (idx == 0) { act->setEnabled(false); } QObject::connect(act, SIGNAL(triggered()), showColumnSignalMapper, SLOT(map())); showColumnSignalMapper->setMapping(act, idx); idx++; } QObject::connect(showColumnSignalMapper, SIGNAL(mapped(int)), this, SLOT(slotShowHideColumn(int))); menu.addSeparator(); { QAction *act = menu.addAction(i18n("Adjust Column Sizes")); QObject::connect(act, &QAction::triggered, this, &View::slotAdjustColumnSizes); } { QAction *act = menu.addAction(i18n("Show Default Columns")); QObject::connect(act, &QAction::triggered, this, &View::slotShowDefaultColumns); } menu.addSeparator(); { QAction *act = menu.addAction(i18n("Display Tooltips")); act->setCheckable(true); act->setChecked(MessageListSettings::self()->messageToolTipEnabled()); QObject::connect(act, &QAction::triggered, this, &View::slotDisplayTooltips); } menu.addSeparator(); MessageList::Util::fillViewMenu(&menu, d->mWidget); menu.exec(header()->mapToGlobal(pnt)); } void View::slotAdjustColumnSizes() { if (!d->mTheme) { return; } d->mTheme->resetColumnSizes(); applyThemeColumns(); } void View::slotShowDefaultColumns() { if (!d->mTheme) { return; } d->mTheme->resetColumnState(); applyThemeColumns(); } void View::slotDisplayTooltips(bool showTooltips) { MessageListSettings::self()->setMessageToolTipEnabled(showTooltips); } void View::slotShowHideColumn(int columnIdx) { if (!d->mTheme) { return; // oops } if (columnIdx == 0) { return; // can never be hidden } if (columnIdx >= d->mTheme->columns().count()) { return; } const bool showIt = header()->isSectionHidden(columnIdx); Theme::Column *column = d->mTheme->columns().at(columnIdx); Q_ASSERT(column); // first save column state (as it is, with the column still in previous state) saveThemeColumnState(); // If a section has just been shown, invalidate its width in the skin // since QTreeView assigned it a (possibly insane) default width. // If a section has been hidden, then invalidate its width anyway... // so finally invalidate width always, here. column->setCurrentlyVisible(showIt); column->setCurrentWidth(-1); // then apply theme columns to re-compute proportional widths (so we hopefully stay in the view) applyThemeColumns(); } Item *View::currentItem() const { QModelIndex idx = currentIndex(); if (!idx.isValid()) { return Q_NULLPTR; } Item *it = static_cast< Item * >(idx.internalPointer()); Q_ASSERT(it); return it; } MessageItem *View::currentMessageItem(bool selectIfNeeded) const { Item *it = currentItem(); if (!it || (it->type() != Item::Message)) { return Q_NULLPTR; } if (selectIfNeeded) { // Keep things coherent, if the user didn't select it, but acted on it via // a shortcut, do select it now. if (!selectionModel()->isSelected(currentIndex())) { selectionModel()->select(currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows); } } return static_cast< MessageItem * >(it); } void View::setCurrentMessageItem(MessageItem *it, bool center) { if (it) { qCDebug(MESSAGELIST_LOG) << "Setting current message to" << it->subject(); const QModelIndex index = d->mModel->index(it, 0); selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows); if (center) { scrollTo(index, QAbstractItemView::PositionAtCenter); } } else selectionModel()->setCurrentIndex(QModelIndex(), QItemSelectionModel::Current | QItemSelectionModel::Clear); } bool View::selectionEmpty() const { return selectionModel()->selectedRows().isEmpty(); } QList< MessageItem * > View::selectionAsMessageItemList(bool includeCollapsedChildren) const { QList< MessageItem * > selectedMessages; QModelIndexList lSelected = selectionModel()->selectedRows(); if (lSelected.isEmpty()) { return selectedMessages; } QModelIndexList::ConstIterator end(lSelected.constEnd()); for (QModelIndexList::ConstIterator it = lSelected.constBegin(); it != end; ++it) { // The asserts below are theoretically valid but at the time // of writing they fail because of a bug in QItemSelectionModel::selectedRows() // which returns also non-selectable items. //Q_ASSERT( selectedItem->type() == Item::Message ); //Q_ASSERT( ( *it ).isValid() ); if (!(*it).isValid()) { continue; } Item *selectedItem = static_cast< Item * >((*it).internalPointer()); Q_ASSERT(selectedItem); if (selectedItem->type() != Item::Message) { continue; } if (!static_cast< MessageItem * >(selectedItem)->isValid()) { continue; } Q_ASSERT(!selectedMessages.contains(static_cast< MessageItem * >(selectedItem))); if (includeCollapsedChildren && (selectedItem->childItemCount() > 0) && (!isExpanded(*it))) { static_cast< MessageItem * >(selectedItem)->subTreeToList(selectedMessages); } else { selectedMessages.append(static_cast< MessageItem * >(selectedItem)); } } return selectedMessages; } QList< MessageItem * > View::currentThreadAsMessageItemList() const { QList< MessageItem * > currentThread; MessageItem *msg = currentMessageItem(); if (!msg) { return currentThread; } while (msg->parent()) { if (msg->parent()->type() != Item::Message) { break; } msg = static_cast< MessageItem * >(msg->parent()); } msg->subTreeToList(currentThread); return currentThread; } void View::setChildrenExpanded(const Item *root, bool expand) { Q_ASSERT(root); QList< Item * > *childList = root->childItems(); if (!childList) { return; } QList< Item * >::ConstIterator end(childList->constEnd()); for (QList< Item * >::ConstIterator it = childList->constBegin(); it != end; ++it) { QModelIndex idx = d->mModel->index(*it, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< Item * >(idx.internalPointer()) == (*it)); if (expand) { setExpanded(idx, true); if ((*it)->childItemCount() > 0) { setChildrenExpanded(*it, true); } } else { if ((*it)->childItemCount() > 0) { setChildrenExpanded(*it, false); } setExpanded(idx, false); } } } void View::Private::expandFullThread(const QModelIndex &index) { if (! index.isValid()) { return; } Item *item = static_cast< Item * >(index.internalPointer()); if (item->type() != Item::Message) { return; } if (! static_cast< MessageItem * >(item)->parent() || (static_cast< MessageItem * >(item)->parent()->type() != Item::Message)) { q->setChildrenExpanded(item, true); } } void View::setCurrentThreadExpanded(bool expand) { Item *it = currentItem(); if (!it) { return; } if (it->type() == Item::GroupHeader) { setExpanded(currentIndex(), expand); } else if (it->type() == Item::Message) { MessageItem *message = static_cast< MessageItem *>(it); while (message->parent()) { if (message->parent()->type() != Item::Message) { break; } message = static_cast< MessageItem * >(message->parent()); } if (expand) { setExpanded(d->mModel->index(message, 0), true); setChildrenExpanded(message, true); } else { setChildrenExpanded(message, false); setExpanded(d->mModel->index(message, 0), false); } } } void View::setAllThreadsExpanded(bool expand) { if (d->mAggregation->grouping() == Aggregation::NoGrouping) { // we have no groups so threads start under the root item: just expand/unexpand all setChildrenExpanded(d->mModel->rootItem(), expand); return; } // grouping is in effect: must expand/unexpand one level lower QList< Item * > *childList = d->mModel->rootItem()->childItems(); if (!childList) { return; } foreach (Item *item, *childList) { setChildrenExpanded(item, expand); } } void View::setAllGroupsExpanded(bool expand) { if (d->mAggregation->grouping() == Aggregation::NoGrouping) { return; // no grouping in effect } Item *item = d->mModel->rootItem(); QList< Item * > *childList = item->childItems(); if (!childList) { return; } foreach (Item *item, *childList) { Q_ASSERT(item->type() == Item::GroupHeader); QModelIndex idx = d->mModel->index(item, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< Item * >(idx.internalPointer()) == item); if (expand) { if (!isExpanded(idx)) { setExpanded(idx, true); } } else { if (isExpanded(idx)) { setExpanded(idx, false); } } } } void View::selectMessageItems(const QList< MessageItem * > &list) { QItemSelection selection; QList< MessageItem * >::ConstIterator end(list.constEnd()); for (QList< MessageItem * >::ConstIterator it = list.constBegin(); it != end; ++it) { Q_ASSERT(*it); QModelIndex idx = d->mModel->index(*it, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< MessageItem * >(idx.internalPointer()) == (*it)); if (!selectionModel()->isSelected(idx)) { selection.append(QItemSelectionRange(idx)); } ensureDisplayedWithParentsExpanded(*it); } if (!selection.isEmpty()) { selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); } } static inline bool message_type_matches(Item *item, MessageTypeFilter messageTypeFilter) { switch (messageTypeFilter) { case MessageTypeAny: return true; break; case MessageTypeUnreadOnly: return !item->status().isRead(); break; default: // nothing here break; } // never reached Q_ASSERT(false); return false; } Item *View::messageItemAfter(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop) { if (!storageModel()) { return Q_NULLPTR; // no folder } // find the item to start with Item *below; if (referenceItem) { // there was a current item: we start just below it if ( (referenceItem->childItemCount() > 0) && ( (messageTypeFilter != MessageTypeAny) || isExpanded(d->mModel->index(referenceItem, 0)) ) ) { // the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't) below = referenceItem->itemBelow(); } else { // the current item had no children: ask the parent to find the item below Q_ASSERT(referenceItem->parent()); below = referenceItem->parent()->itemBelowChild(referenceItem); } if (!below) { // reached the end if (loop) { // try re-starting from top below = d->mModel->rootItem()->itemBelow(); Q_ASSERT(below); // must exist (we had a current item) if (below == referenceItem) { return Q_NULLPTR; // only one item in folder: loop complete } } else { // looping not requested return Q_NULLPTR; } } } else { // there was no current item, start from beginning below = d->mModel->rootItem()->itemBelow(); if (!below) { return Q_NULLPTR; // folder empty } } // ok.. now below points to the next message. // While it doesn't satisfy our requirements, go further down QModelIndex parentIndex = d->mModel->index(below->parent(), 0); QModelIndex belowIndex = d->mModel->index(below, 0); Q_ASSERT(belowIndex.isValid()); while ( // is not a message (we want messages, don't we ?) (below->type() != Item::Message) || // message filter doesn't match (!message_type_matches(below, messageTypeFilter)) || // is hidden (and we don't want hidden items as they arent "officially" in the view) isRowHidden(belowIndex.row(), parentIndex) || // is not enabled or not selectable ((d->mModel->flags(belowIndex) & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) != (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) ) { // find the next one if ((below->childItemCount() > 0) && ((messageTypeFilter != MessageTypeAny) || isExpanded(belowIndex))) { // the current item had children: either expanded or we want unread messages (and so we'll expand it if it isn't) below = below->itemBelow(); } else { // the current item had no children: ask the parent to find the item below Q_ASSERT(below->parent()); below = below->parent()->itemBelowChild(below); } if (!below) { // we reached the end of the folder if (loop) { // looping requested if (referenceItem) { // <-- this means "we have started from something that is not the top: looping makes sense" below = d->mModel->rootItem()->itemBelow(); } // else mi == 0 and below == 0: we have started from the beginning and reached the end (it will fail the test below and exit) } else { // looping not requested: nothing more to do return Q_NULLPTR; } } if (below == referenceItem) { Q_ASSERT(loop); return Q_NULLPTR; // looped and returned back to the first message } parentIndex = d->mModel->index(below->parent(), 0); belowIndex = d->mModel->index(below, 0); Q_ASSERT(belowIndex.isValid()); } return below; } Item *View::nextMessageItem(MessageTypeFilter messageTypeFilter, bool loop) { return messageItemAfter(currentMessageItem(false), messageTypeFilter, loop); } Item *View::deepestExpandedChild(Item *referenceItem) const { const int children = referenceItem->childItemCount(); if (children > 0 && isExpanded(d->mModel->index(referenceItem, 0))) { return deepestExpandedChild(referenceItem->childItem(children - 1)); } else { return referenceItem; } } Item *View::messageItemBefore(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop) { if (!storageModel()) { return Q_NULLPTR; // no folder } // find the item to start with Item *above; if (referenceItem) { Item *parent = referenceItem->parent(); Item *siblingAbove = parent ? parent->itemAboveChild(referenceItem) : Q_NULLPTR; // there was a current item: we start just above it if ((siblingAbove && siblingAbove != referenceItem && siblingAbove != parent) && (siblingAbove->childItemCount() > 0) && ( (messageTypeFilter != MessageTypeAny) || (isExpanded(d->mModel->index(siblingAbove, 0))) ) ) { // the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't) above = deepestExpandedChild(siblingAbove); } else { // the current item had no children: ask the parent to find the item above Q_ASSERT(referenceItem->parent()); above = referenceItem->parent()->itemAboveChild(referenceItem); } if ((!above) || (above == d->mModel->rootItem())) { // reached the beginning if (loop) { // try re-starting from bottom above = d->mModel->rootItem()->deepestItem(); Q_ASSERT(above); // must exist (we had a current item) Q_ASSERT(above != d->mModel->rootItem()); if (above == referenceItem) { return Q_NULLPTR; // only one item in folder: loop complete } } else { // looping not requested return Q_NULLPTR; } } } else { // there was no current item, start from end above = d->mModel->rootItem()->deepestItem(); if (!above || !above->parent() || (above == d->mModel->rootItem())) { return Q_NULLPTR; // folder empty } } // ok.. now below points to the previous message. // While it doesn't satisfy our requirements, go further up QModelIndex parentIndex = d->mModel->index(above->parent(), 0); QModelIndex aboveIndex = d->mModel->index(above, 0); Q_ASSERT(aboveIndex.isValid()); while ( // is not a message (we want messages, don't we ?) (above->type() != Item::Message) || // message filter doesn't match (!message_type_matches(above, messageTypeFilter)) || // we don't expand items but the item has parents unexpanded (so should be skipped) ( // !expand items (messageTypeFilter == MessageTypeAny) && // has unexpanded parents or is itself hidden (! isDisplayedWithParentsExpanded(above)) ) || // is hidden isRowHidden(aboveIndex.row(), parentIndex) || // is not enabled or not selectable ((d->mModel->flags(aboveIndex) & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) != (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) ) { above = above->itemAbove(); if ((!above) || (above == d->mModel->rootItem())) { // reached the beginning if (loop) { // looping requested if (referenceItem) { // <-- this means "we have started from something that is not the beginning: looping makes sense" above = d->mModel->rootItem()->deepestItem(); } // else mi == 0 and above == 0: we have started from the end and reached the beginning (it will fail the test below and exit) } else { // looping not requested: nothing more to do return Q_NULLPTR; } } if (above == referenceItem) { Q_ASSERT(loop); return Q_NULLPTR; // looped and returned back to the first message } if (!above->parent()) { return Q_NULLPTR; } parentIndex = d->mModel->index(above->parent(), 0); aboveIndex = d->mModel->index(above, 0); Q_ASSERT(aboveIndex.isValid()); } return above; } Item *View::previousMessageItem(MessageTypeFilter messageTypeFilter, bool loop) { return messageItemBefore(currentMessageItem(false), messageTypeFilter, loop); } void View::growOrShrinkExistingSelection(const QModelIndex &newSelectedIndex, bool movingUp) { // Qt: why visualIndex() is private? ...I'd really need it here... int selectedVisualCoordinate = visualRect(newSelectedIndex).top(); int topVisualCoordinate = 0xfffffff; // huuuuuge number int bottomVisualCoordinate = -(0xfffffff); int candidate; QModelIndex bottomIndex; QModelIndex topIndex; // find out the actual selection range const QItemSelection selection = selectionModel()->selection(); foreach (const QItemSelectionRange &range, selection) { // We're asking the model for the index as range.topLeft() and range.bottomRight() // can return indexes in invisible columns which have a null visualRect(). // Column 0, instead, is always visible. QModelIndex top = d->mModel->index(range.top(), 0, range.parent()); QModelIndex bottom = d->mModel->index(range.bottom(), 0, range.parent()); if (top.isValid()) { if (!bottom.isValid()) { bottom = top; } } else { if (!top.isValid()) { top = bottom; } } candidate = visualRect(bottom).bottom(); if (candidate > bottomVisualCoordinate) { bottomVisualCoordinate = candidate; bottomIndex = range.bottomRight(); } candidate = visualRect(top).top(); if (candidate < topVisualCoordinate) { topVisualCoordinate = candidate; topIndex = range.topLeft(); } } if (topIndex.isValid() && bottomIndex.isValid()) { if (movingUp) { if (selectedVisualCoordinate < topVisualCoordinate) { // selecting something above the top: grow selection selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); } else { // selecting something below the top: shrink selection QModelIndexList selectedIndexes = selection.indexes(); foreach (const QModelIndex &idx, selectedIndexes) { if ((idx.column() == 0) && (visualRect(idx).top() > selectedVisualCoordinate)) { selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect); } } } } else { if (selectedVisualCoordinate > bottomVisualCoordinate) { // selecting something below bottom: grow selection selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); } else { // selecting something above bottom: shrink selection QModelIndexList selectedIndexes = selection.indexes(); foreach (const QModelIndex &idx, selectedIndexes) { if ((idx.column() == 0) && (visualRect(idx).top() < selectedVisualCoordinate)) { selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect); } } } } } else { // no existing selection, just grow selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); } } bool View::selectNextMessageItem( MessageTypeFilter messageTypeFilter, ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop ) { Item *it = nextMessageItem(messageTypeFilter, loop); if (!it) { return false; } setFocus(); if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); switch (existingSelectionBehaviour) { case ExpandExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Select); break; case GrowOrShrinkExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); growOrShrinkExistingSelection(idx, false); break; default: //case ClearExistingSelection: setCurrentIndex(idx); break; } if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::selectPreviousMessageItem( MessageTypeFilter messageTypeFilter, ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop ) { Item *it = previousMessageItem(messageTypeFilter, loop); if (!it) { return false; } setFocus(); if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); switch (existingSelectionBehaviour) { case ExpandExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Select); break; case GrowOrShrinkExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); growOrShrinkExistingSelection(idx, true); break; default: //case ClearExistingSelection: setCurrentIndex(idx); break; } if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::focusNextMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop) { Item *it = nextMessageItem(messageTypeFilter, loop); if (!it) { return false; } setFocus(); if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::focusPreviousMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop) { Item *it = previousMessageItem(messageTypeFilter, loop); if (!it) { return false; } setFocus(); if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } void View::selectFocusedMessageItem(bool centerItem) { QModelIndex idx = currentIndex(); if (!idx.isValid()) { return; } setFocus(); if (selectionModel()->isSelected(idx)) { return; } selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } } bool View::selectFirstMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem) { if (!storageModel()) { return false; // nothing to do } Item *it = firstMessageItem(messageTypeFilter); if (!it) { return false; } Q_ASSERT(it != d->mModel->rootItem()); // must never happen (obviously) setFocus(); ensureDisplayedWithParentsExpanded(it); QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); setCurrentIndex(idx); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::selectLastMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem) { if (!storageModel()) { return false; } Item *it = lastMessageItem(messageTypeFilter); if (!it) { return false; } Q_ASSERT(it != d->mModel->rootItem()); setFocus(); ensureDisplayedWithParentsExpanded(it); QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); setCurrentIndex(idx); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } void View::modelFinishedLoading() { Q_ASSERT(storageModel()); Q_ASSERT(!d->mModel->isLoading()); // nothing here for now :) } MessageItemSetReference View::createPersistentSet(const QList< MessageItem * > &items) { return d->mModel->createPersistentSet(items); } QList< MessageItem * > View::persistentSetCurrentMessageItemList(MessageItemSetReference ref) { return d->mModel->persistentSetCurrentMessageItemList(ref); } void View::deletePersistentSet(MessageItemSetReference ref) { d->mModel->deletePersistentSet(ref); } void View::markMessageItemsAsAboutToBeRemoved(QList< MessageItem * > &items, bool bMark) { if (!bMark) { QList< MessageItem * >::ConstIterator end(items.constEnd()); for (QList< MessageItem * >::ConstIterator it = items.constBegin(); it != end; ++it) { if ((*it)->isValid()) { // hasn't been removed in the meantime (*it)->setAboutToBeRemoved(false); } } viewport()->update(); return; } // ok.. we're going to mark the messages as "about to be deleted". // This means that we're going to make them non selectable. // What happens to the selection is generally an untrackable big mess. // Several components and entities are involved. // Qutie tries to apply some kind of internal logic in order to keep // "something" selected and "something" (else) to be current. // The results sometimes appear to depend on the current moon phase. // The Model will do crazy things in order to preserve the current // selection (and possibly the current item). If it's impossible then // it will make its own guesses about what should be selected next. // A problem is that the Model will do it one message at a time. // When item reparenting/reordering is involved then the guesses // can produce non-intuitive results. // Add the fact that selection and current item are distinct concepts, // their relative interaction depends on the settings and is often quite // unclear. // Add the fact that (at the time of writing) several styles don't show // the current item (only Yoda knows why) and this causes some confusion to the user. // Add the fact that the operations are asynchronous: deletion will start // a job, do some event loop processing and then complete the work at a later time. // The Qutie views also tend to accumulate the changes and perform them // all at once at the latest possible stage. // A radical approach is needed: we FIRST deal with the selection // by tring to move it away from the messages about to be deleted // and THEN mark the (hopefully no longer selected) messages as "about to be deleted". // First of all, find out if we're going to clear the entire selection (very likely). bool clearingEntireSelection = true; QModelIndexList selectedIndexes = selectionModel()->selectedRows(0); if (selectedIndexes.count() > items.count()) { // the selection is bigger: we can't clear it completely clearingEntireSelection = false; } else { // the selection has same size or is smaller: we can clear it completely with our removal foreach (const QModelIndex &selectedIndex, selectedIndexes) { Q_ASSERT(selectedIndex.isValid()); Q_ASSERT(selectedIndex.column() == 0); Item *selectedItem = static_cast< Item * >(selectedIndex.internalPointer()); Q_ASSERT(selectedItem); if (selectedItem->type() != Item::Message) { continue; } if (!items.contains(static_cast< MessageItem * >(selectedItem))) { // the selection contains something that we aren't going to remove: // we will not clear the selection completely clearingEntireSelection = false; break; } } } if (clearingEntireSelection) { // Try to clear the current selection and select something sensible instead, // so after the deletion we will not end up with a random selection. // Pick up a message in the set (which is very likely to be contiguous), walk the tree // and select the next message that is NOT in the set. MessageItem *aMessage = items.last(); Q_ASSERT(aMessage); // Avoid infinite loops by carrying only a limited number of attempts. // If there is any message that is not in the set then items.count() attemps should find it. int maxAttempts = items.count(); while (items.contains(aMessage) && (maxAttempts > 0)) { Item *next = messageItemAfter(aMessage, MessageTypeAny, false); if (!next) { // no way aMessage = Q_NULLPTR; break; } Q_ASSERT(next->type() == Item::Message); aMessage = static_cast< MessageItem * >(next); maxAttempts--; } if (!aMessage) { // try backwards aMessage = items.first(); Q_ASSERT(aMessage); maxAttempts = items.count(); while (items.contains(aMessage) && (maxAttempts > 0)) { Item *prev = messageItemBefore(aMessage, MessageTypeAny, false); if (!prev) { // no way aMessage = Q_NULLPTR; break; } Q_ASSERT(prev->type() == Item::Message); aMessage = static_cast< MessageItem * >(prev); maxAttempts--; } } if (aMessage) { QModelIndex aMessageIndex = d->mModel->index(aMessage, 0); Q_ASSERT(aMessageIndex.isValid()); Q_ASSERT(static_cast< MessageItem * >(aMessageIndex.internalPointer()) == aMessage); Q_ASSERT(!selectionModel()->isSelected(aMessageIndex)); setCurrentIndex(aMessageIndex); selectionModel()->select(aMessageIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } } // else we aren't clearing the entire selection so something should just stay selected. // Now mark messages as about to be removed. QList< MessageItem * >::ConstIterator end(items.constEnd()); for (QList< MessageItem * >::ConstIterator it = items.constBegin(); it != end; ++it) { (*it)->setAboutToBeRemoved(true); QModelIndex idx = d->mModel->index(*it, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< MessageItem * >(idx.internalPointer()) == *it); if (selectionModel()->isSelected(idx)) { selectionModel()->select(idx, QItemSelectionModel::Deselect | QItemSelectionModel::Rows); } } viewport()->update(); } void View::ensureDisplayedWithParentsExpanded(Item *it) { Q_ASSERT(it); Q_ASSERT(it->parent()); Q_ASSERT(it->isViewable()); // must be attached to the viewable root if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) { setRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0), false); } it = it->parent(); while (it->parent()) { if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) { setRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0), false); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< Item * >(idx.internalPointer()) == it); if (!isExpanded(idx)) { setExpanded(idx, true); } it = it->parent(); } } bool View::isDisplayedWithParentsExpanded(Item *it) const { // An item is currently viewable iff // - it is marked as viewable in the item structure (that is, qt knows about its existence) // (and this means that all of its parents are marked as viewable) // - it is not explicitly hidden // - all of its parents are expanded if (!it) { return false; // be nice and allow the caller not to care } if (!it->isViewable()) { return false; // item not viewable (not attached to the viewable root or qt not yet aware of it) } // the item and all the parents are marked as viewable. if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) { return false; // item qt representation explicitly hidden } // the item (and theoretically all the parents) are not explicitly hidden // check the parent chain it = it->parent(); while (it) { if (it == d->mModel->rootItem()) { return true; // parent is root item: ok } // parent is not root item if (!isExpanded(d->mModel->index(it, 0))) { return false; // parent is not expanded (so child not actually visible) } it = it->parent(); // climb up } // parent hierarchy interrupted somewhere return false; } bool View::isThreaded() const { if (!d->mAggregation) { return false; } return d->mAggregation->threading() != Aggregation::NoThreading; } void View::slotSelectionChanged(const QItemSelection &, const QItemSelection &) { // We assume that when selection changes, current item also changes. QModelIndex current = currentIndex(); if (!current.isValid()) { if (d->mLastCurrentItem) { d->mWidget->viewMessageSelected(Q_NULLPTR); d->mLastCurrentItem = Q_NULLPTR; } d->mWidget->viewMessageSelected(Q_NULLPTR); d->mWidget->viewSelectionChanged(); return; } if (!selectionModel()->isSelected(current)) { if (selectedIndexes().count() < 1) { // It may happen after row removals: Model calls this slot on currentIndex() // that actually might have changed "silently", without being selected. QItemSelection selection; selection.append(QItemSelectionRange(current)); selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); return; // the above recurses } else { // something is still selected anyway // This is probably a result of CTRL+Click which unselected current: leave it as it is. return; } } Item *it = static_cast< Item * >(current.internalPointer()); Q_ASSERT(it); switch (it->type()) { case Item::Message: { if (d->mLastCurrentItem != it) { qCDebug(MESSAGELIST_LOG) << "View message selected [" << static_cast< MessageItem * >(it)->subject() << "]"; d->mWidget->viewMessageSelected(static_cast< MessageItem * >(it)); d->mLastCurrentItem = it; } } break; case Item::GroupHeader: if (d->mLastCurrentItem) { d->mWidget->viewMessageSelected(Q_NULLPTR); d->mLastCurrentItem = Q_NULLPTR; } break; default: // should never happen Q_ASSERT(false); break; } d->mWidget->viewSelectionChanged(); } void View::mouseDoubleClickEvent(QMouseEvent *e) { // Perform a hit test if (!d->mDelegate->hitTest(e->pos(), true)) { return; } // Something was hit :) Item *it = static_cast< Item * >(d->mDelegate->hitItem()); if (!it) { return; // should never happen } switch (it->type()) { case Item::Message: { // Let QTreeView handle the expansion QTreeView::mousePressEvent(e); switch (e->button()) { case Qt::LeftButton: if (d->mDelegate->hitContentItem()) { // Double clikcking on clickable icons does NOT activate the message if (d->mDelegate->hitContentItem()->isIcon() && d->mDelegate->hitContentItem()->isClickable()) { return; } } d->mWidget->viewMessageActivated(static_cast< MessageItem * >(it)); break; default: // make gcc happy break; } } break; case Item::GroupHeader: { // Don't let QTreeView handle the selection (as it deselects the curent messages) switch (e->button()) { case Qt::LeftButton: if (it->childItemCount() > 0) { // toggle expanded state setExpanded(d->mDelegate->hitIndex(), !isExpanded(d->mDelegate->hitIndex())); } break; default: // make gcc happy break; } } break; default: // should never happen Q_ASSERT(false); break; } } void View::changeMessageStatusRead(MessageItem *it, bool read) { Akonadi::MessageStatus set = it->status(); Akonadi::MessageStatus unset = it->status(); if (read) { set.setRead(true); unset.setRead(false); } else { set.setRead(false); unset.setRead(true); } viewport()->update(); // This will actually request the widget to perform a status change on the storage. // The request will be then processed by the Model and the message will be updated again. d->mWidget->viewMessageStatusChangeRequest(it, set, unset); } void View::changeMessageStatus(MessageItem *it, Akonadi::MessageStatus set, Akonadi::MessageStatus unset) { // We first change the status of MessageItem itself. This will make the change // visible to the user even if the Model is actually in the middle of a long job (maybe it's loading) // and can't process the status change request immediately. // Here we actually desynchronize the cache and trust that the later call to // d->mWidget->viewMessageStatusChangeRequest() will really perform the status change on the storage. // Well... in KMail it will unless something is really screwed. Anyway, if it will not, at the next // load the status will be just unchanged: no animals will be harmed. qint32 stat = it->status().toQInt32(); stat |= set.toQInt32(); stat &= ~(unset.toQInt32()); Akonadi::MessageStatus status; status.fromQInt32(stat); it->setStatus(status); // Trigger an update so the immediate change will be shown to the user viewport()->update(); // This will actually request the widget to perform a status change on the storage. // The request will be then processed by the Model and the message will be updated again. d->mWidget->viewMessageStatusChangeRequest(it, set, unset); } void View::mousePressEvent(QMouseEvent *e) { d->mMousePressPosition = QPoint(); // Perform a hit test if (!d->mDelegate->hitTest(e->pos(), true)) { return; } // Something was hit :) Item *it = static_cast< Item * >(d->mDelegate->hitItem()); if (!it) { return; // should never happen } // Abort any pending message pre-selection as the user is probably // already navigating the view (so pre-selection would make his view jump // to an unexpected place). d->mModel->setPreSelectionMode(PreSelectNone); switch (it->type()) { case Item::Message: { d->mMousePressPosition = e->pos(); switch (e->button()) { case Qt::LeftButton: // if we have multi selection then the meaning of hitting // the content item is quite unclear. if (d->mDelegate->hitContentItem() && (selectedIndexes().count() > 1)) { qCDebug(MESSAGELIST_LOG) << "Left hit with selectedIndexes().count() == " << selectedIndexes().count(); switch (d->mDelegate->hitContentItem()->type()) { case Theme::ContentItem::AnnotationIcon: static_cast< MessageItem * >(it)->editAnnotation(); return; // don't select the item break; case Theme::ContentItem::ActionItemStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isToAct() ? Akonadi::MessageStatus() : Akonadi::MessageStatus::statusToAct(), it->status().isToAct() ? Akonadi::MessageStatus::statusToAct() : Akonadi::MessageStatus() ); return; // don't select the item break; case Theme::ContentItem::ImportantStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isImportant() ? Akonadi::MessageStatus() : Akonadi::MessageStatus::statusImportant(), it->status().isImportant() ? Akonadi::MessageStatus::statusImportant() : Akonadi::MessageStatus() ); return; // don't select the item case Theme::ContentItem::ReadStateIcon: changeMessageStatusRead(static_cast< MessageItem * >(it), it->status().isRead() ? false : true); return; break; case Theme::ContentItem::SpamHamStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isSpam() ? Akonadi::MessageStatus() : (it->status().isHam() ? Akonadi::MessageStatus::statusSpam() : Akonadi::MessageStatus::statusHam()), it->status().isSpam() ? Akonadi::MessageStatus::statusSpam() : (it->status().isHam() ? Akonadi::MessageStatus::statusHam() : Akonadi::MessageStatus()) ); return; // don't select the item break; case Theme::ContentItem::WatchedIgnoredStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isIgnored() ? Akonadi::MessageStatus() : (it->status().isWatched() ? Akonadi::MessageStatus::statusIgnored() : Akonadi::MessageStatus::statusWatched()), it->status().isIgnored() ? Akonadi::MessageStatus::statusIgnored() : (it->status().isWatched() ? Akonadi::MessageStatus::statusWatched() : Akonadi::MessageStatus()) ); return; // don't select the item break; default: // make gcc happy break; } } // Let QTreeView handle the selection and Q_EMIT the appropriate signals (slotSelectionChanged() may be called) QTreeView::mousePressEvent(e); break; case Qt::RightButton: // Let QTreeView handle the selection and Q_EMIT the appropriate signals (slotSelectionChanged() may be called) QTreeView::mousePressEvent(e); e->accept(); d->mWidget->viewMessageListContextPopupRequest(selectionAsMessageItemList(), viewport()->mapToGlobal(e->pos())); break; default: // make gcc happy break; } } break; case Item::GroupHeader: { // Don't let QTreeView handle the selection (as it deselects the curent messages) GroupHeaderItem *groupHeaderItem = static_cast< GroupHeaderItem * >(it); switch (e->button()) { case Qt::LeftButton: { QModelIndex index = d->mModel->index(groupHeaderItem, 0); if (index.isValid()) { setCurrentIndex(index); } if (!d->mDelegate->hitContentItem()) { return; } if (d->mDelegate->hitContentItem()->type() == Theme::ContentItem::ExpandedStateIcon) { if (groupHeaderItem->childItemCount() > 0) { // toggle expanded state setExpanded(d->mDelegate->hitIndex(), !isExpanded(d->mDelegate->hitIndex())); } } } break; case Qt::RightButton: d->mWidget->viewGroupHeaderContextPopupRequest(groupHeaderItem, viewport()->mapToGlobal(e->pos())); break; default: // make gcc happy break; } } break; default: // should never happen Q_ASSERT(false); break; } } void View::mouseMoveEvent(QMouseEvent *e) { if (!(e->buttons() & Qt::LeftButton)) { QTreeView::mouseMoveEvent(e); return; } if (d->mMousePressPosition.isNull()) { return; } if ((e->pos() - d->mMousePressPosition).manhattanLength() <= QApplication::startDragDistance()) { return; } d->mWidget->viewStartDragRequest(); } #if 0 void View::contextMenuEvent(QContextMenuEvent *e) { Q_UNUSED(e); QModelIndex index = currentIndex(); if (index.isValid()) { QRect indexRect = this->visualRect(index); QPoint pos; if ((indexRect.isValid()) && (indexRect.bottom() > 0)) { if (indexRect.bottom() > viewport()->height()) { if (indexRect.top() <= viewport()->height()) { pos = indexRect.topLeft(); } } else { pos = indexRect.bottomLeft(); } } Item *item = static_cast< Item * >(index.internalPointer()); if (item) { if (item->type() == Item::GroupHeader) { d->mWidget->viewGroupHeaderContextPopupRequest(static_cast< GroupHeaderItem * >(item), viewport()->mapToGlobal(pos)); } else if (!selectionEmpty()) { d->mWidget->viewMessageListContextPopupRequest(selectionAsMessageItemList(), viewport()->mapToGlobal(pos)); e->accept(); } } } } #endif void View::dragEnterEvent(QDragEnterEvent *e) { d->mWidget->viewDragEnterEvent(e); } void View::dragMoveEvent(QDragMoveEvent *e) { d->mWidget->viewDragMoveEvent(e); } void View::dropEvent(QDropEvent *e) { d->mWidget->viewDropEvent(e); } void View::changeEvent(QEvent *e) { switch (e->type()) { case QEvent::FontChange: d->mDelegate->generalFontChanged(); case QEvent::PaletteChange: case QEvent::StyleChange: case QEvent::LayoutDirectionChange: case QEvent::LocaleChange: case QEvent::LanguageChange: // All of these affect the theme's internal cache. setTheme(d->mTheme); // A layoutChanged() event will screw up the view state a bit. // Since this is a rare event we just reload the view. reload(); break; default: // make gcc happy by default break; } QTreeView::changeEvent(e); } bool View::event(QEvent *e) { // We catch ToolTip events and pass everything else if (e->type() != QEvent::ToolTip) { return QTreeView::event(e); } if (!MessageListSettings::self()->messageToolTipEnabled()) { return true; // don't display tooltips } QHelpEvent *he = dynamic_cast< QHelpEvent * >(e); if (!he) { return true; // eh ? } QPoint pnt = viewport()->mapFromGlobal(mapToGlobal(he->pos())); if (pnt.y() < 0) { return true; // don't display the tooltip for items hidden under the header } QModelIndex idx = indexAt(pnt); if (!idx.isValid()) { return true; // may be } Item *it = static_cast< Item * >(idx.internalPointer()); if (!it) { return true; // hum } Q_ASSERT(storageModel()); QColor bckColor = palette().color(QPalette::ToolTipBase); QColor txtColor = palette().color(QPalette::ToolTipText); QColor darkerColor( ((bckColor.red() * 8) + (txtColor.red() * 2)) / 10, ((bckColor.green() * 8) + (txtColor.green() * 2)) / 10, ((bckColor.blue() * 8) + (txtColor.blue() * 2)) / 10 ); QString bckColorName = bckColor.name(); QString txtColorName = txtColor.name(); QString darkerColorName = darkerColor.name(); const bool textIsLeftToRight = (QApplication::layoutDirection() == Qt::LeftToRight); const QString textDirection = textIsLeftToRight ? QStringLiteral("left") : QStringLiteral("right"); QString tip = QStringLiteral( "" ); switch (it->type()) { case Item::Message: { MessageItem *mi = static_cast< MessageItem * >(it); tip += QStringLiteral( "" \ "" \ "" ).arg(txtColorName).arg(bckColorName).arg(mi->subject().toHtmlEscaped()).arg(textDirection); tip += QLatin1String( "" \ "
" \ "
" \ "%3" \ "
" \ "
" \ "" ); const QString htmlCodeForStandardRow = QStringLiteral( "" \ "" \ "" \ ""); if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("From")).arg(MessageCore::StringUtil::stripEmailAddr(mi->sender())); tip += htmlCodeForStandardRow.arg(i18nc("Receiver of the email", "To")).arg(MessageCore::StringUtil::stripEmailAddr(mi->receiver())); tip += htmlCodeForStandardRow.arg(i18n("Date")).arg(mi->formattedDate()); } else { tip += htmlCodeForStandardRow.arg(MessageCore::StringUtil::stripEmailAddr(mi->sender())).arg(i18n("From")); tip += htmlCodeForStandardRow.arg(MessageCore::StringUtil::stripEmailAddr(mi->receiver())).arg(i18nc("Receiver of the email", "To")); tip += htmlCodeForStandardRow.arg(mi->formattedDate(), i18n("Date")); } QString status = mi->statusDescription(); const QString tags = mi->tagListDescription(); if (!tags.isEmpty()) { if (!status.isEmpty()) { status += QLatin1String(", "); } status += tags; } if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("Status")).arg(status); tip += htmlCodeForStandardRow.arg(i18n("Size")).arg(mi->formattedSize()); } else { tip += htmlCodeForStandardRow.arg(status).arg(i18n("Status")); tip += htmlCodeForStandardRow.arg(mi->formattedSize()).arg(i18n("Size")); } if (mi->hasAnnotation()) { if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("Note"), mi->annotation().replace(QLatin1Char('\n'), QStringLiteral("
"))); } else { tip += htmlCodeForStandardRow.arg(mi->annotation().replace(QLatin1Char('\n'), QStringLiteral("
"))).arg(i18n("Note")); } } QString content = MessageList::Util::contentSummary(mi->akonadiItem()); if (!content.trimmed().isEmpty()) { if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("Preview"), content.replace(QLatin1Char('\n'), QStringLiteral("
"))); } else { tip += htmlCodeForStandardRow.arg(content.replace(QLatin1Char('\n'), QStringLiteral("
"))).arg(i18n("Preview")); } } tip += QLatin1String( "" \ "" ); // FIXME: Find a way to show also CC and other header fields ? if (mi->hasChildren()) { Item::ChildItemStats stats; mi->childItemStats(stats); QString statsText; statsText = i18np("%1 reply", "%1 replies", mi->childItemCount()); statsText += QLatin1String(", "); statsText += i18np( "%1 message in subtree (%2 unread)", "%1 messages in subtree (%2 unread)", stats.mTotalChildCount, stats.mUnreadChildCount ); tip += QStringLiteral( "" \ "" \ "" ).arg(darkerColorName).arg(statsText).arg(textDirection); } } break; case Item::GroupHeader: { GroupHeaderItem *ghi = static_cast< GroupHeaderItem * >(it); tip += QStringLiteral( "" \ "" \ "" ).arg(txtColorName).arg(bckColorName).arg(ghi->label()).arg(textDirection); QString description; switch (d->mAggregation->grouping()) { case Aggregation::GroupByDate: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: if (ghi->label().contains(QRegExp(QLatin1String("[0-9]")))) description = i18nc( "@info:tooltip Formats to something like 'Threads started on 2008-12-21'", "Threads started on %1", ghi->label() ); else description = i18nc( "@info:tooltip Formats to something like 'Threads started Yesterday'", "Threads started %1", ghi->label() ); break; case Aggregation::MostRecentMessage: description = i18n("Threads with messages dated %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (ghi->label().contains(QRegExp(QLatin1String("[0-9]")))) { if (storageModel()->containsOutboundMessages()) description = i18nc( "@info:tooltip Formats to something like 'Messages sent on 2008-12-21'", "Messages sent on %1", ghi->label() ); else description = i18nc( "@info:tooltip Formats to something like 'Messages received on 2008-12-21'", "Messages received on %1", ghi->label() ); } else { if (storageModel()->containsOutboundMessages()) description = i18nc( "@info:tooltip Formats to something like 'Messages sent Yesterday'", "Messages sent %1", ghi->label() ); else description = i18nc( "@info:tooltip Formats to something like 'Messages received Yesterday'", "Messages received %1", ghi->label() ); } } break; case Aggregation::GroupByDateRange: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: description = i18n("Threads started within %1", ghi->label()); break; case Aggregation::MostRecentMessage: description = i18n("Threads containing messages with dates within %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (storageModel()->containsOutboundMessages()) { description = i18n("Messages sent within %1", ghi->label()); } else { description = i18n("Messages received within %1", ghi->label()); } } break; case Aggregation::GroupBySenderOrReceiver: case Aggregation::GroupBySender: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: description = i18n("Threads started by %1", ghi->label()); break; case Aggregation::MostRecentMessage: description = i18n("Threads with most recent message by %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (storageModel()->containsOutboundMessages()) { if (d->mAggregation->grouping() == Aggregation::GroupBySenderOrReceiver) { description = i18n("Messages sent to %1", ghi->label()); } else { description = i18n("Messages sent by %1", ghi->label()); } } else { description = i18n("Messages received from %1", ghi->label()); } } break; case Aggregation::GroupByReceiver: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: description = i18n("Threads directed to %1", ghi->label()); break; case Aggregation::MostRecentMessage: description = i18n("Threads with most recent message directed to %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (storageModel()->containsOutboundMessages()) { description = i18n("Messages sent to %1", ghi->label()); } else { description = i18n("Messages received by %1", ghi->label()); } } break; default: // nuthin, make gcc happy break; } if (!description.isEmpty()) { tip += QStringLiteral( "" \ "" \ "" ).arg(description).arg(textDirection); } if (ghi->hasChildren()) { Item::ChildItemStats stats; ghi->childItemStats(stats); QString statsText; if (d->mAggregation->threading() != Aggregation::NoThreading) { statsText = i18np("%1 thread", "%1 threads", ghi->childItemCount()); statsText += QLatin1String(", "); } statsText += i18np( "%1 message (%2 unread)", "%1 messages (%2 unread)", stats.mTotalChildCount, stats.mUnreadChildCount ); tip += QStringLiteral( "" \ "" \ "" ).arg(darkerColorName).arg(statsText).arg(textDirection); } } break; default: // nuthin (just make gcc happy for now) break; } tip += QLatin1String( "
" \ "
" \ "%1:" \ "
" \ "
" \ "%2" \ "
" \ "%2" \ "
" \ "
" \ "%3" \ "
" \ "
" \ "%1" \ "
" \ "%2" \ "
" ); QToolTip::showText(he->globalPos(), tip, viewport(), visualRect(idx)); return true; } void View::slotCollapseAllGroups() { setAllGroupsExpanded(false); } void View::slotExpandAllGroups() { setAllGroupsExpanded(true); } void View::slotCollapseCurrentItem() { setCurrentThreadExpanded(false); } void View::slotExpandCurrentItem() { setCurrentThreadExpanded(true); } void View::focusQuickSearch(const QString &selectedText) { d->mWidget->focusQuickSearch(selectedText); } QList View::currentFilterStatus() const { return d->mWidget->currentFilterStatus(); } -MessageList::Core::QuickSearchLine::SearchOptions View::currentOptions() const -{ - return d->mWidget->currentOptions(); -} - QString View::currentFilterSearchString() const { return d->mWidget->currentFilterSearchString(); } void View::setRowHidden(int row, const QModelIndex &parent, bool hide) { const QModelIndex rowModelIndex = model()->index(row, 0, parent); const Item *const rowItem = static_cast< Item * >(rowModelIndex.internalPointer()); if (rowItem) { const bool currentlyHidden = isRowHidden(row, parent); if (currentlyHidden != hide) { if (currentMessageItem() == rowItem) { selectionModel()->clear(); selectionModel()->clearSelection(); } } } QTreeView::setRowHidden(row, parent, hide); } void View::sortOrderMenuAboutToShow(QMenu *menu) { d->mWidget->sortOrderMenuAboutToShow(menu); } void View::aggregationMenuAboutToShow(QMenu *menu) { d->mWidget->aggregationMenuAboutToShow(menu); } void View::themeMenuAboutToShow(QMenu *menu) { d->mWidget->themeMenuAboutToShow(menu); } void View::setCollapseItem(const QModelIndex &index) { if (index.isValid()) { setExpanded(index, false); } } void View::setExpandItem(const QModelIndex &index) { if (index.isValid()) { setExpanded(index, true); } } void View::setQuickSearchClickMessage(const QString &msg) { d->mWidget->quickSearch()->setPlaceholderText(msg); } #include "moc_view.cpp" diff --git a/messagelist/src/core/view.h b/messagelist/src/core/view.h index fa564cd3ac..2cdd9b6807 100644 --- a/messagelist/src/core/view.h +++ b/messagelist/src/core/view.h @@ -1,698 +1,697 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #ifndef __MESSAGELIST_CORE_VIEW_H__ #define __MESSAGELIST_CORE_VIEW_H__ #include #include #include #include #include class QMenu; namespace Akonadi { class MessageStatus; } namespace MessageList { namespace Core { typedef long int MessageItemSetReference; class Aggregation; class Delegate; class Item; class MessageItem; class Model; class Theme; class SortOrder; class StorageModel; class Widget; /** * The MessageList::View is the real display of the message list. It is * based on QTreeView, has a Model that manipulates the underlying message storage * and a Delegate that is responsable of painting the items. */ class View : public QTreeView { friend class Model; friend class ModelPrivate; Q_OBJECT public: explicit View(Widget *parent); ~View(); /** * Returns the Model attacched to this View. You probably never need to manipulate * it directly. */ Model *model() const; /** * Returns the Delegate attacched to this View. You probably never need to manipulate * it directly. Model uses it to obtain size hints. */ Delegate *delegate() const; /** * Sets the StorageModel to be displayed in this view. The StorageModel may be 0 (so no content is displayed). * Setting the StorageModel will obviously trigger a view reload. * Be sure to set the Aggregation and the Theme BEFORE calling this function. * * Pre-selection is the action of automatically selecting a message just after the folder * has finished loading. See Model::setStorageModel() for more information. */ void setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode = PreSelectLastSelected); /** * Returns the currently displayed StorageModel. May be 0. */ StorageModel *storageModel() const; /** * Sets the aggregation for this view. * Does not trigger a reload of the view: you *MUST* trigger it manually. */ void setAggregation(const Aggregation *aggregation); /** * Sets the specified theme for this view. * Does not trigger a reload of the view: you *MUST* trigger it manually. */ void setTheme(Theme *theme); /** * Sets the specified sort order. * Does not trigger a reload of the view: you *MUST* trigger it manually. */ void setSortOrder(const SortOrder *sortOrder); /** * Triggers a reload of the view in order to re-display the current folder. * Call this function after changing the Aggregation or the Theme. */ void reload(); /** * Returns the current MessageItem (that is bound to current StorageModel). * May return 0 if there is no current message or no current StorageModel. * If the current message item isn't currently selected (so is only focused) * then it's selected when this function is called, unless selectIfNeeded is false. */ MessageItem *currentMessageItem(bool selectIfNeeded = true) const; /** * Returns the current Item (that is bound to current StorageModel). * May return 0 if there is no current item or no current StorageModel. * If the current item isn't currently selected (so is only focused) * then it's selected when this function is called. */ Item *currentItem() const; /** * Sets the current message item. */ void setCurrentMessageItem(MessageItem *it, bool center = false); /** * Returns true if the specified item is currently displayed in the tree * and has all the parents expanded. This means that the user can * see the message (by eventually scrolling the view). */ bool isDisplayedWithParentsExpanded(Item *it) const; /** * Makes sure that the specified is currently viewable by the user. * This means that the user can see the message (by eventually scrolling the view). */ void ensureDisplayedWithParentsExpanded(Item *it); /** * Returns the currently selected MessageItems (bound to current StorageModel). * The list may be empty if there are no selected messages or no StorageModel. * * If includeCollapsedChildren is true then the children of the selected but * collapsed items are also added to the list. * * The returned list is guaranteed to be valid only until you return control * to the main even loop. Don't store it for any longer. If you need to reference * this set of messages at a later stage then take a look at createPersistentSet(). */ QList< MessageItem * > selectionAsMessageItemList(bool includeCollapsedChildren = true) const; /** * Returns the MessageItems bound to the current StorageModel that * are part of the current thread. The current thread is the thread * that contains currentMessageItem(). * The list may be empty if there is no currentMessageItem() or no StorageModel. * * The returned list is guaranteed to be valid only until you return control * to the main even loop. Don't store it for any longer. If you need to reference * this set of messages at a later stage then take a look at createPersistentSet(). */ QList< MessageItem * > currentThreadAsMessageItemList() const; /** * Fast function that determines if the selection is empty */ bool selectionEmpty() const; /** * Selects the specified MessageItems. The current selection is NOT removed. * Use clearSelection() for that purpose. */ void selectMessageItems(const QList< MessageItem * > &list); /** * Creates a persistent set for the specified MessageItems and * returns its reference. Later you can use this reference * to retrieve the list of MessageItems that are still valid. * See persistentSetCurrentMessageList() for that. * * Persistent sets consume resources (both memory and CPU time * while manipulating the view) so be sure to call deletePersistentSet() * when you no longer need it. */ MessageItemSetReference createPersistentSet(const QList< MessageItem * > &items); /** * Returns the list of MessageItems that are still existing in the * set pointed by the specified reference. This list will contain * at most the messages that you have passed to createPersistentSet() * but may contain less (even 0) if these MessageItem object were removed * from the view for some reason. */ QList< MessageItem * > persistentSetCurrentMessageItemList(MessageItemSetReference ref); /** * Deletes the persistent set pointed by the specified reference. * If the set does not exist anymore, nothing happens. */ void deletePersistentSet(MessageItemSetReference ref); /** * If bMark is true this function marks the messages as "about to be removed" * so they appear dimmer and aren't selectable in the view. * If bMark is false then this function clears the "about to be removed" state * for the specified MessageItems. */ void markMessageItemsAsAboutToBeRemoved(QList< MessageItem * > &items, bool bMark); /** * Returns true if the current Aggregation is threaded, false otherwise * (or if there is no current Aggregation). */ bool isThreaded() const; /** * If expand is true then it expands the current thread, otherwise * collapses it. */ void setCurrentThreadExpanded(bool expand); /** * If expand is true then it expands all the threads, otherwise * collapses them. */ void setAllThreadsExpanded(bool expand); /** * If expand is true then it expands all the groups (only the toplevel * group item: inner threads are NOT expanded). If expand is false * then it collapses all the groups. If no grouping is in effect * then this function does nothing. */ void setAllGroupsExpanded(bool expand); /** * Selects the next message item in the view. * * messageTypeFilter can be used to limit the selection to * a certain category of messages. * * existingSelectionBehaviour specifies how the existing selection * is manipulated. It may be cleared, expanded or grown/shrinked. * * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * If loop is true then the "next" algorithm will restart from the beginning * of the list if the end is reached, otherwise it will just stop returning false. * * \sa MessageList::Core::MessageTypeFilter * \sa MessageList::Core::ExistingSelectionBehaviour */ bool selectNextMessageItem( MessageTypeFilter messageTypeFilter, ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop ); /** * Selects the previous message item in the view. * * messageTypeFilter can be used to limit the selection to * a certain category of messages. * * existingSelectionBehaviour specifies how the existing selection * is manipulated. It may be cleared, expanded or grown/shrinked. * * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * If loop is true then the "previous" algorithm will restart from the end * of the list if the beginning is reached, otherwise it will just stop returning false. * * \sa MessageList::Core::MessageTypeFilter * \sa MessageList::Core::ExistingSelectionBehaviour */ bool selectPreviousMessageItem( MessageTypeFilter messageTypeFilter, ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop ); /** * Focuses the next message item in the view without actually selecting it. * * messageTypeFilter can be used to limit the selection to * a certain category of messages. * * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * If loop is true then the "next" algorithm will restart from the beginning * of the list if the end is reached, otherwise it will just stop returning false. */ bool focusNextMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop); /** * Focuses the previous message item in the view without actually selecting it. * If unread is true then focuses the previous unread message item. * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * If loop is true then the "previous" algorithm will restart from the end * of the list if the beginning is reached, otherwise it will just stop returning false. */ bool focusPreviousMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop); /** * Selects the currently focused message item. If the currently focused * message is already selected (which is very likely) nothing happens. * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. */ void selectFocusedMessageItem(bool centerItem); /** * Selects the first message item in the view that matches messageTypeFilter. * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. */ bool selectFirstMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem); /** * Selects the last message item in the view that matches messageTypeFilter. * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. */ bool selectLastMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem); /** * Sets the focus on the quick search line of the currently active tab. */ void focusQuickSearch(const QString &selectedText); /** * Returns the Akonadi::MessageStatus in the current quicksearch field. */ QList currentFilterStatus() const; /** * Returns the search term in the current quicksearch field. */ QString currentFilterSearchString() const; /** * Called to hide or show the specified row from the view. * @reimp */ virtual void setRowHidden(int row, const QModelIndex &parent, bool hide); void sortOrderMenuAboutToShow(QMenu *menu); void aggregationMenuAboutToShow(QMenu *menu); void themeMenuAboutToShow(QMenu *menu); void setCollapseItem(const QModelIndex &index); void setExpandItem(const QModelIndex &index); void setQuickSearchClickMessage(const QString &msg); - MessageList::Core::QuickSearchLine::SearchOptions currentOptions() const; protected: /** * Reimplemented in order to catch QHelpEvent */ bool event(QEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to catch palette, font and style changes */ void changeEvent(QEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to apply theme column widths on the first show */ void showEvent(QShowEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to handle clicks with sub-item precision. */ void mousePressEvent(QMouseEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to handle double clicks with sub-item precision. */ void mouseDoubleClickEvent(QMouseEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to handle DnD */ void mouseMoveEvent(QMouseEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to handle context menu request via keyboard */ //void contextMenuEvent(QContextMenuEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to handle message DnD */ void dragEnterEvent(QDragEnterEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to handle message DnD */ void dragMoveEvent(QDragMoveEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to handle message DnD */ void dropEvent(QDropEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to resize columns when header is not visible */ void resizeEvent(QResizeEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented in order to kill the QTreeView column auto-resizing */ int sizeHintForColumn(int logicalColumnIndex) const Q_DECL_OVERRIDE; /** * Reimplemented in order to disable update of the geometries * while a job step is running (as it takes a very long time and it's called for every item insertion...) * TODO: not true anymore, it's called after a delay. */ void updateGeometries() Q_DECL_OVERRIDE; /** * Returns true if the vertical scrollbar should keep to the top or bottom * while inserting items. */ bool isScrollingLocked() const; /** * Used to enable/disable the ignoring of updateGeometries() calls. */ void ignoreUpdateGeometries(bool ignore); /** * This is called by the model from inside setFolder(). * It's called just after the model has been reset but before * any row has been inserted. This allows us to call updateThemeColumns() as needed. */ void modelHasBeenReset(); void modelAboutToEmitLayoutChanged(); void modelEmittedLayoutChanged(); /** * Recursive helper for currentThreadAsMessageItemList() */ void appendMessageItemChildren(MessageItem *par, QList< MessageItem * > &list); /** * This is called by the model to insulate us from certain QTreeView signals * This is because they may be spurious (caused by Model item rearrangements). */ void ignoreCurrentChanges(bool ignore); /** * Expands or collapses the children of the specified item, recursively. */ void setChildrenExpanded(const Item *parent, bool expand); /** * Finds the next message item with respect to the current item. * If there is no current item then the search starts from the beginning. * Returns 0 if no next message could be found. * * messageTypeFilter can be used to limit the selection to * a certain category of messages. * If loop is true then restarts from the beginning if end is * reached, otherwise it just returns 0 in this case. */ Item *nextMessageItem(MessageTypeFilter messageTypeFilter, bool loop); /** * Finds message item that comes "after" the reference item. * If reference item is 0 then the search starts from the beginning. * Returns 0 if no next message could be found. * * messageTypeFilter can be used to limit the selection to * a certain category of messages. * If loop is true then restarts from the beginning if end is * reached, otherwise it just returns 0 in this case. */ Item *messageItemAfter(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop); /** * Finds the first message item in the view. * * messageTypeFilter can be used to limit the selection to * a certain category of messages. * * Returns 0 if the view is empty. */ Item *firstMessageItem(MessageTypeFilter messageTypeFilter) { return messageItemAfter(Q_NULLPTR, messageTypeFilter, false); } /** * Finds the previous message item with respect to the current item. * If there is no current item then the search starts from the end. * Returns 0 if no previous message could be found. * * messageTypeFilter can be used to limit the selection to * a certain category of messages. * If loop is true then restarts from the end if beginning is * reached, otherwise it just return 0 in this case. */ Item *previousMessageItem(MessageTypeFilter messageTypeFilter, bool loop); /** * Returns the deepest child that is visible (i.e. not in a collapsed tree) of * the specified reference item. */ Item *deepestExpandedChild(Item *referenceItem) const; /** * Finds message item that comes "before" the reference item. * If reference item is 0 then the search starts from the end. * Returns 0 if no next message could be found. * * messageTypeFilter can be used to limit the selection to * a certain category of messages. * If loop is true then restarts from the beginning if end is * reached, otherwise it just returns 0 in this case. */ Item *messageItemBefore(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop); /** * Finds the last message item in the view. * * messageTypeFilter can be used to limit the selection to * a certain category of messages. * * Returns 0 if the view is empty. */ Item *lastMessageItem(MessageTypeFilter messageTypeFilter) { return messageItemBefore(Q_NULLPTR, messageTypeFilter, false); } /** * This is called by Model to signal a start of a lengthy job batch. * Note that this is NOT called for jobs that can be completed in a single step. */ void modelJobBatchStarted(); /** * This is called by Model to signal the end of a lengthy job batch. * Note that this is NOT called for jobs that can be completed in a single step. */ void modelJobBatchTerminated(); /** * This is called by Model to signal that the initial loading stage of a newly * attached StorageModel is terminated. */ void modelFinishedLoading(); /** * Performs a change in the specified MessageItem status. * It first applies the change to the cached state in MessageItem and * then requests our parent widget to act on the storage. */ void changeMessageStatus(MessageItem *it, Akonadi::MessageStatus set, Akonadi::MessageStatus unset); void changeMessageStatusRead(MessageItem *it, bool read); /** * Starts a short-delay timer connected to saveThemeColumnState(). * Used to accumulate consecutive changes and break out of the call stack * up to the main event loop (since in the call stack the column state might be left undefined). */ void triggerDelayedSaveThemeColumnState(); /** * Starts a short-delay timer connected to applyThemeColumns(). * Used to accumulate consecutive changes and break out of the call stack * up to the main event loop (since multiple resize events tend to be sent by Qt at startup). */ void triggerDelayedApplyThemeColumns(); /** * This is used by the selection functions to grow/shrink the existing selection * according to the newly selected item passed as parameter. * If movingUp is true then: if the newly selected item is above the current selection top * then the selection is expanded, otherwise it's shrunk. If movingUp is false then: if the * newly selected item is below the current selection bottom then the selection is expanded * otherwise it's shrunk. */ void growOrShrinkExistingSelection(const QModelIndex &newSelectedIndex, bool movingUp); public Q_SLOTS: /** * Collapses all the group headers (if present in the current Aggregation) */ void slotCollapseAllGroups(); /** * Expands all the group headers (if present in the current Aggregation) */ void slotExpandAllGroups(); /** * Expands the currect item. * If it's a Message, it expands its thread, if its a group header it expands the group */ void slotExpandCurrentItem(); /** * Collapses the currect item. * If it's a Message, it collapses its thread, if its a group header it collapses the group */ void slotCollapseCurrentItem(); protected Q_SLOTS: /** * Handles context menu requests for the header. */ void slotHeaderContextMenuRequested(const QPoint &pnt); /** * Handles the actions of the header context menu for showing/hiding a column. */ void slotShowHideColumn(int columnIndex); /** * Handles the Adjust Column Sizes action of the header context menu. */ void slotAdjustColumnSizes(); /** * Handles the Show Default Columns action of the header context menu. */ void slotShowDefaultColumns(); /** * Handles the Display Tooltips action of the header context menu. */ void slotDisplayTooltips(bool showTooltips); /** * Handles section resizes in order to save the column widths */ void slotHeaderSectionResized(int logicalIndex, int oldWidth, int newWidth); /** * Handles selection item management */ void slotSelectionChanged(const QItemSelection ¤t, const QItemSelection &); /** * Saves the state of the columns (width and visility) to the currently selected theme object. */ void saveThemeColumnState(); /** * Applies the theme columns to this view. * Columns visible by default are shown, the other are hidden. * Visible columns are assigned space inside the view by using the size hints and some heuristics. */ void applyThemeColumns(); private: /// expand the whole thread (including all child messages) Q_PRIVATE_SLOT(d, void expandFullThread(const QModelIndex &)) class Private; Private *d; }; // class View } // namespace Core } // namespace MessageList #endif //!__MESSAGELIST_CORE_VIEW_H__ diff --git a/messagelist/src/core/widgetbase.cpp b/messagelist/src/core/widgetbase.cpp index c46edb9b43..0ab5511e42 100644 --- a/messagelist/src/core/widgetbase.cpp +++ b/messagelist/src/core/widgetbase.cpp @@ -1,1106 +1,1100 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "core/widgetbase.h" #include "core/widgets/quicksearchwarning.h" #include "core/aggregation.h" #include "core/theme.h" #include "core/filter.h" #include "core/manager.h" #include "core/optionset.h" #include "core/view.h" #include "core/model.h" #include "core/messageitem.h" #include "core/storagemodelbase.h" #include "messagelistsettings.h" #include "utils/configureaggregationsdialog.h" #include "utils/configurethemesdialog.h" #include #include #include #include #include #include #include #include #include "messagelist_debug.h" #include #include #include #include #include #include using namespace MessageList::Core; class Widget::Private { public: Private(Widget *owner) : q(owner), quickSearchLine(Q_NULLPTR), mView(Q_NULLPTR), mSearchTimer(Q_NULLPTR), mStorageModel(Q_NULLPTR), mAggregation(Q_NULLPTR), mTheme(Q_NULLPTR), mFilter(Q_NULLPTR), mStorageUsesPrivateTheme(false), mStorageUsesPrivateAggregation(false), mStorageUsesPrivateSortOrder(false), mStatusFilterComboPopulationInProgress(false) { } /** * Small helper for switching SortOrder::MessageSorting and SortOrder::SortDirection * on the fly. * After doing this, the sort indicator in the header is updated. */ void switchMessageSorting(SortOrder::MessageSorting messageSorting, SortOrder::SortDirection sortDirection, int logicalHeaderColumnIndex); /** * Check if our sort order can still be used with this aggregation. * This can happen if the global aggregation changed, for example we can now * have "most recent in subtree" sorting with an aggregation without threading. * If this happens, reset to the default sort order and don't use the global sort * order. */ void checkSortOrder(const StorageModel *storageModel); void setDefaultAggregationForStorageModel(const StorageModel *storageModel); void setDefaultThemeForStorageModel(const StorageModel *storageModel); void setDefaultSortOrderForStorageModel(const StorageModel *storageModel); void applyFilter(); Widget *const q; QuickSearchWarning *quickSearchWarning; QuickSearchLine *quickSearchLine; View *mView; QString mLastAggregationId; QString mLastThemeId; QTimer *mSearchTimer; StorageModel *mStorageModel; ///< The currently displayed storage. The storage itself /// is owned by MessageList::Widget. Aggregation *mAggregation; ///< The currently set aggregation mode, a deep copy Theme *mTheme; ///< The currently set theme, a deep copy SortOrder mSortOrder; ///< The currently set sort order Filter *mFilter; ///< The currently applied filter, owned by us. bool mStorageUsesPrivateTheme; ///< true if the current folder does not use the global theme bool mStorageUsesPrivateAggregation; ///< true if the current folder does not use the global aggregation bool mStorageUsesPrivateSortOrder; ///< true if the current folder does not use the global sort order QUrl mCurrentFolderUrl; ///< The Akonadi URL of the current folder Akonadi::Collection mCurrentFolder; ///< The current folder int mCurrentStatusFilterIndex; bool mStatusFilterComboPopulationInProgress; }; Widget::Widget(QWidget *pParent) : QWidget(pParent), d(new Private(this)) { Manager::registerWidget(this); connect(Manager::instance(), &Manager::aggregationsChanged, this, &Widget::aggregationsChanged); connect(Manager::instance(), &Manager::themesChanged, this, &Widget::themesChanged); setAutoFillBackground(true); setObjectName(QStringLiteral("messagelistwidget")); QVBoxLayout *g = new QVBoxLayout(this); g->setMargin(0); g->setSpacing(0); d->quickSearchLine = new QuickSearchLine; d->quickSearchLine->setObjectName(QStringLiteral("quicksearchline")); connect(d->quickSearchLine, &QuickSearchLine::clearButtonClicked, this, &Widget::searchEditClearButtonClicked); connect(d->quickSearchLine, &QuickSearchLine::searchEditTextEdited, this, &Widget::searchEditTextEdited); - connect(d->quickSearchLine, &QuickSearchLine::searchOptionChanged, this, &Widget::searchEditTextEdited); connect(d->quickSearchLine, &QuickSearchLine::statusButtonsClicked, this, &Widget::slotStatusButtonsClicked); g->addWidget(d->quickSearchLine, 0); d->quickSearchWarning = new QuickSearchWarning(this); g->addWidget(d->quickSearchWarning, 0); d->mView = new View(this); d->mView->setFrameStyle(QFrame::NoFrame); d->mView->setSortOrder(&d->mSortOrder); d->mView->setObjectName(QStringLiteral("messagealistview")); g->addWidget(d->mView, 1); connect(d->mView->header(), &QHeaderView::sectionClicked, this, &Widget::slotViewHeaderSectionClicked); d->mSearchTimer = Q_NULLPTR; } Widget::~Widget() { d->mView->setStorageModel(Q_NULLPTR); Manager::unregisterWidget(this); delete d->mSearchTimer; delete d->mTheme; delete d->mAggregation; delete d->mFilter; delete d->mStorageModel; delete d; } void Widget::changeQuicksearchVisibility(bool show) { KLineEdit *const lineEdit = d->quickSearchLine->searchEdit(); if (!show) { //if we hide it we do not want to apply the filter, //otherwise someone is maybe stuck with x new emails //and cannot read it because of filter lineEdit->clear(); //we focus the message list if we hide the searchbar d->mView->setFocus(Qt::OtherFocusReason); } else { // on show: we focus the lineedit for fast filtering lineEdit->setFocus(Qt::OtherFocusReason); if (d->mFilter) { resetFilter(); } } d->quickSearchLine->changeQuicksearchVisibility(show); MessageListSettings::self()->setShowQuickSearch(show); } void Widget::populateStatusFilterCombo() { if (d->mStatusFilterComboPopulationInProgress) { return; } d->mStatusFilterComboPopulationInProgress = true; KComboBox *tagFilterComboBox = d->quickSearchLine->tagFilterComboBox(); d->mCurrentStatusFilterIndex = (tagFilterComboBox->currentIndex() != -1) ? tagFilterComboBox->currentIndex() : 0; disconnect(tagFilterComboBox, static_cast(&KComboBox::currentIndexChanged), this, &Widget::statusSelected); tagFilterComboBox->clear(); fillMessageTagCombo(); } void Widget::addMessageTagItem(const QPixmap &icon, const QString &text, const QVariant &data) { d->quickSearchLine->tagFilterComboBox()->addItem(icon, text, data); } void Widget::setCurrentStatusFilterItem() { d->quickSearchLine->updateComboboxVisibility(); connect(d->quickSearchLine->tagFilterComboBox(), SIGNAL(currentIndexChanged(int)), this, SLOT(statusSelected(int))); d->quickSearchLine->tagFilterComboBox()->setCurrentIndex(d->mCurrentStatusFilterIndex >= d->quickSearchLine->tagFilterComboBox()->count() ? 0 : d->mCurrentStatusFilterIndex); d->mStatusFilterComboPopulationInProgress = false; } MessageItem *Widget::currentMessageItem() const { return view()->currentMessageItem(); } -MessageList::Core::QuickSearchLine::SearchOptions Widget::currentOptions() const -{ - return d->quickSearchLine->searchOptions(); -} - QList Widget::currentFilterStatus() const { if (d->mFilter) { return d->mFilter->status(); } return QList(); } QString Widget::currentFilterSearchString() const { if (d->mFilter) { return d->mFilter->searchString(); } return QString(); } QString Widget::currentFilterTagId() const { if (d->mFilter) { return d->mFilter->tagId(); } return QString(); } void Widget::Private::setDefaultAggregationForStorageModel(const StorageModel *storageModel) { const Aggregation *opt = Manager::instance()->aggregationForStorageModel(storageModel, &mStorageUsesPrivateAggregation); Q_ASSERT(opt); delete mAggregation; mAggregation = new Aggregation(*opt); mView->setAggregation(mAggregation); mLastAggregationId = opt->id(); } void Widget::Private::setDefaultThemeForStorageModel(const StorageModel *storageModel) { const Theme *opt = Manager::instance()->themeForStorageModel(storageModel, &mStorageUsesPrivateTheme); Q_ASSERT(opt); delete mTheme; mTheme = new Theme(*opt); mView->setTheme(mTheme); mLastThemeId = opt->id(); } void Widget::Private::checkSortOrder(const StorageModel *storageModel) { if (storageModel && mAggregation && !mSortOrder.validForAggregation(mAggregation)) { qCDebug(MESSAGELIST_LOG) << "Could not restore sort order for folder" << storageModel->id(); mSortOrder = SortOrder::defaultForAggregation(mAggregation, mSortOrder); // Change the global sort order if the sort order didn't fit the global aggregation. // Otherwise, if it is a per-folder aggregation, make the sort order per-folder too. if (mStorageUsesPrivateAggregation) { mStorageUsesPrivateSortOrder = true; } if (mStorageModel) { Manager::instance()->saveSortOrderForStorageModel(storageModel, mSortOrder, mStorageUsesPrivateSortOrder); } switchMessageSorting(mSortOrder.messageSorting(), mSortOrder.messageSortDirection(), -1); } } void Widget::Private::setDefaultSortOrderForStorageModel(const StorageModel *storageModel) { // Load the sort order from config and update column headers mSortOrder = Manager::instance()->sortOrderForStorageModel(storageModel, &mStorageUsesPrivateSortOrder); switchMessageSorting(mSortOrder.messageSorting(), mSortOrder.messageSortDirection(), -1); checkSortOrder(storageModel); } void Widget::saveCurrentSelection() { if (d->mStorageModel) { // Save the current selection MessageItem *lastSelectedMessageItem = d->mView->currentMessageItem(false); if (lastSelectedMessageItem) { d->mStorageModel->savePreSelectedMessage( lastSelectedMessageItem ? lastSelectedMessageItem->uniqueId() : 0 ); } } } void Widget::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode) { if (storageModel == d->mStorageModel) { return; // nuthin to do here } d->setDefaultAggregationForStorageModel(storageModel); d->setDefaultThemeForStorageModel(storageModel); d->setDefaultSortOrderForStorageModel(storageModel); StorageModel *oldModel = d->mStorageModel; d->mStorageModel = storageModel; d->mView->setStorageModel(d->mStorageModel, preSelectionMode); delete oldModel; d->quickSearchLine->tagFilterComboBox()->setEnabled(d->mStorageModel); d->quickSearchLine->searchEdit()->setEnabled(d->mStorageModel); d->quickSearchLine->setContainsOutboundMessages(d->mStorageModel->containsOutboundMessages()); } StorageModel *Widget::storageModel() const { return d->mStorageModel; } KLineEdit *Widget::quickSearch() const { return d->quickSearchLine->searchEdit(); } View *Widget::view() const { return d->mView; } void Widget::themeMenuAboutToShow() { if (!d->mStorageModel) { return; } QMenu *menu = dynamic_cast< QMenu * >(sender()); if (!menu) { return; } themeMenuAboutToShow(menu); } void Widget::themeMenuAboutToShow(QMenu *menu) { menu->clear(); menu->addSection(i18n("Theme")); QActionGroup *grp = new QActionGroup(menu); QList< Theme * > sortedThemes = Manager::instance()->themes().values(); QAction *act; qSort(sortedThemes.begin(), sortedThemes.end(), MessageList::Core::Theme::compareName); QList< Theme * >::ConstIterator endTheme(sortedThemes.constEnd()); for (QList< Theme * >::ConstIterator it = sortedThemes.constBegin(); it != endTheme; ++it) { act = menu->addAction((*it)->name()); act->setCheckable(true); grp->addAction(act); act->setChecked(d->mLastThemeId == (*it)->id()); act->setData(QVariant((*it)->id())); connect(act, &QAction::triggered, this, &Widget::themeSelected); } menu->addSeparator(); act = menu->addAction(i18n("Configure...")); connect(act, &QAction::triggered, this, &Widget::configureThemes); } void Widget::setPrivateSortOrderForStorage() { if (!d->mStorageModel) { return; } d->mStorageUsesPrivateSortOrder = !d->mStorageUsesPrivateSortOrder; Manager::instance()->saveSortOrderForStorageModel(d->mStorageModel, d->mSortOrder, d->mStorageUsesPrivateSortOrder); } void Widget::configureThemes() { Utils::ConfigureThemesDialog *dialog = new Utils::ConfigureThemesDialog(window()); dialog->selectTheme(d->mLastThemeId); dialog->show(); } void Widget::themeSelected(bool) { if (!d->mStorageModel) { return; // nuthin to do } QAction *act = dynamic_cast< QAction * >(sender()); if (!act) { return; } QVariant v = act->data(); const QString id = v.toString(); if (id.isEmpty()) { return; } const Theme *opt = Manager::instance()->theme(id); delete d->mTheme; d->mTheme = new Theme(*opt); d->mView->setTheme(d->mTheme); d->mLastThemeId = opt->id(); //mStorageUsesPrivateTheme = false; Manager::instance()->saveThemeForStorageModel(d->mStorageModel, opt->id(), d->mStorageUsesPrivateTheme); d->mView->reload(); } void Widget::aggregationMenuAboutToShow() { QMenu *menu = dynamic_cast< QMenu * >(sender()); if (!menu) { return; } aggregationMenuAboutToShow(menu); } void Widget::aggregationMenuAboutToShow(QMenu *menu) { menu->clear(); menu->addSection(i18n("Aggregation")); QActionGroup *grp = new QActionGroup(menu); QList< Aggregation * > sortedAggregations = Manager::instance()->aggregations().values(); QAction *act; qSort(sortedAggregations.begin(), sortedAggregations.end(), MessageList::Core::Aggregation::compareName); QList::ConstIterator endagg(sortedAggregations.constEnd()); for (QList< Aggregation * >::ConstIterator it = sortedAggregations.constBegin(); it != endagg; ++it) { act = menu->addAction((*it)->name()); act->setCheckable(true); grp->addAction(act); act->setChecked(d->mLastAggregationId == (*it)->id()); act->setData(QVariant((*it)->id())); connect(act, &QAction::triggered, this, &Widget::aggregationSelected); } menu->addSeparator(); act = menu->addAction(i18n("Configure...")); act->setData(QVariant(QString())); connect(act, &QAction::triggered, this, &Widget::aggregationSelected); } void Widget::aggregationSelected(bool) { QAction *act = dynamic_cast< QAction * >(sender()); if (!act) { return; } QVariant v = act->data(); QString id = v.toString(); if (id.isEmpty()) { Utils::ConfigureAggregationsDialog *dialog = new Utils::ConfigureAggregationsDialog(window()); dialog->selectAggregation(d->mLastAggregationId); dialog->show(); return; } if (!d->mStorageModel) { return; // nuthin to do } const Aggregation *opt = Manager::instance()->aggregation(id); delete d->mAggregation; d->mAggregation = new Aggregation(*opt); d->mView->setAggregation(d->mAggregation); d->mLastAggregationId = opt->id(); //mStorageUsesPrivateAggregation = false; Manager::instance()->saveAggregationForStorageModel(d->mStorageModel, opt->id(), d->mStorageUsesPrivateAggregation); // The sort order might not be valid anymore for this aggregation d->checkSortOrder(d->mStorageModel); d->mView->reload(); } void Widget::sortOrderMenuAboutToShow() { if (!d->mAggregation) { return; } QMenu *menu = dynamic_cast< QMenu * >(sender()); if (!menu) { return; } sortOrderMenuAboutToShow(menu); } void Widget::sortOrderMenuAboutToShow(QMenu *menu) { menu->clear(); menu->addSection(i18n("Message Sort Order")); QActionGroup *grp; QAction *act; QList< QPair< QString, int > > options; QList< QPair< QString, int > >::ConstIterator it; grp = new QActionGroup(menu); options = SortOrder::enumerateMessageSortingOptions(d->mAggregation->threading()); QList< QPair< QString, int > >::ConstIterator end(options.constEnd()); for (it = options.constBegin(); it != end; ++it) { act = menu->addAction((*it).first); act->setCheckable(true); grp->addAction(act); act->setChecked(d->mSortOrder.messageSorting() == (*it).second); act->setData(QVariant((*it).second)); } connect(grp, &QActionGroup::triggered, this, &Widget::messageSortingSelected); options = SortOrder::enumerateMessageSortDirectionOptions(d->mSortOrder.messageSorting()); if (options.size() >= 2) { menu->addSection(i18n("Message Sort Direction")); grp = new QActionGroup(menu); end = options.constEnd(); for (it = options.constBegin(); it != end; ++it) { act = menu->addAction((*it).first); act->setCheckable(true); grp->addAction(act); act->setChecked(d->mSortOrder.messageSortDirection() == (*it).second); act->setData(QVariant((*it).second)); } connect(grp, &QActionGroup::triggered, this, &Widget::messageSortDirectionSelected); } options = SortOrder::enumerateGroupSortingOptions(d->mAggregation->grouping()); if (options.size() >= 2) { menu->addSection(i18n("Group Sort Order")); grp = new QActionGroup(menu); end = options.constEnd(); for (it = options.constBegin(); it != end; ++it) { act = menu->addAction((*it).first); act->setCheckable(true); grp->addAction(act); act->setChecked(d->mSortOrder.groupSorting() == (*it).second); act->setData(QVariant((*it).second)); } connect(grp, &QActionGroup::triggered, this, &Widget::groupSortingSelected); } options = SortOrder::enumerateGroupSortDirectionOptions(d->mAggregation->grouping(), d->mSortOrder.groupSorting()); if (options.size() >= 2) { menu->addSection(i18n("Group Sort Direction")); grp = new QActionGroup(menu); end = options.constEnd(); for (it = options.constBegin(); it != end; ++it) { act = menu->addAction((*it).first); act->setCheckable(true); grp->addAction(act); act->setChecked(d->mSortOrder.groupSortDirection() == (*it).second); act->setData(QVariant((*it).second)); } connect(grp, &QActionGroup::triggered, this, &Widget::groupSortDirectionSelected); } menu->addSeparator(); act = menu->addAction(i18n("Folder Always Uses This Sort Order")); act->setCheckable(true); act->setChecked(d->mStorageUsesPrivateSortOrder); connect(act, &QAction::triggered, this, &Widget::setPrivateSortOrderForStorage); } void Widget::Private::switchMessageSorting(SortOrder::MessageSorting messageSorting, SortOrder::SortDirection sortDirection, int logicalHeaderColumnIndex) { mSortOrder.setMessageSorting(messageSorting); mSortOrder.setMessageSortDirection(sortDirection); // If the logicalHeaderColumnIndex was specified then we already know which // column we should set the sort indicator to. If it wasn't specified (it's -1) // then we need to find it out in the theme. if (logicalHeaderColumnIndex == -1) { // try to find the specified message sorting in the theme columns const QList< Theme::Column * > &columns = mTheme->columns(); int idx = 0; // First try with a well defined message sorting. foreach (const Theme::Column *column, columns) { if (!mView->header()->isSectionHidden(idx)) { if (column->messageSorting() == messageSorting) { // found a visible column with this message sorting logicalHeaderColumnIndex = idx; break; } } ++idx; } // if still not found, try again with a wider range if (logicalHeaderColumnIndex == -1) { idx = 0; foreach (const Theme::Column *column, columns) { if (!mView->header()->isSectionHidden(idx)) { if ( ( (column->messageSorting() == SortOrder::SortMessagesBySenderOrReceiver) || (column->messageSorting() == SortOrder::SortMessagesByReceiver) || (column->messageSorting() == SortOrder::SortMessagesBySender) ) && ( (messageSorting == SortOrder::SortMessagesBySenderOrReceiver) || (messageSorting == SortOrder::SortMessagesByReceiver) || (messageSorting == SortOrder::SortMessagesBySender) ) ) { // found a visible column with this message sorting logicalHeaderColumnIndex = idx; break; } } ++idx; } } } if (logicalHeaderColumnIndex == -1) { // not found: either not a column-based sorting or the related column is hidden mView->header()->setSortIndicatorShown(false); return; } mView->header()->setSortIndicatorShown(true); if (sortDirection == SortOrder::Ascending) { mView->header()->setSortIndicator(logicalHeaderColumnIndex, Qt::AscendingOrder); } else { mView->header()->setSortIndicator(logicalHeaderColumnIndex, Qt::DescendingOrder); } } void Widget::messageSortingSelected(QAction *action) { if (!d->mAggregation) { return; } if (!action) { return; } if (!d->mStorageModel) { return; } bool ok; SortOrder::MessageSorting ord = static_cast< SortOrder::MessageSorting >(action->data().toInt(&ok)); if (!ok) { return; } d->switchMessageSorting(ord, d->mSortOrder.messageSortDirection(), -1); Manager::instance()->saveSortOrderForStorageModel(d->mStorageModel, d->mSortOrder, d->mStorageUsesPrivateSortOrder); d->mView->reload(); } void Widget::messageSortDirectionSelected(QAction *action) { if (!d->mAggregation) { return; } if (!action) { return; } if (!d->mStorageModel) { return; } bool ok; SortOrder::SortDirection ord = static_cast< SortOrder::SortDirection >(action->data().toInt(&ok)); if (!ok) { return; } d->switchMessageSorting(d->mSortOrder.messageSorting(), ord, -1); Manager::instance()->saveSortOrderForStorageModel(d->mStorageModel, d->mSortOrder, d->mStorageUsesPrivateSortOrder); d->mView->reload(); } void Widget::groupSortingSelected(QAction *action) { if (!d->mAggregation) { return; } if (!action) { return; } if (!d->mStorageModel) { return; } bool ok; SortOrder::GroupSorting ord = static_cast< SortOrder::GroupSorting >(action->data().toInt(&ok)); if (!ok) { return; } d->mSortOrder.setGroupSorting(ord); Manager::instance()->saveSortOrderForStorageModel(d->mStorageModel, d->mSortOrder, d->mStorageUsesPrivateSortOrder); d->mView->reload(); } void Widget::groupSortDirectionSelected(QAction *action) { if (!d->mAggregation) { return; } if (!action) { return; } if (!d->mStorageModel) { return; } bool ok; SortOrder::SortDirection ord = static_cast< SortOrder::SortDirection >(action->data().toInt(&ok)); if (!ok) { return; } d->mSortOrder.setGroupSortDirection(ord); Manager::instance()->saveSortOrderForStorageModel(d->mStorageModel, d->mSortOrder, d->mStorageUsesPrivateSortOrder); d->mView->reload(); } void Widget::resetFilter() { delete d->mFilter; d->mFilter = Q_NULLPTR; d->mView->model()->setFilter(Q_NULLPTR); d->quickSearchLine->resetFilter(); d->quickSearchWarning->animatedHide(); } void Widget::slotViewHeaderSectionClicked(int logicalIndex) { if (!d->mTheme) { return; } if (!d->mAggregation) { return; } if (logicalIndex >= d->mTheme->columns().count()) { return; } if (!d->mStorageModel) { return; } const Theme::Column *column = d->mTheme->column(logicalIndex); if (!column) { return; // should never happen... } if (column->messageSorting() == SortOrder::NoMessageSorting) { return; // this is a null op. } if (d->mSortOrder.messageSorting() == column->messageSorting()) { // switch sort direction if (d->mSortOrder.messageSortDirection() == SortOrder::Ascending) { d->switchMessageSorting(d->mSortOrder.messageSorting(), SortOrder::Descending, logicalIndex); } else { d->switchMessageSorting(d->mSortOrder.messageSorting(), SortOrder::Ascending, logicalIndex); } } else { // keep sort direction but switch sort order d->switchMessageSorting(column->messageSorting(), d->mSortOrder.messageSortDirection(), logicalIndex); } Manager::instance()->saveSortOrderForStorageModel(d->mStorageModel, d->mSortOrder, d->mStorageUsesPrivateSortOrder); d->mView->reload(); } void Widget::themesChanged() { d->setDefaultThemeForStorageModel(d->mStorageModel); d->mView->reload(); } void Widget::aggregationsChanged() { d->setDefaultAggregationForStorageModel(d->mStorageModel); d->checkSortOrder(d->mStorageModel); d->mView->reload(); } void Widget::fillMessageTagCombo() { // nothing here: must be overridden in derived classes setCurrentStatusFilterItem(); } void Widget::tagIdSelected(const QVariant &data) { QString tagId = data.toString(); if (tagId.isEmpty()) { if (d->mFilter) { if (d->mFilter->isEmpty()) { resetFilter(); return; } } } else { if (!d->mFilter) { d->mFilter = new Filter(); } d->mFilter->setTagId(tagId); } d->mView->model()->setFilter(d->mFilter); } void Widget::statusSelected(int index) { if (index == 0) { resetFilter(); return; } tagIdSelected(d->quickSearchLine->tagFilterComboBox()->itemData(index)); d->mView->model()->setFilter(d->mFilter); } void Widget::searchEditTextEdited() { // This slot is called whenever the user edits the search QLineEdit. // Since the user is likely to type more than one character // so we start the real search after a short delay in order to catch // multiple textEdited() signals. if (!d->mSearchTimer) { d->mSearchTimer = new QTimer(this); connect(d->mSearchTimer, &QTimer::timeout, this, &Widget::searchTimerFired); } else { d->mSearchTimer->stop(); // eventually } d->mSearchTimer->setSingleShot(true); d->mSearchTimer->start(1000); } void Widget::slotStatusButtonsClicked() { // We also arbitrairly set tagId to an empty string, though we *could* allow filtering // by status AND tag... if (d->mFilter) { d->mFilter->setTagId(QString()); } QList lst = d->quickSearchLine->status(); if (lst.isEmpty()) { if (d->mFilter) { d->mFilter->setStatus(lst); if (d->mFilter->isEmpty()) { qCDebug(MESSAGELIST_LOG) << " RESET FILTER"; resetFilter(); return; } } } else { // don't have this status bit if (!d->mFilter) { d->mFilter = new Filter(); } d->mFilter->setStatus(lst); } d->mView->model()->setFilter(d->mFilter); } void Widget::searchTimerFired() { // A search is pending. if (d->mSearchTimer) { d->mSearchTimer->stop(); } if (!d->mFilter) { d->mFilter = new Filter(); } const QString text = d->quickSearchLine->searchEdit()->text(); if (!text.isEmpty()) { KCompletion *comp = d->quickSearchLine->searchEdit()->completionObject(); comp->addItem(text); } d->mFilter->setCurrentFolder(d->mCurrentFolder); - d->mFilter->setSearchString(text, d->quickSearchLine->searchOptions()); + d->mFilter->setSearchString(text); d->quickSearchWarning->setSearchText(text); if (d->mFilter->isEmpty()) { resetFilter(); return; } d->mView->model()->setFilter(d->mFilter); } void Widget::searchEditClearButtonClicked() { if (!d->mFilter) { return; } resetFilter(); d->mView->scrollTo(d->mView->currentIndex(), QAbstractItemView::PositionAtCenter); } void Widget::viewMessageSelected(MessageItem *) { } void Widget::viewMessageActivated(MessageItem *) { } void Widget::viewSelectionChanged() { } void Widget::viewMessageListContextPopupRequest(const QList< MessageItem * > &, const QPoint &) { } void Widget::viewGroupHeaderContextPopupRequest(GroupHeaderItem *, const QPoint &) { } void Widget::viewDragEnterEvent(QDragEnterEvent *) { } void Widget::viewDragMoveEvent(QDragMoveEvent *) { } void Widget::viewDropEvent(QDropEvent *) { } void Widget::viewStartDragRequest() { } void Widget::viewJobBatchStarted() { } void Widget::viewJobBatchTerminated() { } void Widget::viewMessageStatusChangeRequest(MessageItem *msg, Akonadi::MessageStatus set, Akonadi::MessageStatus clear) { Q_UNUSED(msg); Q_UNUSED(set); Q_UNUSED(clear); } void Widget::focusQuickSearch(const QString &selectedText) { d->quickSearchLine->focusQuickSearch(selectedText); } bool Widget::isThreaded() const { return d->mView->isThreaded(); } bool Widget::selectionEmpty() const { return d->mView->selectionEmpty(); } void Widget::setCurrentFolder(const Akonadi::Collection &collection) { d->mCurrentFolder = collection; } bool Widget::searchEditHasFocus() const { return d->quickSearchLine->searchEdit()->hasFocus(); } diff --git a/messagelist/src/core/widgetbase.h b/messagelist/src/core/widgetbase.h index 525a4e36a8..1da0038cea 100644 --- a/messagelist/src/core/widgetbase.h +++ b/messagelist/src/core/widgetbase.h @@ -1,287 +1,286 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #ifndef __MESSAGELIST_CORE_WIDGETBASE_H__ #define __MESSAGELIST_CORE_WIDGETBASE_H__ #include #include #include #include #include #include class KLineEdit; class QActionGroup; class KComboBox; class QMenu; namespace Akonadi { class Collection; class MessageStatus; } namespace MessageList { namespace Core { class GroupHeaderItem; class MessageItem; class StorageModel; class View; /** * Provides a widget which has the messagelist and the most important helper widgets, * like the search line and the comboboxes for changing status filtering, aggregation etc. */ class Widget : public QWidget { friend class View; Q_OBJECT public: explicit Widget(QWidget *parent); ~Widget(); /** * Sets the storage model for this Widget. * * Pre-selection is the action of automatically selecting a message just after the folder * has finished loading. See Model::setStorageModel() for more information. */ void setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode = PreSelectLastSelected); /** * Returns the StorageModel currently set. May be 0. */ StorageModel *storageModel() const; /** * Returns the search line of this widget. Can be 0 if the quick search * is disabled in the global configuration. */ KLineEdit *quickSearch() const; /** * Returns the View attached to this Widget. Never 0. */ View *view() const; /** * Returns the current MessageItem in the current folder. * May return 0 if there is no current message or no current folder. */ Core::MessageItem *currentMessageItem() const; /** * Returns the Akonadi::MessageStatus in the current quicksearch field. */ QList currentFilterStatus() const; /** * Returns the search term in the current quicksearch field. */ QString currentFilterSearchString() const; /** * Returns the id of the MessageItem::Tag currently set in the quicksearch field. */ QString currentFilterTagId() const; /** * Sets the focus on the quick search line of the currently active tab. */ void focusQuickSearch(const QString &selectedText); /** * Returns true if the current Aggregation is threaded, false otherwise * (or if there is no current Aggregation). */ bool isThreaded() const; /** * Fast function that determines if the selection is empty */ bool selectionEmpty() const; /** * Sets the current folder. */ void setCurrentFolder(const Akonadi::Collection &collection); void saveCurrentSelection(); bool searchEditHasFocus() const; void sortOrderMenuAboutToShow(QMenu *menu); void themeMenuAboutToShow(QMenu *menu); void aggregationMenuAboutToShow(QMenu *menu); - MessageList::Core::QuickSearchLine::SearchOptions currentOptions() const; public Q_SLOTS: /** * This is called to setup the status filter's KComboBox. */ void populateStatusFilterCombo(); /** * Shows or hides the quicksearch field, the filter combobox and the toolbutton for advanced search. */ void changeQuicksearchVisibility(bool); protected: /** * Called when the "Message Status/Tag" filter menu is opened by the user. * You may Q_DECL_OVERRIDE this function in order to add some "custom tag" entries * to the menu. The entries should be placed in a QActionGroup which should be returned * to the caller. The QAction objects associated to the entries should have * the string id of the tag set as data() and the tag icon set as icon(). * The default implementation does nothing. * * Once the tag retrieval is complete call setCurrentStatusFilterItem() */ virtual void fillMessageTagCombo(); void addMessageTagItem(const QPixmap &, const QString &, const QVariant &); /** * Must be called by fillMessageTagCombo() */ void setCurrentStatusFilterItem(); /** * This is called by View when a message is single-clicked (thus selected and made current) */ virtual void viewMessageSelected(MessageItem *msg); /** * This is called by View when a message is double-clicked or activated by other input means */ virtual void viewMessageActivated(MessageItem *msg); /** * This is called by View when selection changes. */ virtual void viewSelectionChanged(); /** * This is called by View when a message is right clicked. */ virtual void viewMessageListContextPopupRequest(const QList< MessageItem * > &selectedItems, const QPoint &globalPos); /** * This is called by View when a group header is right clicked. */ virtual void viewGroupHeaderContextPopupRequest(GroupHeaderItem *group, const QPoint &globalPos); /** * This is called by View when a drag enter event is received */ virtual void viewDragEnterEvent(QDragEnterEvent *e); /** * This is called by View when a drag move event is received */ virtual void viewDragMoveEvent(QDragMoveEvent *e); /** * This is called by View when a drop event is received */ virtual void viewDropEvent(QDropEvent *e); /** * This is called by View when a drag can possibly be started */ virtual void viewStartDragRequest(); /** * This is called by View when a message item is manipulated by the user * in a way that it's status should change. (e.g, by clicking on a status icon, for example). */ virtual void viewMessageStatusChangeRequest(MessageItem *msg, Akonadi::MessageStatus set, Akonadi::MessageStatus clear); /** * This is called by View to signal a start of a (possibly lengthy) job batch. */ virtual void viewJobBatchStarted(); /** * This is called by View to signal the end of a (possibly lengthy) job batch. */ virtual void viewJobBatchTerminated(); void tagIdSelected(const QVariant &data); Q_SIGNALS: /** * Notify the outside when updating the status bar with a message * could be useful */ void statusMessage(const QString &message); protected Q_SLOTS: /** * This is called by Manager when the option sets stored within have changed. */ void aggregationsChanged(); /** * This is called by Manager when the option sets stored within have changed. */ void themesChanged(); void themeMenuAboutToShow(); void aggregationMenuAboutToShow(); void themeSelected(bool); void configureThemes(); void setPrivateSortOrderForStorage(); void aggregationSelected(bool); void statusSelected(int index); void searchEditTextEdited(); void searchTimerFired(); void searchEditClearButtonClicked(); void sortOrderMenuAboutToShow(); void messageSortingSelected(QAction *action); void messageSortDirectionSelected(QAction *action); void groupSortingSelected(QAction *action); void groupSortDirectionSelected(QAction *action); void resetFilter(); /** * Handles header section clicks switching the Aggregation MessageSorting on-the-fly. */ void slotViewHeaderSectionClicked(int logicalIndex); void slotStatusButtonsClicked(); private: class Private; Private *const d; }; } // namespace Core } // namespace MessageList #endif //!__MESSAGELIST_CORE_WIDGET_H__ diff --git a/messagelist/src/core/widgets/quicksearchline.cpp b/messagelist/src/core/widgets/quicksearchline.cpp index b2fe914c7d..944f880766 100644 --- a/messagelist/src/core/widgets/quicksearchline.cpp +++ b/messagelist/src/core/widgets/quicksearchline.cpp @@ -1,416 +1,254 @@ /* Copyright (c) 2014-2015 Montel Laurent This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "quicksearchline.h" #include "messagelistsettings.h" #include #include "PimCommon/LineEditWithCompleter" #include #include #include #include #include #include #include #include #include #include #include #include using namespace MessageList::Core; QuickSearchLine::QuickSearchLine(QWidget *parent) : QWidget(parent), mContainsOutboundMessages(false), mFilterStatusMapper(Q_NULLPTR) { QVBoxLayout *vbox = new QVBoxLayout; vbox->setMargin(0); vbox->setSpacing(0); setLayout(vbox); QWidget *w = new QWidget; QHBoxLayout *hbox = new QHBoxLayout; hbox->setMargin(0); hbox->setSpacing(0); w->setLayout(hbox); vbox->addWidget(w); - mQuickSearchFilterWidget = new QWidget; - mQuickSearchFilterWidget->setObjectName(QStringLiteral("quicksearchfilterwidget")); - QHBoxLayout *quickSearchButtonLayout = new QHBoxLayout; - mQuickSearchFilterWidget->setLayout(quickSearchButtonLayout); - quickSearchButtonLayout->addStretch(0); - QLabel *quickLab = new QLabel(i18n("Quick Filter:")); - quickSearchButtonLayout->addWidget(quickLab); - initializeStatusSearchButton(quickSearchButtonLayout); - vbox->addWidget(mQuickSearchFilterWidget); - - mQuickSearchFilterWidget->hide(); - mSearchEdit = new PimCommon::LineEditWithCompleter(this); mSearchEdit->setPlaceholderText(i18nc("Search for messages.", "Search")); mSearchEdit->setObjectName(QStringLiteral("quicksearch")); mSearchEdit->setClearButtonShown(true); connect(mSearchEdit, &KLineEdit::textChanged, this, &QuickSearchLine::slotSearchEditTextEdited); connect(mSearchEdit, &KLineEdit::clearButtonClicked, this, &QuickSearchLine::slotClearButtonClicked); hbox->addWidget(mSearchEdit); - mMoreOptions = new QPushButton(QIcon::fromTheme(QStringLiteral("arrow-down-double")), i18n("More..."), this); - mMoreOptions->setObjectName(QStringLiteral("moreoptions")); - mMoreOptions->setFlat(true); - mMoreOptions->setCheckable(true); - connect(mMoreOptions, &QPushButton::toggled, this, &QuickSearchLine::slotMoreOptionClicked); - hbox->addWidget(mMoreOptions); - // The status filter button. Will be populated later, as populateStatusFilterCombo() is virtual mTagFilterCombo = new KComboBox(this); mTagFilterCombo->setMaximumWidth(300); mTagFilterCombo->setMaximumWidth(200); mTagFilterCombo->hide(); hbox->addWidget(mTagFilterCombo); //Be disable until we have a storageModel => logical that it's disable. mSearchEdit->setEnabled(false); mTagFilterCombo->setEnabled(false); - mExtraOption = new QWidget; - mExtraOption->setObjectName(QStringLiteral("extraoptions")); - hbox = new QHBoxLayout; - hbox->setMargin(0); - vbox->addWidget(mExtraOption); - mExtraOption->setLayout(hbox); - mExtraOption->hide(); - - hbox->addStretch(0); - QLabel *lab = new QLabel(i18n("Filter message by:")); - hbox->addWidget(lab); - - mSearchEveryWhere = new QPushButton(i18n("Full Message")); - mSearchEveryWhere->setObjectName(QStringLiteral("full_message")); - mSearchEveryWhere->setFlat(true); - mSearchEveryWhere->setCheckable(true); - mSearchEveryWhere->setChecked(true); - connect(mSearchEveryWhere, &QPushButton::clicked, this, &QuickSearchLine::slotSearchBy); - hbox->addWidget(mSearchEveryWhere); - - mSearchAgainstBody = new QPushButton(i18n("Body")); - mSearchAgainstBody->setObjectName(QStringLiteral("body")); - mSearchAgainstBody->setFlat(true); - mSearchAgainstBody->setCheckable(true); - connect(mSearchAgainstBody, &QPushButton::clicked, this, &QuickSearchLine::slotSearchBy); - hbox->addWidget(mSearchAgainstBody); - - mSearchAgainstSubject = new QPushButton(i18n("Subject")); - mSearchAgainstSubject->setCheckable(true); - mSearchAgainstSubject->setFlat(true); - mSearchAgainstSubject->setObjectName(QStringLiteral("subject")); - connect(mSearchAgainstSubject, &QPushButton::clicked, this, &QuickSearchLine::slotSearchBy); - hbox->addWidget(mSearchAgainstSubject); - - mSearchAgainstFromOrTo = new QPushButton; - changeSearchAgainstFromOrToText(); - mSearchAgainstFromOrTo->setObjectName(QStringLiteral("fromorto")); - mSearchAgainstFromOrTo->setCheckable(true); - mSearchAgainstFromOrTo->setFlat(true); - connect(mSearchAgainstFromOrTo, &QPushButton::clicked, this, &QuickSearchLine::slotSearchBy); - hbox->addWidget(mSearchAgainstFromOrTo); - - mSearchAgainstBcc = new QPushButton(i18n("Bcc")); - mSearchAgainstBcc->setObjectName(QStringLiteral("bcc")); - mSearchAgainstBcc->setCheckable(true); - mSearchAgainstBcc->setFlat(true); - connect(mSearchAgainstBcc, &QPushButton::clicked, this, &QuickSearchLine::slotSearchBy); - hbox->addWidget(mSearchAgainstBcc); - - installEventFilter(this); - mMoreOptions->installEventFilter(this); mTagFilterCombo->installEventFilter(this); - mSearchEveryWhere->installEventFilter(this); - mSearchAgainstBody->installEventFilter(this); - mSearchAgainstSubject->installEventFilter(this); - mSearchAgainstFromOrTo->installEventFilter(this); - mSearchAgainstBcc->installEventFilter(this); - mQuickSearchFilterWidget->installEventFilter(this); - mExtraOption->installEventFilter(this); changeQuicksearchVisibility(MessageListSettings::self()->showQuickSearch()); } QuickSearchLine::~QuickSearchLine() { } -void QuickSearchLine::slotSearchBy() -{ - QObject *button = sender(); - if (mSearchEveryWhere != button) { - mSearchEveryWhere->setChecked(false); - } - if (mSearchAgainstBody != button) { - mSearchAgainstBody->setChecked(false); - } - if (mSearchAgainstSubject != button) { - mSearchAgainstSubject->setChecked(false); - } - if (mSearchAgainstFromOrTo != button) { - mSearchAgainstFromOrTo->setChecked(false); - } - if (mSearchAgainstBcc != button) { - mSearchAgainstBcc->setChecked(false); - } - - slotSearchOptionChanged(); -} - -void QuickSearchLine::slotMoreOptionClicked(bool b) -{ - mQuickSearchFilterWidget->setVisible(b); - if (b) { - mMoreOptions->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up-double"))); - } else { - mMoreOptions->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down-double"))); - } -} - void QuickSearchLine::slotSearchEditTextEdited(const QString &text) { int minimumStringLength = 3; if (text.startsWith(QLatin1Char('"')) && text.endsWith(QLatin1Char('"'))) { minimumStringLength = 5; } if (!text.trimmed().isEmpty()) { if (text.length() >= minimumStringLength) { - mExtraOption->show(); Q_EMIT searchEditTextEdited(text); - } else { - mExtraOption->hide(); } } else { slotClearButtonClicked(); } } void QuickSearchLine::slotClearButtonClicked() { - mExtraOption->hide(); - mSearchEveryWhere->setChecked(true); if (mTagFilterCombo->isVisible()) { mTagFilterCombo->setCurrentIndex(0); } Q_EMIT clearButtonClicked(); } -void QuickSearchLine::slotSearchOptionChanged() -{ - Q_EMIT searchOptionChanged(); -} - -QuickSearchLine::SearchOptions QuickSearchLine::searchOptions() const -{ - QuickSearchLine::SearchOptions searchOptions; - if (mSearchEveryWhere->isChecked()) { - searchOptions |= SearchEveryWhere; - } - if (mSearchAgainstBody->isChecked()) { - searchOptions |= SearchAgainstBody; - } - if (mSearchAgainstSubject->isChecked()) { - searchOptions |= SearchAgainstSubject; - } - if (mSearchAgainstFromOrTo->isChecked()) { - if (mContainsOutboundMessages) { - searchOptions |= SearchAgainstTo; - } else { - searchOptions |= SearchAgainstFrom; - } - } - if (mSearchAgainstBcc->isChecked()) { - searchOptions |= SearchAgainstBcc; - } - return searchOptions; -} - void QuickSearchLine::focusQuickSearch(const QString &selectedText) { if (!selectedText.isEmpty()) { mSearchEdit->setText(selectedText); } mSearchEdit->setFocus(); } KComboBox *QuickSearchLine::tagFilterComboBox() const { return mTagFilterCombo; } KLineEdit *QuickSearchLine::searchEdit() const { return mSearchEdit; } void QuickSearchLine::resetFilter() { Q_FOREACH (QToolButton *button, mListStatusButton) { button->setChecked(false); } if (mTagFilterCombo->isVisible()) { mTagFilterCombo->setCurrentIndex(0); } - mSearchEveryWhere->setChecked(true); - mSearchAgainstBody->setChecked(false); - mSearchAgainstSubject->setChecked(false); - mSearchAgainstFromOrTo->setChecked(false); - mSearchAgainstBcc->setChecked(false); - - mExtraOption->hide(); } void QuickSearchLine::createQuickSearchButton(const QIcon &icon, const QString &text, int value, QLayout *quickSearchButtonLayout) { QToolButton *button = new QToolButton; button->setIcon(icon); button->setText(text); button->setAutoRaise(true); button->setToolTip(text); button->setCheckable(true); button->setChecked(false); button->setProperty("statusvalue", value); mFilterStatusMapper->setMapping(button, value); connect(button, SIGNAL(clicked(bool)), mFilterStatusMapper, SLOT(map())); quickSearchButtonLayout->addWidget(button); button->installEventFilter(this); button->setFocusPolicy(Qt::StrongFocus); mListStatusButton.append(button); } bool QuickSearchLine::containsOutboundMessages() const { return mContainsOutboundMessages; } void QuickSearchLine::setContainsOutboundMessages(bool containsOutboundMessages) { if (mContainsOutboundMessages != containsOutboundMessages) { mContainsOutboundMessages = containsOutboundMessages; - changeSearchAgainstFromOrToText(); - } -} - -void QuickSearchLine::changeSearchAgainstFromOrToText() -{ - if (mContainsOutboundMessages) { - mSearchAgainstFromOrTo->setText(i18n("To")); - } else { - mSearchAgainstFromOrTo->setText(i18n("From")); } } void QuickSearchLine::initializeStatusSearchButton(QLayout *quickSearchButtonLayout) { //Bug Qt we can't use QButtonGroup + QToolButton + change focus. => use QSignalMapper(); mFilterStatusMapper = new QSignalMapper(this); connect(mFilterStatusMapper, static_cast(&QSignalMapper::mapped), this, &QuickSearchLine::statusButtonsClicked); createQuickSearchButton(QIcon::fromTheme(QStringLiteral("mail-unread")), i18nc("@action:inmenu Status of a message", "Unread"), Akonadi::MessageStatus::statusUnread().toQInt32(), quickSearchButtonLayout); createQuickSearchButton(QIcon::fromTheme(QStringLiteral("mail-replied")), i18nc("@action:inmenu Status of a message", "Replied"), Akonadi::MessageStatus::statusReplied().toQInt32(), quickSearchButtonLayout); createQuickSearchButton(QIcon::fromTheme(QStringLiteral("mail-forwarded")), i18nc("@action:inmenu Status of a message", "Forwarded"), Akonadi::MessageStatus::statusForwarded().toQInt32(), quickSearchButtonLayout); createQuickSearchButton(QIcon::fromTheme(QStringLiteral("emblem-important")), i18nc("@action:inmenu Status of a message", "Important"), Akonadi::MessageStatus::statusImportant().toQInt32(), quickSearchButtonLayout); createQuickSearchButton(QIcon::fromTheme(QStringLiteral("mail-task")), i18nc("@action:inmenu Status of a message", "Action Item"), Akonadi::MessageStatus::statusToAct().toQInt32(), quickSearchButtonLayout); createQuickSearchButton(QIcon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("messagelist/pics/mail-thread-watch.png"))), i18nc("@action:inmenu Status of a message", "Watched"), Akonadi::MessageStatus::statusWatched().toQInt32(), quickSearchButtonLayout); createQuickSearchButton(QIcon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("messagelist/pics/mail-thread-ignored.png"))), i18nc("@action:inmenu Status of a message", "Ignored"), Akonadi::MessageStatus::statusIgnored().toQInt32(), quickSearchButtonLayout); createQuickSearchButton(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18nc("@action:inmenu Status of a message", "Has Attachment"), Akonadi::MessageStatus::statusHasAttachment().toQInt32(), quickSearchButtonLayout); createQuickSearchButton(QIcon::fromTheme(QStringLiteral("mail-invitation")), i18nc("@action:inmenu Status of a message", "Has Invitation"), Akonadi::MessageStatus::statusHasInvitation().toQInt32(), quickSearchButtonLayout); createQuickSearchButton(QIcon::fromTheme(QStringLiteral("mail-mark-junk")), i18nc("@action:inmenu Status of a message", "Spam"), Akonadi::MessageStatus::statusSpam().toQInt32(), quickSearchButtonLayout); createQuickSearchButton(QIcon::fromTheme(QStringLiteral("mail-mark-notjunk")), i18nc("@action:inmenu Status of a message", "Ham"), Akonadi::MessageStatus::statusHam().toQInt32(), quickSearchButtonLayout); } QList QuickSearchLine::status() const { QList lstStatus; Q_FOREACH (QToolButton *button, mListStatusButton) { if (button->isChecked()) { Akonadi::MessageStatus status; status.fromQInt32(static_cast< qint32 >(button->property("statusvalue").toInt())); lstStatus.append(status); } } return lstStatus; } void QuickSearchLine::updateComboboxVisibility() { mTagFilterCombo->setVisible(!mSearchEdit->isHidden() && mTagFilterCombo->count()); } bool QuickSearchLine::eventFilter(QObject *object, QEvent *e) { const bool shortCutOverride = (e->type() == QEvent::ShortcutOverride); if (shortCutOverride) { e->accept(); return true; } return QWidget::eventFilter(object, e); } void QuickSearchLine::changeQuicksearchVisibility(bool show) { mSearchEdit->setVisible(show); mTagFilterCombo->setVisible(show && mTagFilterCombo->count()); - mMoreOptions->setVisible(show);************************************* } diff --git a/messagelist/src/core/widgets/quicksearchline.h b/messagelist/src/core/widgets/quicksearchline.h index dbf5367e91..0bdf2ec3f0 100644 --- a/messagelist/src/core/widgets/quicksearchline.h +++ b/messagelist/src/core/widgets/quicksearchline.h @@ -1,110 +1,82 @@ /* Copyright (c) 2014-2015 Montel Laurent This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef QUICKSEARCHLINE_H #define QUICKSEARCHLINE_H #include #include "messagelist_export.h" #include class KLineEdit; class KComboBox; class QToolButton; class QPushButton; class QSignalMapper; class QPushButton; namespace MessageList { namespace Core { class MESSAGELIST_EXPORT QuickSearchLine : public QWidget { Q_OBJECT public: explicit QuickSearchLine(QWidget *parent = Q_NULLPTR); ~QuickSearchLine(); - enum SearchOption { - SearchEveryWhere = 1, - SearchAgainstBody = 2, - SearchAgainstSubject = 4, - SearchAgainstFrom = 8, - SearchAgainstBcc = 16, - SearchAgainstTo = 32 - }; - - Q_ENUMS(SearchOption) - Q_DECLARE_FLAGS(SearchOptions, SearchOption) - - SearchOptions searchOptions() const; - void focusQuickSearch(const QString &selectedText); KComboBox *tagFilterComboBox() const; KLineEdit *searchEdit() const; - QToolButton *openFullSearchButton() const; void resetFilter(); QList status() const; void updateComboboxVisibility(); bool containsOutboundMessages() const; void setContainsOutboundMessages(bool containsOutboundMessages); void changeQuicksearchVisibility(bool show); Q_SIGNALS: void clearButtonClicked(); void searchEditTextEdited(const QString &); - void searchOptionChanged(); void statusButtonsClicked(); protected: bool eventFilter(QObject *object, QEvent *e) Q_DECL_OVERRIDE; private Q_SLOTS: - void slotSearchOptionChanged(); void slotSearchEditTextEdited(const QString &text); void slotClearButtonClicked(); - void slotMoreOptionClicked(bool b); - void slotSearchBy(); private: void initializeStatusSearchButton(QLayout *quickSearchButtonLayout); void createQuickSearchButton(const QIcon &icon, const QString &text, int value, QLayout *quickSearchButtonLayout); - void changeSearchAgainstFromOrToText(); QList mListStatusButton; KLineEdit *mSearchEdit; KComboBox *mTagFilterCombo; - QPushButton *mMoreOptions; - QPushButton *mSearchEveryWhere; - QPushButton *mSearchAgainstBody; - QPushButton *mSearchAgainstSubject; - QPushButton *mSearchAgainstFromOrTo; - QPushButton *mSearchAgainstBcc; - QWidget *mExtraOption; - QWidget *mQuickSearchFilterWidget; bool mContainsOutboundMessages; QSignalMapper *mFilterStatusMapper; }; } } #endif // QUICKSEARCHLINE_H diff --git a/messagelist/src/pane.cpp b/messagelist/src/pane.cpp index 95daef622e..95041238e4 100644 --- a/messagelist/src/pane.cpp +++ b/messagelist/src/pane.cpp @@ -1,1198 +1,1189 @@ /* Copyright (c) 2009 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "pane.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "storagemodel.h" #include "core/widgets/quicksearchline.h" #include "widget.h" #include "messagelistsettings.h" #include "core/manager.h" #include #include "core/model.h" namespace MessageList { class Q_DECL_HIDDEN Pane::Private { public: Private(Pane *owner) : q(owner), mXmlGuiClient(Q_NULLPTR), mActionMenu(Q_NULLPTR), mModel(Q_NULLPTR), mSelectionModel(Q_NULLPTR), mPreSelectionMode(Core::PreSelectLastSelected), mNewTabButton(Q_NULLPTR), mCloseTabButton(Q_NULLPTR), mCloseTabAction(Q_NULLPTR), mActivateNextTabAction(Q_NULLPTR), mActivatePreviousTabAction(Q_NULLPTR), mMoveTabLeftAction(Q_NULLPTR), mMoveTabRightAction(Q_NULLPTR), mPreferEmptyTab(false), mMaxTabCreated(0) { } void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); void onNewTabClicked(); void onCloseTabClicked(); void activateTab(); void closeTab(QWidget *); void onCurrentTabChanged(); void onTabContextMenuRequest(const QPoint &pos); void activateNextTab(); void activatePreviousTab(); void moveTabLeft(); void moveTabRight(); void moveTabBackward(); void moveTabForward(); void changeQuicksearchVisibility(bool); void addActivateTabAction(int i); void slotTabCloseRequested(int index); QItemSelection mapSelectionToSource(const QItemSelection &selection) const; QItemSelection mapSelectionFromSource(const QItemSelection &selection) const; void updateTabControls(); Pane *const q; KXMLGUIClient *mXmlGuiClient; KActionMenu *mActionMenu; QAbstractItemModel *mModel; QItemSelectionModel *mSelectionModel; Core::PreSelectionMode mPreSelectionMode; QHash mWidgetSelectionHash; QList mProxyStack; QToolButton *mNewTabButton; QToolButton *mCloseTabButton; QAction *mCloseTabAction; QAction *mActivateNextTabAction; QAction *mActivatePreviousTabAction; QAction *mMoveTabLeftAction; QAction *mMoveTabRightAction; bool mPreferEmptyTab; int mMaxTabCreated; }; } // namespace MessageList using namespace Akonadi; using namespace MessageList; Pane::Pane(bool restoreSession, QAbstractItemModel *model, QItemSelectionModel *selectionModel, QWidget *parent) : QTabWidget(parent), d(new Private(this)) { setDocumentMode(true); d->mModel = model; d->mSelectionModel = selectionModel; // Build the proxy stack const QAbstractProxyModel *proxyModel = qobject_cast(d->mSelectionModel->model()); while (proxyModel) { if (static_cast(proxyModel) == d->mModel) { break; } d->mProxyStack << proxyModel; const QAbstractProxyModel *nextProxyModel = qobject_cast(proxyModel->sourceModel()); if (!nextProxyModel) { // It's the final model in the chain, so it is necessarily the sourceModel. Q_ASSERT(qobject_cast(proxyModel->sourceModel()) == d->mModel); break; } proxyModel = nextProxyModel; } // Proxy stack done d->mNewTabButton = new QToolButton(this); d->mNewTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-new"))); d->mNewTabButton->adjustSize(); d->mNewTabButton->setToolTip(i18nc("@info:tooltip", "Open a new tab")); #ifndef QT_NO_ACCESSIBILITY d->mNewTabButton->setAccessibleName(i18n("New tab")); #endif setCornerWidget(d->mNewTabButton, Qt::TopLeftCorner); connect(d->mNewTabButton, SIGNAL(clicked()), SLOT(onNewTabClicked())); d->mCloseTabButton = new QToolButton(this); d->mCloseTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); d->mCloseTabButton->adjustSize(); d->mCloseTabButton->setToolTip(i18nc("@info:tooltip", "Close the current tab")); #ifndef QT_NO_ACCESSIBILITY d->mCloseTabButton->setAccessibleName(i18n("Close tab")); #endif setCornerWidget(d->mCloseTabButton, Qt::TopRightCorner); connect(d->mCloseTabButton, SIGNAL(clicked()), SLOT(onCloseTabClicked())); setTabsClosable(MessageListSettings::self()->tabsHaveCloseButton()); connect(this, SIGNAL(tabCloseRequested(int)), this, SLOT(slotTabCloseRequested(int))); readConfig(restoreSession); setMovable(true); connect(d->mSelectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(onSelectionChanged(QItemSelection,QItemSelection))); connect(this, SIGNAL(currentChanged(int)), this, SLOT(onCurrentTabChanged())); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onTabContextMenuRequest(QPoint))); connect(MessageListSettings::self(), SIGNAL(configChanged()), this, SLOT(updateTabControls())); connect(this, &QTabWidget::tabBarDoubleClicked, this, &Pane::createNewTab); tabBar()->installEventFilter(this); } Pane::~Pane() { writeConfig(true); delete d; } void Pane::Private::addActivateTabAction(int i) { QString actionname; actionname.sprintf("activate_tab_%02d", i); QAction *action = new QAction(i18n("Activate Tab %1", i), q); mXmlGuiClient->actionCollection()->addAction(actionname, action); mXmlGuiClient->actionCollection()->setDefaultShortcut(action, QKeySequence(QStringLiteral("Alt+%1").arg(i))); connect(action, SIGNAL(triggered(bool)), q, SLOT(activateTab())); } void Pane::Private::slotTabCloseRequested(int index) { QWidget *w = q->widget(index); if (w) { closeTab(w); } } void Pane::setXmlGuiClient(KXMLGUIClient *xmlGuiClient) { d->mXmlGuiClient = xmlGuiClient; KToggleAction *const showHideQuicksearch = new KToggleAction(i18n("Show Quick Search Bar"), this); d->mXmlGuiClient->actionCollection()->setDefaultShortcut(showHideQuicksearch, Qt::CTRL + Qt::Key_H); showHideQuicksearch->setChecked(MessageListSettings::showQuickSearch()); d->mXmlGuiClient->actionCollection()->addAction(QStringLiteral("show_quick_search"), showHideQuicksearch); connect(showHideQuicksearch, SIGNAL(triggered(bool)), this, SLOT(changeQuicksearchVisibility(bool))); for (int i = 0; i < count(); ++i) { Widget *w = qobject_cast(widget(i)); w->setXmlGuiClient(d->mXmlGuiClient); } // Setup "View->Message List" actions. if (xmlGuiClient) { if (d->mActionMenu) { d->mXmlGuiClient->actionCollection()->removeAction(d->mActionMenu); } d->mActionMenu = new KActionMenu(QIcon(), i18n("Message List"), this); d->mXmlGuiClient->actionCollection()->addAction(QStringLiteral("view_message_list"), d->mActionMenu); MessageList::Util::fillViewMenu(d->mActionMenu->menu(), this); d->mActionMenu->addSeparator(); QAction *action = new QAction(i18n("Create New Tab"), this); d->mXmlGuiClient->actionCollection()->addAction(QStringLiteral("create_new_tab"), action); d->mXmlGuiClient->actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_O)); connect(action, SIGNAL(triggered(bool)), SLOT(onNewTabClicked())); d->mActionMenu->addAction(action); d->mMaxTabCreated = count(); for (int i = 1; i < 10 && i <= count(); ++i) { d->addActivateTabAction(i); } d->mCloseTabAction = new QAction(i18n("Close Tab"), this); d->mXmlGuiClient->actionCollection()->addAction(QStringLiteral("close_current_tab"), d->mCloseTabAction); d->mXmlGuiClient->actionCollection()->setDefaultShortcut(d->mCloseTabAction, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_W)); connect(d->mCloseTabAction, SIGNAL(triggered(bool)), SLOT(onCloseTabClicked())); d->mActionMenu->addAction(d->mCloseTabAction); d->mCloseTabAction->setEnabled(false); d->mActivateNextTabAction = new QAction(i18n("Activate Next Tab"), this); d->mXmlGuiClient->actionCollection()->addAction(QStringLiteral("activate_next_tab"), d->mActivateNextTabAction); d->mActivateNextTabAction->setEnabled(false); connect(d->mActivateNextTabAction, SIGNAL(triggered(bool)), SLOT(activateNextTab())); d->mActivatePreviousTabAction = new QAction(i18n("Activate Previous Tab"), this); d->mXmlGuiClient->actionCollection()->addAction(QStringLiteral("activate_previous_tab"), d->mActivatePreviousTabAction); d->mActivatePreviousTabAction->setEnabled(false); connect(d->mActivatePreviousTabAction, SIGNAL(triggered(bool)), SLOT(activatePreviousTab())); d->mMoveTabLeftAction = new QAction(i18n("Move Tab Left"), this); d->mXmlGuiClient->actionCollection()->addAction(QStringLiteral("move_tab_left"), d->mMoveTabLeftAction); d->mMoveTabLeftAction->setEnabled(false); connect(d->mMoveTabLeftAction, SIGNAL(triggered(bool)), SLOT(moveTabLeft())); d->mMoveTabRightAction = new QAction(i18n("Move Tab Right"), this); d->mXmlGuiClient->actionCollection()->addAction(QStringLiteral("move_tab_right"), d->mMoveTabRightAction); d->mMoveTabRightAction->setEnabled(false); connect(d->mMoveTabRightAction, SIGNAL(triggered(bool)), SLOT(moveTabRight())); } } bool Pane::selectNextMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop) { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return true; } return w->selectNextMessageItem(messageTypeFilter, existingSelectionBehaviour, centerItem, loop); } else { return false; } } bool Pane::selectPreviousMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop) { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return true; } return w->selectPreviousMessageItem(messageTypeFilter, existingSelectionBehaviour, centerItem, loop); } else { return false; } } bool Pane::focusNextMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem, bool loop) { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return true; } return w->focusNextMessageItem(messageTypeFilter, centerItem, loop); } else { return false; } } bool Pane::focusPreviousMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem, bool loop) { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return true; } return w->focusPreviousMessageItem(messageTypeFilter, centerItem, loop); } else { return false; } } void Pane::selectFocusedMessageItem(bool centerItem) { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return; } w->selectFocusedMessageItem(centerItem); } } bool Pane::selectFirstMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem) { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return true; } return w->selectFirstMessageItem(messageTypeFilter, centerItem); } else { return false; } } bool Pane::selectLastMessageItem(Core::MessageTypeFilter messageTypeFilter, bool centerItem) { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return true; } return w->selectLastMessageItem(messageTypeFilter, centerItem); } else { return false; } } void Pane::selectAll() { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return; } w->selectAll(); } } void Pane::setCurrentThreadExpanded(bool expand) { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return; } w->setCurrentThreadExpanded(expand); } } void Pane::setAllThreadsExpanded(bool expand) { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return; } w->setAllThreadsExpanded(expand); } } void Pane::setAllGroupsExpanded(bool expand) { Widget *w = static_cast(currentWidget()); if (w) { if (w->view()->model()->isLoading()) { return; } w->setAllGroupsExpanded(expand); } } void Pane::focusQuickSearch(const QString &selectedText) { Widget *w = static_cast(currentWidget()); if (w) { w->focusQuickSearch(selectedText); } } void Pane::setQuickSearchClickMessage(const QString &msg) { Widget *w = static_cast(currentWidget()); if (w) { w->setQuickSearchClickMessage(msg); } } void Pane::Private::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { if (mPreferEmptyTab) { q->createNewTab(); } Widget *w = static_cast(q->currentWidget()); QItemSelectionModel *s = mWidgetSelectionHash[w]; w->saveCurrentSelection(); // Deselect old before we select new - so that the messagelist can clear first. s->select(mapSelectionToSource(deselected), QItemSelectionModel::Deselect); if (s->selection().isEmpty()) { w->view()->model()->setPreSelectionMode(mPreSelectionMode); } s->select(mapSelectionToSource(selected), QItemSelectionModel::Select); QString label; QIcon icon; QString toolTip; foreach (const QModelIndex &index, s->selectedRows()) { label += index.data(Qt::DisplayRole).toString() + QLatin1String(", "); } label.chop(2); if (label.isEmpty()) { label = i18nc("@title:tab Empty messagelist", "Empty"); icon = QIcon(); } else if (s->selectedRows().size() == 1) { icon = s->selectedRows().first().data(Qt::DecorationRole).value(); QModelIndex idx = s->selectedRows().first().parent(); toolTip = label; while (idx != QModelIndex()) { toolTip = idx.data().toString() + QLatin1Char('/') + toolTip; idx = idx.parent(); } } else { icon = QIcon::fromTheme(QStringLiteral("folder")); } const int index = q->indexOf(w); q->setTabText(index, label); q->setTabIcon(index, icon); q->setTabToolTip(index, toolTip); if (mPreferEmptyTab) { disconnect(mSelectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)), q, SLOT(onSelectionChanged(QItemSelection,QItemSelection))); mSelectionModel->select(mapSelectionFromSource(s->selection()), QItemSelectionModel::ClearAndSelect); connect(mSelectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)), q, SLOT(onSelectionChanged(QItemSelection,QItemSelection))); } } void Pane::Private::activateTab() { q->tabBar()->setCurrentIndex(q->sender()->objectName().rightRef(2).toInt() - 1); } void Pane::Private::moveTabRight() { const int numberOfTab = q->tabBar()->count(); if (numberOfTab == 1) { return; } if (QApplication::isRightToLeft()) { moveTabForward(); } else { moveTabBackward(); } } void Pane::Private::moveTabLeft() { const int numberOfTab = q->tabBar()->count(); if (numberOfTab == 1) { return; } if (QApplication::isRightToLeft()) { moveTabBackward(); } else { moveTabForward(); } } void Pane::Private::moveTabForward() { const int currentIndex = q->tabBar()->currentIndex(); if (currentIndex == q->tabBar()->count() - 1) { return; } q->tabBar()->moveTab(currentIndex, currentIndex + 1); } void Pane::Private::moveTabBackward() { const int currentIndex = q->tabBar()->currentIndex(); if (currentIndex == 0) { return; } q->tabBar()->moveTab(currentIndex, currentIndex - 1); } void Pane::Private::activateNextTab() { const int numberOfTab = q->tabBar()->count(); if (numberOfTab == 1) { return; } int indexTab = (q->tabBar()->currentIndex() + 1); if (indexTab == numberOfTab) { indexTab = 0; } q->tabBar()->setCurrentIndex(indexTab); } void Pane::Private::activatePreviousTab() { const int numberOfTab = q->tabBar()->count(); if (numberOfTab == 1) { return; } int indexTab = (q->tabBar()->currentIndex() - 1); if (indexTab == -1) { indexTab = numberOfTab - 1; } q->tabBar()->setCurrentIndex(indexTab); } void Pane::Private::onNewTabClicked() { q->createNewTab(); } void Pane::Private::onCloseTabClicked() { closeTab(q->currentWidget()); } void Pane::Private::closeTab(QWidget *w) { if (!w || (q->count() < 2)) { return; } delete w; updateTabControls(); } void Pane::Private::changeQuicksearchVisibility(bool show) { for (int i = 0; i < q->count(); ++i) { Widget *w = qobject_cast(q->widget(i)); w->changeQuicksearchVisibility(show); } } bool Pane::eventFilter(QObject *object, QEvent *event) { if (event->type() == QEvent::MouseButtonPress) { QMouseEvent *const mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::MidButton) { return true; } } return QTabWidget::eventFilter(object, event); } void Pane::Private::onCurrentTabChanged() { Q_EMIT q->currentTabChanged(); Widget *w = static_cast(q->currentWidget()); QItemSelectionModel *s = mWidgetSelectionHash[w]; disconnect(mSelectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)), q, SLOT(onSelectionChanged(QItemSelection,QItemSelection))); mSelectionModel->select(mapSelectionFromSource(s->selection()), QItemSelectionModel::ClearAndSelect); connect(mSelectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)), q, SLOT(onSelectionChanged(QItemSelection,QItemSelection))); } void Pane::Private::onTabContextMenuRequest(const QPoint &pos) { QTabBar *bar = q->tabBar(); if (q->count() <= 1) { return; } const int indexBar = bar->tabAt(bar->mapFrom(q, pos)); if (indexBar == -1) { return; } Widget *w = qobject_cast(q->widget(indexBar)); if (!w) { return; } QMenu menu(q); QAction *closeTabAction = menu.addAction(i18nc("@action:inmenu", "Close Tab")); closeTabAction->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); QAction *allOther = menu.addAction(i18nc("@action:inmenu", "Close All Other Tabs")); allOther->setIcon(QIcon::fromTheme(QStringLiteral("tab-close-other"))); QAction *action = menu.exec(q->mapToGlobal(pos)); if (action == allOther) { // Close all other tabs QList widgets; const int index = q->indexOf(w); for (int i = 0; i < q->count(); ++i) { if (i == index) { continue; // Skip the current one } Widget *other = qobject_cast(q->widget(i)); widgets << other; } foreach (Widget *other, widgets) { delete other; } updateTabControls(); } else if (action == closeTabAction) { closeTab(q->widget(indexBar)); } } MessageList::StorageModel *Pane::createStorageModel(QAbstractItemModel *model, QItemSelectionModel *selectionModel, QObject *parent) { return new MessageList::StorageModel(model, selectionModel, parent); } void Pane::setCurrentFolder(const Akonadi::Collection &collection, bool, Core::PreSelectionMode preSelectionMode, const QString &overrideLabel) { d->mPreSelectionMode = preSelectionMode; Widget *w = static_cast(currentWidget()); if (w) { w->setCurrentFolder(collection); QItemSelectionModel *s = d->mWidgetSelectionHash[w]; MessageList::StorageModel *m = createStorageModel(d->mModel, s, w); w->setStorageModel(m, preSelectionMode); if (!overrideLabel.isEmpty()) { int index = indexOf(w); setTabText(index, overrideLabel); } } } void Pane::updateTabIconText(const Akonadi::Collection &collection, const QString &label, const QIcon &icon) { for (int i = 0; i < count(); ++i) { Widget *w = qobject_cast(widget(i)); if (w->currentCollection() == collection) { const int index = indexOf(w); setTabText(index, label); setTabIcon(index, icon); } } } QItemSelectionModel *Pane::createNewTab() { Widget *w = new Widget(this); w->setXmlGuiClient(d->mXmlGuiClient); addTab(w, i18nc("@title:tab Empty messagelist", "Empty")); if (d->mXmlGuiClient && count() < 10) { if (d->mMaxTabCreated < count()) { d->mMaxTabCreated = count(); d->addActivateTabAction(d->mMaxTabCreated); } } QItemSelectionModel *s = new QItemSelectionModel(d->mModel, w); MessageList::StorageModel *m = createStorageModel(d->mModel, s, w); w->setStorageModel(m); d->mWidgetSelectionHash[w] = s; connect(w, &Widget::messageSelected, this, &Pane::messageSelected); connect(w, &Widget::messageActivated, this, &Pane::messageActivated); connect(w, &Widget::selectionChanged, this, &Pane::selectionChanged); connect(w, &Widget::messageStatusChangeRequest, this, &Pane::messageStatusChangeRequest); connect(w, &Core::Widget::statusMessage, this, &Pane::statusMessage); d->updateTabControls(); setCurrentWidget(w); return s; } QItemSelection Pane::Private::mapSelectionToSource(const QItemSelection &selection) const { QItemSelection result = selection; foreach (const QAbstractProxyModel *proxy, mProxyStack) { result = proxy->mapSelectionToSource(result); } return result; } QItemSelection Pane::Private::mapSelectionFromSource(const QItemSelection &selection) const { QItemSelection result = selection; typedef QList::ConstIterator Iterator; for (Iterator it = mProxyStack.end() - 1; it != mProxyStack.begin(); --it) { result = (*it)->mapSelectionFromSource(result); } result = mProxyStack.first()->mapSelectionFromSource(result); return result; } void Pane::Private::updateTabControls() { const bool enableAction = (q->count() > 1); if (enableAction) { q->setCornerWidget(mCloseTabButton, Qt::TopRightCorner); mCloseTabButton->setVisible(true); } else { q->setCornerWidget(Q_NULLPTR, Qt::TopRightCorner); } if (mCloseTabAction) { mCloseTabAction->setEnabled(enableAction); } if (mActivatePreviousTabAction) { mActivatePreviousTabAction->setEnabled(enableAction); } if (mActivateNextTabAction) { mActivateNextTabAction->setEnabled(enableAction); } if (mMoveTabRightAction) { mMoveTabRightAction->setEnabled(enableAction); } if (mMoveTabLeftAction) { mMoveTabLeftAction->setEnabled(enableAction); } if (MessageListSettings::self()->autoHideTabBarWithSingleTab()) { q->tabBar()->setVisible(enableAction); if (enableAction) { q->setCornerWidget(mNewTabButton, Qt::TopLeftCorner); mNewTabButton->setVisible(true); } else { q->setCornerWidget(Q_NULLPTR, Qt::TopLeftCorner); } } else { q->tabBar()->setVisible(true); q->setCornerWidget(mNewTabButton, Qt::TopLeftCorner); mNewTabButton->setVisible(true); } const bool hasCloseButton(MessageListSettings::self()->tabsHaveCloseButton()); q->setTabsClosable(hasCloseButton); if (hasCloseButton) { const int numberOfTab(q->count()); if (numberOfTab == 1) { q->tabBar()->tabButton(0, QTabBar::RightSide)->setEnabled(false); } else if (numberOfTab > 1) { q->tabBar()->tabButton(0, QTabBar::RightSide)->setEnabled(true); } } } Item Pane::currentItem() const { Widget *w = static_cast(currentWidget()); if (w == Q_NULLPTR) { return Item(); } return w->currentItem(); } KMime::Message::Ptr Pane::currentMessage() const { Widget *w = static_cast(currentWidget()); if (w == Q_NULLPTR) { return KMime::Message::Ptr(); } return w->currentMessage(); } QList Pane::selectionAsMessageList(bool includeCollapsedChildren) const { Widget *w = static_cast(currentWidget()); if (w == Q_NULLPTR) { return QList(); } return w->selectionAsMessageList(includeCollapsedChildren); } Akonadi::Item::List Pane::selectionAsMessageItemList(bool includeCollapsedChildren) const { Widget *w = static_cast(currentWidget()); if (w == Q_NULLPTR) { return Akonadi::Item::List(); } return w->selectionAsMessageItemList(includeCollapsedChildren); } QList Pane::selectionAsListMessageId(bool includeCollapsedChildren) const { Widget *w = static_cast(currentWidget()); if (w == Q_NULLPTR) { return QList(); } return w->selectionAsListMessageId(includeCollapsedChildren); } QVector Pane::selectionAsMessageItemListId(bool includeCollapsedChildren) const { Widget *w = static_cast(currentWidget()); if (w == Q_NULLPTR) { return QVector(); } return w->selectionAsMessageItemListId(includeCollapsedChildren); } Akonadi::Item::List Pane::currentThreadAsMessageList() const { Widget *w = static_cast(currentWidget()); if (w == Q_NULLPTR) { return Akonadi::Item::List(); } return w->currentThreadAsMessageList(); } Akonadi::Item::List Pane::itemListFromPersistentSet(MessageList::Core::MessageItemSetReference ref) { Widget *w = static_cast(currentWidget()); if (w) { return w->itemListFromPersistentSet(ref); } return Akonadi::Item::List(); } void Pane::deletePersistentSet(MessageList::Core::MessageItemSetReference ref) { Widget *w = static_cast(currentWidget()); if (w) { w->deletePersistentSet(ref); } } void Pane::markMessageItemsAsAboutToBeRemoved(MessageList::Core::MessageItemSetReference ref, bool bMark) { Widget *w = static_cast(currentWidget()); if (w) { w->markMessageItemsAsAboutToBeRemoved(ref, bMark); } } QList Pane::currentFilterStatus() const { Widget *w = static_cast(currentWidget()); if (w == Q_NULLPTR) { return QList(); } return w->currentFilterStatus(); } -Core::QuickSearchLine::SearchOptions Pane::currentOptions() const -{ - Widget *w = static_cast(currentWidget()); - if (w == 0) { - return Core::QuickSearchLine::SearchEveryWhere; - } - return w->currentOptions(); -} - QString Pane::currentFilterSearchString() const { Widget *w = static_cast(currentWidget()); if (w) { return w->currentFilterSearchString(); } return QString(); } bool Pane::isThreaded() const { Widget *w = static_cast(currentWidget()); if (w) { return w->isThreaded(); } return false; } bool Pane::selectionEmpty() const { Widget *w = static_cast(currentWidget()); if (w) { return w->selectionEmpty(); } return false; } bool Pane::getSelectionStats(Akonadi::Item::List &selectedItems, Akonadi::Item::List &selectedVisibleItems, bool *allSelectedBelongToSameThread, bool includeCollapsedChildren) const { Widget *w = static_cast(currentWidget()); if (w == Q_NULLPTR) { return false; } return w->getSelectionStats( selectedItems, selectedVisibleItems, allSelectedBelongToSameThread, includeCollapsedChildren ); } MessageList::Core::MessageItemSetReference Pane::selectionAsPersistentSet(bool includeCollapsedChildren) const { Widget *w = static_cast(currentWidget()); if (w) { return w->selectionAsPersistentSet(includeCollapsedChildren); } return -1; } MessageList::Core::MessageItemSetReference Pane::currentThreadAsPersistentSet() const { Widget *w = static_cast(currentWidget()); if (w) { return w->currentThreadAsPersistentSet(); } return -1; } void Pane::focusView() { Widget *w = static_cast(currentWidget()); if (w) { QWidget *view = w->view(); if (view) { view->setFocus(); } } } void Pane::reloadGlobalConfiguration() { d->updateTabControls(); } QItemSelectionModel *Pane::currentItemSelectionModel() { Widget *w = static_cast(currentWidget()); if (w) { return w->view()->selectionModel(); } return Q_NULLPTR; } void Pane::resetModelStorage() { Widget *w = static_cast(currentWidget()); if (w) { MessageList::StorageModel *m = static_cast(w->storageModel()); if (m) { m->resetModelStorage(); } } } void Pane::setPreferEmptyTab(bool emptyTab) { d->mPreferEmptyTab = emptyTab; } void Pane::saveCurrentSelection() { for (int i = 0; i < count(); ++i) { Widget *w = qobject_cast(widget(i)); w->saveCurrentSelection(); } } void Pane::updateTagComboBox() { for (int i = 0; i < count(); ++i) { Widget *w = qobject_cast(widget(i)); w->populateStatusFilterCombo(); } } void Pane::writeConfig(bool restoreSession) { KConfigGroup conf(MessageList::MessageListSettings::self()->config(), "MessageListPane"); // Delete list before const QStringList list = MessageList::MessageListSettings::self()->config()->groupList().filter(QRegularExpression(QStringLiteral("MessageListTab\\d+"))); foreach (const QString &group, list) { MessageList::MessageListSettings::self()->config()->deleteGroup(group); } if (restoreSession) { conf.writeEntry(QStringLiteral("currentIndex"), currentIndex()); conf.writeEntry(QStringLiteral("tabNumber"), count()); for (int i = 0; i < count(); ++i) { Widget *w = qobject_cast(widget(i)); KConfigGroup grp(MessageList::MessageListSettings::self()->config(), QStringLiteral("MessageListTab%1").arg(i)); grp.writeEntry(QStringLiteral("collectionId"), w->currentCollection().id()); grp.writeEntry(QStringLiteral("HeaderState"), w->view()->header()->saveState()); } } conf.sync(); } void Pane::readConfig(bool restoreSession) { if (MessageList::MessageListSettings::self()->config()->hasGroup(QStringLiteral("MessageListPane"))) { KConfigGroup conf(MessageList::MessageListSettings::self()->config(), "MessageListPane"); const int numberOfTab = conf.readEntry(QStringLiteral("tabNumber"), 0); if (numberOfTab == 0) { createNewTab(); } else { for (int i = 0; i < numberOfTab; ++i) { createNewTab(); restoreHeaderSettings(i); if (restoreSession) { #if 0 //TODO fix me Akonadi::Collection::Id id = grp.readEntry(QStringLiteral("collectionId"), -1); ETMViewStateSaver *saver = new ETMViewStateSaver; saver->setSelectionModel(selectionModel); if (id != -1) { ETMViewStateSaver *saver = new ETMViewStateSaver; saver->setSelectionModel(selectionModel); saver->restoreState(grp); saver->selectCollections(Akonadi::Collection::List() << Akonadi::Collection(id)); saver->restoreCurrentItem(QString::fromLatin1("c%1").arg(id)); } #endif } } setCurrentIndex(conf.readEntry(QStringLiteral("currentIndex"), 0)); } } else { createNewTab(); restoreHeaderSettings(0); } } void Pane::restoreHeaderSettings(int index) { KConfigGroup grp(MessageList::MessageListSettings::self()->config(), QStringLiteral("MessageListTab%1").arg(index)); if (grp.exists()) { Widget *w = qobject_cast(widget(index)); w->view()->header()->restoreState(grp.readEntry(QStringLiteral("HeaderState"), QByteArray())); } } bool Pane::searchEditHasFocus() const { Widget *w = static_cast(currentWidget()); if (w) { return w->searchEditHasFocus(); } return false; } void Pane::sortOrderMenuAboutToShow() { QMenu *menu = dynamic_cast< QMenu * >(sender()); if (!menu) { return; } const Widget *const w = static_cast(currentWidget()); w->view()->sortOrderMenuAboutToShow(menu); } void Pane::aggregationMenuAboutToShow() { QMenu *menu = dynamic_cast< QMenu * >(sender()); if (!menu) { return; } const Widget *const w = static_cast(currentWidget()); w->view()->aggregationMenuAboutToShow(menu); } void Pane::themeMenuAboutToShow() { QMenu *menu = dynamic_cast< QMenu * >(sender()); if (!menu) { return; } const Widget *const w = static_cast(currentWidget()); w->view()->themeMenuAboutToShow(menu); } void Pane::populateStatusFilterCombo() { for (int i = 0; i < count(); ++i) { Widget *w = qobject_cast(widget(i)); w->populateStatusFilterCombo(); } } #include "moc_pane.cpp" diff --git a/messagelist/src/pane.h b/messagelist/src/pane.h index 0e0ae6e1be..de55549da3 100644 --- a/messagelist/src/pane.h +++ b/messagelist/src/pane.h @@ -1,471 +1,470 @@ /* Copyright (c) 2009 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __MESSAGELIST_PANE_H__ #define __MESSAGELIST_PANE_H__ #include #include #include #include #include #include #include #include #include class KXMLGUIClient; class QAbstractItemModel; class QItemSelectionModel; class QItemSelection; namespace KPIM { class MessageStatus; } namespace MessageList { class Widget; class StorageModel; /** * This is the main MessageList panel for Akonadi applications. * It contains multiple MessageList::Widget tabs * so it can actually display multiple folder sets at once. * * When a KXmlGuiWindow is passed to setXmlGuiClient, the XMLGUI * defined context menu @c akonadi_messagelist_contextmenu is * used if available. * */ class MESSAGELIST_EXPORT Pane : public QTabWidget { Q_OBJECT public: /** * Create a Pane wrapping the specified model and selection. */ explicit Pane(bool restoreSession, QAbstractItemModel *model, QItemSelectionModel *selectionModel, QWidget *parent = Q_NULLPTR); ~Pane(); virtual MessageList::StorageModel *createStorageModel(QAbstractItemModel *model, QItemSelectionModel *selectionModel, QObject *parent); virtual void writeConfig(bool restoreSession); /** * Sets the XML GUI client which the pane is used in. * * This is needed if you want to use the built-in context menu. * Passing 0 is ok and will disable the builtin context menu. * * @param xmlGuiClient The KXMLGUIClient the view is used in. */ void setXmlGuiClient(KXMLGUIClient *xmlGuiClient); /** * Returns the current message for the list as Akonadi::Item. * May return an invalid Item if there is no current message or no current folder. */ Akonadi::Item currentItem() const; /** * Returns the current message for the list as KMime::Message::Ptr. * May return 0 if there is no current message or no current folder. */ KMime::Message::Ptr currentMessage() const; /** * Returns the currently selected KMime::Message::Ptr (bound to current StorageModel). * The list may be empty if there are no selected messages or no StorageModel. * * If includeCollapsedChildren is true then the children of the selected but * collapsed items are also added to the list. * * The returned list is guaranteed to be valid only until you return control * to the main even loop. Don't store it for any longer. If you need to reference * this set of messages at a later stage then take a look at createPersistentSet(). */ QList selectionAsMessageList(bool includeCollapsedChildren = true) const; /** * Returns the currently selected Items (bound to current StorageModel). * The list may be empty if there are no selected messages or no StorageModel. * * If includeCollapsedChildren is true then the children of the selected but * collapsed items are also added to the list. * * The returned list is guaranteed to be valid only until you return control * to the main even loop. Don't store it for any longer. If you need to reference * this set of messages at a later stage then take a look at createPersistentSet(). */ Akonadi::Item::List selectionAsMessageItemList(bool includeCollapsedChildren = true) const; /** * Returns the currently selected Items id(bound to current StorageModel). * The list may be empty if there are no selected messages or no StorageModel. * * If includeCollapsedChildren is true then the children of the selected but * collapsed items are also added to the list. * * The returned list is guaranteed to be valid only until you return control * to the main even loop. Don't store it for any longer. If you need to reference * this set of messages at a later stage then take a look at createPersistentSet(). */ QVector selectionAsMessageItemListId(bool includeCollapsedChildren = true) const; QList selectionAsListMessageId(bool includeCollapsedChildren = true) const; /** * Returns the Akonadi::Item bound to the current StorageModel that * are part of the current thread. The current thread is the thread * that contains currentMessageItem(). * The list may be empty if there is no currentMessageItem() or no StorageModel. * * The returned list is guaranteed to be valid only until you return control * to the main even loop. Don't store it for any longer. If you need to reference * this set of messages at a later stage then take a look at createPersistentSet(). */ Akonadi::Item::List currentThreadAsMessageList() const; /** * Selects the next message item in the view. * * messageTypeFilter can be used to restrict the selection to only certain message types. * * existingSelectionBehaviour specifies how the existing selection * is manipulated. It may be cleared, expanded or grown/shrinked. * * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * If loop is true then the "next" algorithm will restart from the beginning * of the list if the end is reached, otherwise it will just stop returning false. */ bool selectNextMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop); /** * Selects the previous message item in the view. * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * * messageTypeFilter can be used to restrict the selection to only certain message types. * * existingSelectionBehaviour specifies how the existing selection * is manipulated. It may be cleared, expanded or grown/shrinked. * * If loop is true then the "previous" algorithm will restart from the end * of the list if the beginning is reached, otherwise it will just stop returning false. */ bool selectPreviousMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop); /** * Focuses the next message item in the view without actually selecting it. * * messageTypeFilter can be used to restrict the selection to only certain message types. * * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * If loop is true then the "next" algorithm will restart from the beginning * of the list if the end is reached, otherwise it will just stop returning false. */ bool focusNextMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem, bool loop); /** * Focuses the previous message item in the view without actually selecting it. * * messageTypeFilter can be used to restrict the selection to only certain message types. * * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * If loop is true then the "previous" algorithm will restart from the end * of the list if the beginning is reached, otherwise it will just stop returning false. */ bool focusPreviousMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem, bool loop); /** * Selects the currently focused message item. May do nothing if the * focused message item is already selected (which is very likely). * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. */ void selectFocusedMessageItem(bool centerItem); /** * Selects the first message item in the view that matches the specified Core::MessageTypeFilter. * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * * If the current view is already loaded then the request will * be satisfied immediately (well... if an unread message exists at all). * If the current view is still loading then the selection of the first * message will be scheduled to be executed when loading terminates. * * So this function doesn't actually guarantee that an unread or new message * was selected when the call returns. Take care :) * * The function returns true if a message was selected and false otherwise. */ bool selectFirstMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem); /** * Selects the last message item in the view that matches the specified Core::MessageTypeFilter. * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * * The function returns true if a message was selected and false otherwise. */ bool selectLastMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem); /** * If expand is true then it expands the current thread, otherwise * collapses it. */ void setCurrentThreadExpanded(bool expand); /** * If expand is true then it expands all the threads, otherwise * collapses them. */ void setAllThreadsExpanded(bool expand); /** * If expand is true then it expands all the groups (only the toplevel * group item: inner threads are NOT expanded). If expand is false * then it collapses all the groups. If no grouping is in effect * then this function does nothing. */ void setAllGroupsExpanded(bool expand); /** * Sets the focus on the quick search line of the currently active tab. */ void focusQuickSearch(const QString &selectedText = QString()); /** * Returns the Akonadi::MessageStatus in the current quicksearch field. */ QList currentFilterStatus() const; /** * Returns the search term in the current quicksearch field. */ QString currentFilterSearchString() const; /** * Returns true if the current Aggregation is threaded, false otherwise * (or if there is no current Aggregation). */ bool isThreaded() const; /** * Fast function that determines if the selection is empty */ bool selectionEmpty() const; /** * Fills the lists of the selected message serial numbers and of the selected+visible ones. * Returns true if the returned stats are valid (there is a current folder after all) * and false otherwise. This is called by KMMainWidget in a single place so we optimize by * making it a single sweep on the selection. * * If includeCollapsedChildren is true then the children of the selected but * collapsed items are also included in the stats */ bool getSelectionStats(Akonadi::Item::List &selectedItems, Akonadi::Item::List &selectedVisibleItems, bool *allSelectedBelongToSameThread, bool includeCollapsedChildren = true) const; /** * Deletes the persistent set pointed by the specified reference. * If the set does not exist anymore, nothing happens. */ void deletePersistentSet(MessageList::Core::MessageItemSetReference ref); /** * If bMark is true this function marks the messages as "about to be removed" * so they appear dimmer and aren't selectable in the view. * If bMark is false then this function clears the "about to be removed" state * for the specified MessageItems. */ void markMessageItemsAsAboutToBeRemoved(MessageList::Core::MessageItemSetReference ref, bool bMark); /** * Return Akonadi::Item from messageItemReference */ Akonadi::Item::List itemListFromPersistentSet(MessageList::Core::MessageItemSetReference ref); /** * Return a persistent set from current selection */ MessageList::Core::MessageItemSetReference selectionAsPersistentSet(bool includeCollapsedChildren = true) const; /** * Return a persistent set from current thread */ MessageList::Core::MessageItemSetReference currentThreadAsPersistentSet() const; /** * Sets the focus on the view of the currently active tab. */ void focusView(); /** * Reloads global configuration and eventually reloads all the views. */ void reloadGlobalConfiguration(); /** * Returns the QItemSelectionModel for the currently displayed tab. */ QItemSelectionModel *currentItemSelectionModel(); /** * Sets the current folder to be displayed by this Pane. * If the specified folder is already open in one of the tabs * then that tab is made current (and no reloading happens). * If the specified folder is not open yet then behaviour * depends on the preferEmptyTab value as follows. * * If preferEmptyTab is set to false then the (new) folder is loaded * in the current tab. If preferEmptyTab is set to true then the (new) folder is * loaded in the first empty tab (or a new one if there are no empty ones). * * Pre-selection is the action of automatically selecting a message just after the folder * has finished loading. See Model::setStorageModel() for more information. * * If overrideLabel is not empty then it's used as the tab text for the * specified folder. This is useful to signal a particular folder state * like "loading..." */ void setCurrentFolder( const Akonadi::Collection &fld, bool preferEmptyTab = false, MessageList::Core::PreSelectionMode preSelectionMode = MessageList::Core::PreSelectLastSelected, const QString &overrideLabel = QString() ); void resetModelStorage(); void setPreferEmptyTab(bool emptyTab); void updateTabIconText(const Akonadi::Collection &collection, const QString &label, const QIcon &icon); void saveCurrentSelection(); void updateTagComboBox(); bool searchEditHasFocus() const; void setQuickSearchClickMessage(const QString &msg); void populateStatusFilterCombo(); - Core::QuickSearchLine::SearchOptions currentOptions() const; public Q_SLOTS: /** * Selects all the items in the current folder. */ void selectAll(); /** * Add a new tab to the Pane and select it. */ QItemSelectionModel *createNewTab(); void sortOrderMenuAboutToShow(); void aggregationMenuAboutToShow(); void themeMenuAboutToShow(); Q_SIGNALS: /** * Emitted when a message is selected (that is, single clicked and thus made current in the view) * Note that this message CAN be 0 (when the current item is cleared, for example). * * This signal is emitted when a SINGLE message is selected in the view, probably * by clicking on it or by simple keyboard navigation. When multiple items * are selected at once (by shift+clicking, for example) then you will get * this signal only for the last clicked message (or at all, if the last shift+clicked * thing is a group header...). You should handle selection changed in this case. */ void messageSelected(const Akonadi::Item &item); /** * Emitted when a message is doubleclicked or activated by other input means */ void messageActivated(const Akonadi::Item &item); /** * Emitted when the selection in the view changes. */ void selectionChanged(); /** * Emitted when a message wants its status to be changed */ void messageStatusChangeRequest(const Akonadi::Item &item, const Akonadi::MessageStatus &set, const Akonadi::MessageStatus &clear); /** * Notify the outside when updating the status bar with a message * could be useful */ void statusMessage(const QString &message); /** * Emitted when the current tab has changed. Clients using the * selection model from currentItemSelectionModel() should * ask for it again, as it may be different now. */ void currentTabChanged(); private: void restoreHeaderSettings(int index); void readConfig(bool restoreSession); Q_PRIVATE_SLOT(d, void onSelectionChanged(const QItemSelection &, const QItemSelection &)) Q_PRIVATE_SLOT(d, void onNewTabClicked()) Q_PRIVATE_SLOT(d, void onCloseTabClicked()) Q_PRIVATE_SLOT(d, void activateTab()) Q_PRIVATE_SLOT(d, void moveTabLeft()) Q_PRIVATE_SLOT(d, void moveTabRight()) Q_PRIVATE_SLOT(d, void activateNextTab()) Q_PRIVATE_SLOT(d, void activatePreviousTab()) Q_PRIVATE_SLOT(d, void closeTab(QWidget *)) Q_PRIVATE_SLOT(d, void onCurrentTabChanged()) Q_PRIVATE_SLOT(d, void onTabContextMenuRequest(const QPoint &)) Q_PRIVATE_SLOT(d, void updateTabControls()) Q_PRIVATE_SLOT(d, void changeQuicksearchVisibility(bool)) Q_PRIVATE_SLOT(d, void slotTabCloseRequested(int index)) bool eventFilter(QObject *obj, QEvent *event) Q_DECL_OVERRIDE; class Private; Private *const d; }; } // namespace MessageList #endif //!__MESSAGELIST_PANE_H__ diff --git a/messagelist/src/widget.cpp b/messagelist/src/widget.cpp index 22efb36fc5..ae5880c0ae 100644 --- a/messagelist/src/widget.cpp +++ b/messagelist/src/widget.cpp @@ -1,768 +1,763 @@ /* Copyright (c) 2009 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "widget.h" #include #include #include #include #include "storagemodel.h" #include "core/messageitem.h" #include "core/view.h" #include #include #include #include #include #include #include #include #include "messagelist_debug.h" #include #include #include #include #include #include #include #include "core/groupheaderitem.h" #include #include #include #include #include namespace MessageList { class Q_DECL_HIDDEN Widget::Private { public: Private(Widget *owner) : q(owner), mLastSelectedMessage(-1), mXmlGuiClient(Q_NULLPTR), mMonitor(Q_NULLPTR) { } Akonadi::Item::List selectionAsItems() const; Akonadi::Item itemForRow(int row) const; KMime::Message::Ptr messageForRow(int row) const; Widget *const q; int mLastSelectedMessage; KXMLGUIClient *mXmlGuiClient; QModelIndex mGroupHeaderItemIndex; Akonadi::Monitor *mMonitor; }; } // namespace MessageList using namespace MessageList; using namespace Akonadi; Widget::Widget(QWidget *parent) : Core::Widget(parent), d(new Private(this)) { populateStatusFilterCombo(); d->mMonitor = new Akonadi::Monitor(this); d->mMonitor->setTypeMonitored(Akonadi::Monitor::Tags); connect(d->mMonitor, &Akonadi::Monitor::tagAdded, this, &Widget::populateStatusFilterCombo); connect(d->mMonitor, &Akonadi::Monitor::tagRemoved, this, &Widget::populateStatusFilterCombo); connect(d->mMonitor, &Akonadi::Monitor::tagChanged, this, &Widget::populateStatusFilterCombo); } Widget::~Widget() { delete d; } void Widget::setXmlGuiClient(KXMLGUIClient *xmlGuiClient) { d->mXmlGuiClient = xmlGuiClient; } bool Widget::canAcceptDrag(const QDropEvent *e) { if (e->source() == view()->viewport()) { return false; } Collection::List collections = static_cast(storageModel())->displayedCollections(); if (collections.size() != 1) { return false; // no folder here or too many (in case we can't decide where the drop will end) } const Collection target = collections.first(); if ((target.rights() & Collection::CanCreateItem) == 0) { return false; // no way to drag into } const QList urls = e->mimeData()->urls(); foreach (const QUrl &url, urls) { const Collection collection = Collection::fromUrl(url); if (collection.isValid()) { // You're not supposed to drop collections here return false; } else { // Yay, this is an item! const QString type = url.queryItemValue(QStringLiteral("type")); if (!target.contentMimeTypes().contains(type)) { return false; } } } return true; } bool Widget::selectNextMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop) { return view()->selectNextMessageItem(messageTypeFilter, existingSelectionBehaviour, centerItem, loop); } bool Widget::selectPreviousMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop) { return view()->selectPreviousMessageItem(messageTypeFilter, existingSelectionBehaviour, centerItem, loop); } bool Widget::focusNextMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem, bool loop) { return view()->focusNextMessageItem(messageTypeFilter, centerItem, loop); } bool Widget::focusPreviousMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem, bool loop) { return view()->focusPreviousMessageItem(messageTypeFilter, centerItem, loop); } void Widget::selectFocusedMessageItem(bool centerItem) { view()->selectFocusedMessageItem(centerItem); } bool Widget::selectFirstMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem) { return view()->selectFirstMessageItem(messageTypeFilter, centerItem); } bool Widget::selectLastMessageItem(Core::MessageTypeFilter messageTypeFilter, bool centerItem) { return view()->selectLastMessageItem(messageTypeFilter, centerItem); } void Widget::selectAll() { view()->setAllGroupsExpanded(true); view()->selectAll(); } void Widget::setCurrentThreadExpanded(bool expand) { view()->setCurrentThreadExpanded(expand); } void Widget::setAllThreadsExpanded(bool expand) { view()->setAllThreadsExpanded(expand); } void Widget::setAllGroupsExpanded(bool expand) { view()->setAllGroupsExpanded(expand); } void Widget::focusQuickSearch(const QString &selectedText) { view()->focusQuickSearch(selectedText); } void Widget::setQuickSearchClickMessage(const QString &msg) { view()->setQuickSearchClickMessage(msg); } void Widget::fillMessageTagCombo() { Akonadi::TagFetchJob *fetchJob = new Akonadi::TagFetchJob(this); fetchJob->fetchScope().fetchAttribute(); connect(fetchJob, &Akonadi::TagFetchJob::result, this, &Widget::slotTagsFetched); } void Widget::slotTagsFetched(KJob *job) { if (job->error()) { qCWarning(MESSAGELIST_LOG) << "Failed to load tags " << job->errorString(); return; } Akonadi::TagFetchJob *fetchJob = static_cast(job); KConfigGroup conf(MessageList::MessageListSettings::self()->config(), "MessageListView"); const QString tagSelected = conf.readEntry(QStringLiteral("TagSelected")); if (tagSelected.isEmpty()) { setCurrentStatusFilterItem(); return; } const QStringList tagSelectedLst = tagSelected.split(QLatin1Char(',')); addMessageTagItem(QIcon::fromTheme(QStringLiteral("mail-flag")).pixmap(16, 16), i18nc("Item in list of Akonadi tags, to show all e-mails", "All"), QString()); QStringList tagFound; foreach (const Akonadi::Tag &akonadiTag, fetchJob->tags()) { if (tagSelectedLst.contains(akonadiTag.url().url())) { tagFound.append(akonadiTag.url().url()); QString iconName = QStringLiteral("mail-tagged"); const QString label = akonadiTag.name(); const QString id = akonadiTag.url().url(); Akonadi::TagAttribute *attr = akonadiTag.attribute(); if (attr) { iconName = attr->iconName(); } addMessageTagItem(QIcon::fromTheme(iconName).pixmap(16, 16), label, QVariant(id)); } } conf.writeEntry(QStringLiteral("TagSelected"), tagFound); conf.sync(); setCurrentStatusFilterItem(); } void Widget::viewMessageSelected(MessageList::Core::MessageItem *msg) { int row = -1; if (msg) { row = msg->currentModelIndexRow(); } if (!msg || !msg->isValid() || !storageModel()) { d->mLastSelectedMessage = -1; Q_EMIT messageSelected(Item()); return; } Q_ASSERT(row >= 0); d->mLastSelectedMessage = row; Q_EMIT messageSelected(d->itemForRow(row)); // this MAY be null } void Widget::viewMessageActivated(MessageList::Core::MessageItem *msg) { Q_ASSERT(msg); // must not be null Q_ASSERT(storageModel()); if (!msg->isValid()) { return; } int row = msg->currentModelIndexRow(); Q_ASSERT(row >= 0); // The assert below may fail when quickly opening and closing a non-selected thread. // This will actually activate the item without selecting it... //Q_ASSERT( d->mLastSelectedMessage == row ); if (d->mLastSelectedMessage != row) { // Very ugly. We are activating a non selected message. // This is very likely a double click on the plus sign near a thread leader. // Dealing with mLastSelectedMessage here would be expensive: it would involve releasing the last selected, // emitting signals, handling recursion... ugly. // We choose a very simple solution: double clicking on the plus sign near a thread leader does // NOT activate the message (i.e open it in a toplevel window) if it isn't previously selected. return; } Q_EMIT messageActivated(d->itemForRow(row)); // this MAY be null } void Widget::viewSelectionChanged() { Q_EMIT selectionChanged(); if (!currentMessageItem()) { Q_EMIT messageSelected(Item()); } } void Widget::viewMessageListContextPopupRequest(const QList< MessageList::Core::MessageItem * > &selectedItems, const QPoint &globalPos) { Q_UNUSED(selectedItems); if (!d->mXmlGuiClient) { return; } QMenu *popup = static_cast(d->mXmlGuiClient->factory()->container( QStringLiteral("akonadi_messagelist_contextmenu"), d->mXmlGuiClient)); if (popup) { popup->exec(globalPos); } } void Widget::viewMessageStatusChangeRequest(MessageList::Core::MessageItem *msg, Akonadi::MessageStatus set, Akonadi::MessageStatus clear) { Q_ASSERT(msg); // must not be null Q_ASSERT(storageModel()); if (!msg->isValid()) { return; } int row = msg->currentModelIndexRow(); Q_ASSERT(row >= 0); Item item = d->itemForRow(row); Q_ASSERT(item.isValid()); Q_EMIT messageStatusChangeRequest(item, set, clear); } void Widget::viewGroupHeaderContextPopupRequest(MessageList::Core::GroupHeaderItem *ghi, const QPoint &globalPos) { Q_UNUSED(ghi); QMenu menu(this); QAction *act; QModelIndex index = view()->model()->index(ghi, 0); d->mGroupHeaderItemIndex = index; if (view()->isExpanded(index)) { act = menu.addAction(i18n("Collapse Group")); connect(act, &QAction::triggered, this, &Widget::slotCollapseItem); } else { act = menu.addAction(i18n("Expand Group")); connect(act, &QAction::triggered, this, &Widget::slotExpandItem); } menu.addSeparator(); act = menu.addAction(i18n("Expand All Groups")); connect(act, &QAction::triggered, view(), &Core::View::slotExpandAllGroups); act = menu.addAction(i18n("Collapse All Groups")); connect(act, &QAction::triggered, view(), &Core::View::slotCollapseAllGroups); menu.exec(globalPos); } void Widget::viewDragEnterEvent(QDragEnterEvent *e) { if (!canAcceptDrag(e)) { e->ignore(); return; } e->accept(); } void Widget::viewDragMoveEvent(QDragMoveEvent *e) { if (!canAcceptDrag(e)) { e->ignore(); return; } e->accept(); } enum DragMode { DragCopy, DragMove, DragCancel }; void Widget::viewDropEvent(QDropEvent *e) { if (!canAcceptDrag(e)) { e->ignore(); return; } QList urls = e->mimeData()->urls(); if (urls.isEmpty()) { qCWarning(MESSAGELIST_LOG) << "Could not decode drag data!"; e->ignore(); return; } e->accept(); int action; if ((e->possibleActions() & Qt::MoveAction) == 0) { // We can't move anyway action = DragCopy; } else { action = DragCancel; int keybstate = QApplication::keyboardModifiers(); if (keybstate & Qt::CTRL) { action = DragCopy; } else if (keybstate & Qt::SHIFT) { action = DragMove; } else { QMenu menu; QAction *moveAction = menu.addAction(QIcon::fromTheme(QStringLiteral("go-jump")), i18n("&Move Here")); QAction *copyAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("&Copy Here")); menu.addSeparator(); menu.addAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("C&ancel")); QAction *menuChoice = menu.exec(QCursor::pos()); if (menuChoice == moveAction) { action = DragMove; } else if (menuChoice == copyAction) { action = DragCopy; } else { action = DragCancel; } } } if (action == DragCancel) { return; } Collection::List collections = static_cast(storageModel())->displayedCollections(); Collection target = collections.at(0); Item::List items; items.reserve(urls.count()); foreach (const QUrl &url, urls) { items << Item::fromUrl(url); } if (action == DragCopy) { new ItemCopyJob(items, target, this); } else if (action == DragMove) { new ItemMoveJob(items, target, this); } } void Widget::viewStartDragRequest() { Collection::List collections = static_cast(storageModel())->displayedCollections(); if (collections.isEmpty()) { return; // no folder here } QList selection = view()->selectionAsMessageItemList(); if (selection.isEmpty()) { return; } bool readOnly = false; foreach (const Collection &c, collections) { // We won't be able to remove items from this collection if ((c.rights() & Collection::CanDeleteItem) == 0) { // So the drag will be read-only readOnly = true; break; } } QList urls; urls.reserve(selection.count()); Q_FOREACH (Core::MessageItem *mi, selection) { const Item i = d->itemForRow(mi->currentModelIndexRow()); QUrl url = i.url(Item::Item::Item::UrlWithMimeType); url.addQueryItem(QStringLiteral("parent"), QString::number(mi->parentCollectionId())); urls << url; } QMimeData *mimeData = new QMimeData; mimeData->setUrls(urls); QDrag *drag = new QDrag(view()->viewport()); drag->setMimeData(mimeData); // Set pixmap QPixmap pixmap; if (selection.size() == 1) { pixmap = QPixmap(DesktopIcon(QStringLiteral("mail-message"), KIconLoader::SizeSmall)); } else { pixmap = QPixmap(DesktopIcon(QStringLiteral("document-multiple"), KIconLoader::SizeSmall)); } // Calculate hotspot (as in Konqueror) if (!pixmap.isNull()) { drag->setHotSpot(QPoint(pixmap.width() / 2, pixmap.height() / 2)); drag->setPixmap(pixmap); } if (readOnly) { drag->exec(Qt::CopyAction); } else { drag->exec(Qt::CopyAction | Qt::MoveAction); } } Item::List Widget::Private::selectionAsItems() const { Item::List res; QList selection = q->view()->selectionAsMessageItemList(); res.reserve(selection.count()); foreach (Core::MessageItem *mi, selection) { Item i = itemForRow(mi->currentModelIndexRow()); Q_ASSERT(i.isValid()); res << i; } return res; } Item Widget::Private::itemForRow(int row) const { return static_cast(q->storageModel())->itemForRow(row); } KMime::Message::Ptr Widget::Private::messageForRow(int row) const { return static_cast(q->storageModel())->messageForRow(row); } Item Widget::currentItem() const { Core::MessageItem *mi = view()->currentMessageItem(); if (mi == Q_NULLPTR) { return Item(); } return d->itemForRow(mi->currentModelIndexRow()); } KMime::Message::Ptr Widget::currentMessage() const { Core::MessageItem *mi = view()->currentMessageItem(); if (mi == Q_NULLPTR) { return KMime::Message::Ptr(); } return d->messageForRow(mi->currentModelIndexRow()); } QList Widget::selectionAsMessageList(bool includeCollapsedChildren) const { QList lstMiPtr; QList lstMi = view()->selectionAsMessageItemList(includeCollapsedChildren); if (lstMi.isEmpty()) { return lstMiPtr; } lstMiPtr.reserve(lstMi.count()); foreach (Core::MessageItem *it, lstMi) { lstMiPtr.append(d->messageForRow(it->currentModelIndexRow())); } return lstMiPtr; } Akonadi::Item::List Widget::selectionAsMessageItemList(bool includeCollapsedChildren) const { Akonadi::Item::List lstMiPtr; QList lstMi = view()->selectionAsMessageItemList(includeCollapsedChildren); if (lstMi.isEmpty()) { return lstMiPtr; } lstMiPtr.reserve(lstMi.count()); foreach (Core::MessageItem *it, lstMi) { lstMiPtr.append(d->itemForRow(it->currentModelIndexRow())); } return lstMiPtr; } QVector Widget::selectionAsMessageItemListId(bool includeCollapsedChildren) const { QVector lstMiPtr; QList lstMi = view()->selectionAsMessageItemList(includeCollapsedChildren); if (lstMi.isEmpty()) { return lstMiPtr; } lstMiPtr.reserve(lstMi.count()); foreach (Core::MessageItem *it, lstMi) { lstMiPtr.append(d->itemForRow(it->currentModelIndexRow()).id()); } return lstMiPtr; } QList Widget::selectionAsListMessageId(bool includeCollapsedChildren) const { QList lstMiPtr; QList lstMi = view()->selectionAsMessageItemList(includeCollapsedChildren); if (lstMi.isEmpty()) { return lstMiPtr; } lstMiPtr.reserve(lstMi.count()); foreach (Core::MessageItem *it, lstMi) { lstMiPtr.append(d->itemForRow(it->currentModelIndexRow()).id()); } return lstMiPtr; } Akonadi::Item::List Widget::currentThreadAsMessageList() const { Akonadi::Item::List lstMiPtr; QList lstMi = view()->currentThreadAsMessageItemList(); if (lstMi.isEmpty()) { return lstMiPtr; } lstMiPtr.reserve(lstMi.count()); foreach (Core::MessageItem *it, lstMi) { lstMiPtr.append(d->itemForRow(it->currentModelIndexRow())); } return lstMiPtr; } -MessageList::Core::QuickSearchLine::SearchOptions Widget::currentOptions() const -{ - return view()->currentOptions(); -} - QList Widget::currentFilterStatus() const { return view()->currentFilterStatus(); } QString Widget::currentFilterSearchString() const { return view()->currentFilterSearchString(); } bool Widget::isThreaded() const { return view()->isThreaded(); } bool Widget::selectionEmpty() const { return view()->selectionEmpty(); } bool Widget::getSelectionStats( Akonadi::Item::List &selectedItems, Akonadi::Item::List &selectedVisibleItems, bool *allSelectedBelongToSameThread, bool includeCollapsedChildren) const { if (!storageModel()) { return false; } selectedItems.clear(); selectedVisibleItems.clear(); QList< Core::MessageItem * > selected = view()->selectionAsMessageItemList(includeCollapsedChildren); Core::MessageItem *topmost = Q_NULLPTR; *allSelectedBelongToSameThread = true; foreach (Core::MessageItem *it, selected) { const Item item = d->itemForRow(it->currentModelIndexRow()); selectedItems.append(item); if (view()->isDisplayedWithParentsExpanded(it)) { selectedVisibleItems.append(item); } if (topmost == Q_NULLPTR) { topmost = (*it).topmostMessage(); } else { if (topmost != (*it).topmostMessage()) { *allSelectedBelongToSameThread = false; } } } return true; } void Widget::deletePersistentSet(MessageList::Core::MessageItemSetReference ref) { view()->deletePersistentSet(ref); } void Widget::markMessageItemsAsAboutToBeRemoved(MessageList::Core::MessageItemSetReference ref, bool bMark) { QList< Core::MessageItem * > lstPersistent = view()->persistentSetCurrentMessageItemList(ref); if (!lstPersistent.isEmpty()) { view()->markMessageItemsAsAboutToBeRemoved(lstPersistent, bMark); } } Akonadi::Item::List Widget::itemListFromPersistentSet(MessageList::Core::MessageItemSetReference ref) { Akonadi::Item::List lstItem; QList< Core::MessageItem * > refList = view()->persistentSetCurrentMessageItemList(ref); if (!refList.isEmpty()) { lstItem.reserve(refList.count()); foreach (Core::MessageItem *it, refList) { lstItem.append(d->itemForRow(it->currentModelIndexRow())); } } return lstItem; } MessageList::Core::MessageItemSetReference Widget::selectionAsPersistentSet(bool includeCollapsedChildren) const { QList lstMi = view()->selectionAsMessageItemList(includeCollapsedChildren); if (lstMi.isEmpty()) { return -1; } return view()->createPersistentSet(lstMi); } MessageList::Core::MessageItemSetReference Widget::currentThreadAsPersistentSet() const { QList lstMi = view()->currentThreadAsMessageItemList(); if (lstMi.isEmpty()) { return -1; } return view()->createPersistentSet(lstMi); } Akonadi::Collection Widget::currentCollection() const { Collection::List collections = static_cast(storageModel())->displayedCollections(); if (collections.size() != 1) { return Akonadi::Collection(); // no folder here or too many (in case we can't decide where the drop will end) } return collections.first(); } void Widget::slotCollapseItem() { view()->setCollapseItem(d->mGroupHeaderItemIndex); } void Widget::slotExpandItem() { view()->setExpandItem(d->mGroupHeaderItemIndex); } diff --git a/messagelist/src/widget.h b/messagelist/src/widget.h index bd72f8d1af..721d495b6b 100644 --- a/messagelist/src/widget.h +++ b/messagelist/src/widget.h @@ -1,425 +1,425 @@ /* Copyright (c) 2009 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __MESSAGELIST_WIDGET_H__ #define __MESSAGELIST_WIDGET_H__ #include #include #include #include #include #include #include #include class KXMLGUIClient; class QWidget; namespace MessageList { /** * The Akonadi specific implementation of the Core::Widget. * * When a KXmlGuiWindow is passed to setXmlGuiClient, the XMLGUI * defined context menu @c akonadi_messagelist_contextmenu is * used if available. * */ class MESSAGELIST_EXPORT Widget : public MessageList::Core::Widget { Q_OBJECT public: /** * Create a new message list widget. */ explicit Widget(QWidget *parent); ~Widget(); /** * Sets the XML GUI client which the view is used in. * * This is needed if you want to use the built-in context menu. * Passing 0 is ok and will disable the builtin context menu. * * @param xmlGuiClient The KXMLGUIClient the view is used in. */ void setXmlGuiClient(KXMLGUIClient *xmlGuiClient); /** * Returns the current message for the list as Akonadi::Item. * May return an invalid Item if there is no current message or no current folder. */ Akonadi::Item currentItem() const; /** * Returns the current message for the list as KMime::Message::Ptr. * May return 0 if there is no current message or no current folder. */ KMime::Message::Ptr currentMessage() const; /** * Returns true if this drag can be accepted by the underlying view */ bool canAcceptDrag(const QDropEvent *e); /** * Selects the next message item in the view. * * messageTypeFilter can be used to restrict the selection to only certain message types. * * existingSelectionBehaviour specifies how the existing selection * is manipulated. It may be cleared, expanded or grown/shrinked. * * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * If loop is true then the "next" algorithm will restart from the beginning * of the list if the end is reached, otherwise it will just stop returning false. */ bool selectNextMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop); /** * Selects the previous message item in the view. * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * * messageTypeFilter can be used to restrict the selection to only certain message types. * * existingSelectionBehaviour specifies how the existing selection * is manipulated. It may be cleared, expanded or grown/shrinked. * * If loop is true then the "previous" algorithm will restart from the end * of the list if the beginning is reached, otherwise it will just stop returning false. */ bool selectPreviousMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop); /** * Focuses the next message item in the view without actually selecting it. * * messageTypeFilter can be used to restrict the selection to only certain message types. * * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * If loop is true then the "next" algorithm will restart from the beginning * of the list if the end is reached, otherwise it will just stop returning false. */ bool focusNextMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem, bool loop); /** * Focuses the previous message item in the view without actually selecting it. * * messageTypeFilter can be used to restrict the selection to only certain message types. * * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * If loop is true then the "previous" algorithm will restart from the end * of the list if the beginning is reached, otherwise it will just stop returning false. */ bool focusPreviousMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem, bool loop); /** * Selects the currently focused message item. May do nothing if the * focused message item is already selected (which is very likely). * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. */ void selectFocusedMessageItem(bool centerItem); /** * Selects the first message item in the view that matches the specified Core::MessageTypeFilter. * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * * If the current view is already loaded then the request will * be satisfied immediately (well... if an unread message exists at all). * If the current view is still loading then the selection of the first * message will be scheduled to be executed when loading terminates. * * So this function doesn't actually guarantee that an unread or new message * was selected when the call returns. Take care :) * * The function returns true if a message was selected and false otherwise. */ bool selectFirstMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem); /** * Selects the last message item in the view that matches the specified Core::MessageTypeFilter. * If centerItem is true then the specified item will be positioned * at the center of the view, if possible. * * The function returns true if a message was selected and false otherwise. */ bool selectLastMessageItem(MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem); /** * Selects all the items in the current folder. */ void selectAll(); /** * If expand is true then it expands the current thread, otherwise * collapses it. */ void setCurrentThreadExpanded(bool expand); /** * If expand is true then it expands all the threads, otherwise * collapses them. */ void setAllThreadsExpanded(bool expand); /** * If expand is true then it expands all the groups (only the toplevel * group item: inner threads are NOT expanded). If expand is false * then it collapses all the groups. If no grouping is in effect * then this function does nothing. */ void setAllGroupsExpanded(bool expand); /** * Sets the focus on the quick search line of the currently active tab. */ void focusQuickSearch(const QString &selectedText); /** * Returns the currently selected KMime::Message::Ptr (bound to current StorageModel). * The list may be empty if there are no selected messages or no StorageModel. * * If includeCollapsedChildren is true then the children of the selected but * collapsed items are also added to the list. * * The returned list is guaranteed to be valid only until you return control * to the main even loop. Don't store it for any longer. If you need to reference * this set of messages at a later stage then take a look at createPersistentSet(). */ QList selectionAsMessageList(bool includeCollapsedChildren = true) const; /** * Returns the currently selected Items (bound to current StorageModel). * The list may be empty if there are no selected messages or no StorageModel. * * If includeCollapsedChildren is true then the children of the selected but * collapsed items are also added to the list. * * The returned list is guaranteed to be valid only until you return control * to the main even loop. Don't store it for any longer. If you need to reference * this set of messages at a later stage then take a look at createPersistentSet(). */ Akonadi::Item::List selectionAsMessageItemList(bool includeCollapsedChildren = true) const; /** * Returns the currently selected Items id (bound to current StorageModel). * The list may be empty if there are no selected messages or no StorageModel. * * If includeCollapsedChildren is true then the children of the selected but * collapsed items are also added to the list. * * The returned list is guaranteed to be valid only until you return control * to the main even loop. Don't store it for any longer. If you need to reference * this set of messages at a later stage then take a look at createPersistentSet(). */ QVector selectionAsMessageItemListId(bool includeCollapsedChildren) const; QList selectionAsListMessageId(bool includeCollapsedChildren) const; /** * Returns the Akonadi::Item bound to the current StorageModel that * are part of the current thread. The current thread is the thread * that contains currentMessageItem(). * The list may be empty if there is no currentMessageItem() or no StorageModel. * * The returned list is guaranteed to be valid only until you return control * to the main even loop. Don't store it for any longer. If you need to reference * this set of messages at a later stage then take a look at createPersistentSet(). */ Akonadi::Item::List currentThreadAsMessageList() const; /** * Returns the Akonadi::MessageStatus in the current quicksearch field. */ QList currentFilterStatus() const; /** * Returns the search term in the current quicksearch field. */ QString currentFilterSearchString() const; /** * Returns true if the current Aggregation is threaded, false otherwise * (or if there is no current Aggregation). */ bool isThreaded() const; /** * Fast function that determines if the selection is empty */ bool selectionEmpty() const; /** * Fills the lists of the selected message serial numbers and of the selected+visible ones. * Returns true if the returned stats are valid (there is a current folder after all) * and false otherwise. This is called by KMMainWidget in a single place so we optimize by * making it a single sweep on the selection. * * If includeCollapsedChildren is true then the children of the selected but * collapsed items are also included in the stats */ bool getSelectionStats(Akonadi::Item::List &selectedSernums, Akonadi::Item::List &selectedVisibleSernums, bool *allSelectedBelongToSameThread, bool includeCollapsedChildren = true) const; /** * Deletes the persistent set pointed by the specified reference. * If the set does not exist anymore, nothing happens. */ void deletePersistentSet(MessageList::Core::MessageItemSetReference ref); /** * If bMark is true this function marks the messages as "about to be removed" * so they appear dimmer and aren't selectable in the view. * If bMark is false then this function clears the "about to be removed" state * for the specified MessageItems. */ void markMessageItemsAsAboutToBeRemoved(MessageList::Core::MessageItemSetReference ref, bool bMark); /** * Return Akonadi::Item from messageItemReference */ Akonadi::Item::List itemListFromPersistentSet(MessageList::Core::MessageItemSetReference ref); /** * Return a persistent set from current selection */ MessageList::Core::MessageItemSetReference selectionAsPersistentSet(bool includeCollapsedChildren = true) const; /** * Return a persistent set from current thread */ MessageList::Core::MessageItemSetReference currentThreadAsPersistentSet() const; Akonadi::Collection currentCollection() const; void setQuickSearchClickMessage(const QString &msg); - MessageList::Core::QuickSearchLine::SearchOptions currentOptions() const; + protected: /** * Reimplemented from MessageList::Core::Widget */ void fillMessageTagCombo() Q_DECL_OVERRIDE; /** * Reimplemented from MessageList::Core::Widget */ void viewMessageSelected(MessageList::Core::MessageItem *msg) Q_DECL_OVERRIDE; /** * Reimplemented from MessageList::Core::Widget */ void viewMessageActivated(MessageList::Core::MessageItem *msg) Q_DECL_OVERRIDE; /** * Reimplemented from MessageList::Core::Widget */ void viewSelectionChanged() Q_DECL_OVERRIDE; /** * Reimplemented from MessageList::Core::Widget */ void viewMessageListContextPopupRequest(const QList< MessageList::Core::MessageItem * > &selectedItems, const QPoint &globalPos) Q_DECL_OVERRIDE; /** * Reimplemented from MessageList::Core::Widget */ void viewGroupHeaderContextPopupRequest(MessageList::Core::GroupHeaderItem *group, const QPoint &globalPos) Q_DECL_OVERRIDE; /** * Reimplemented from MessageList::Core::Widget */ void viewDragEnterEvent(QDragEnterEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented from MessageList::Core::Widget */ void viewDragMoveEvent(QDragMoveEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented from MessageList::Core::Widget */ void viewDropEvent(QDropEvent *e) Q_DECL_OVERRIDE; /** * Reimplemented from MessageList::Core::Widget */ void viewStartDragRequest() Q_DECL_OVERRIDE; /** * Reimplemented from MessageList::Core::Widget */ void viewMessageStatusChangeRequest(MessageList::Core::MessageItem *msg, Akonadi::MessageStatus set, Akonadi::MessageStatus clear) Q_DECL_OVERRIDE; private Q_SLOTS: void slotCollapseItem(); void slotExpandItem(); void slotTagsFetched(KJob *job); Q_SIGNALS: /** * Emitted when a message is selected (that is, single clicked and thus made current in the view) * Note that this message CAN be 0 (when the current item is cleared, for example). * * This signal is emitted when a SINGLE message is selected in the view, probably * by clicking on it or by simple keyboard navigation. When multiple items * are selected at once (by shift+clicking, for example) then you will get * this signal only for the last clicked message (or at all, if the last shift+clicked * thing is a group header...). You should handle selection changed in this case. */ void messageSelected(const Akonadi::Item &item); /** * Emitted when a message is doubleclicked or activated by other input means */ void messageActivated(const Akonadi::Item &item); /** * Emitted when the selection in the view changes. */ void selectionChanged(); /** * Emitted when a message wants its status to be changed */ void messageStatusChangeRequest(const Akonadi::Item &item, const Akonadi::MessageStatus &set, const Akonadi::MessageStatus &clear); private: class Private; Private *const d; }; } // namespace MessageList #endif //!__MESSAGELIST_WIDGET_H__