1 /*
2 
3   SPDX-FileCopyrightText: 2009-2021 Laurent Montel <montel@kde.org>
4 
5   SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "foldertreewidget.h"
9 #include "entitycollectionorderproxymodel.h"
10 #include "foldertreeview.h"
11 #include "hierarchicalfoldermatcher_p.h"
12 #include "kernel/mailkernel.h"
13 #include "util/mailutil.h"
14 
15 #include <PimCommon/PimUtil>
16 #include <PimCommonAkonadi/ImapAclAttribute>
17 
18 #include <Akonadi/AttributeFactory>
19 #include <Akonadi/ChangeRecorder>
20 #include <Akonadi/EntityMimeTypeFilterModel>
21 #include <Akonadi/EntityTreeModel>
22 #include <Akonadi/ItemFetchScope>
23 #include <Akonadi/StatisticsProxyModel>
24 
25 #include <Akonadi/ETMViewStateSaver>
26 #include <Akonadi/EntityTreeView>
27 
28 #include <KMime/Message>
29 
30 #include <MessageCore/MessageCoreSettings>
31 
32 #include <KLocalizedString>
33 
34 #include <QFontDatabase>
35 #include <QHeaderView>
36 #include <QKeyEvent>
37 #include <QLabel>
38 #include <QLineEdit>
39 #include <QPointer>
40 #include <QVBoxLayout>
41 
42 namespace MailCommon
43 {
44 class Q_DECL_HIDDEN FolderTreeWidget::FolderTreeWidgetPrivate
45 {
46 public:
47     QString filter;
48     QString oldFilterStr;
49     Akonadi::StatisticsProxyModel *filterModel = nullptr;
50     FolderTreeView *folderTreeView = nullptr;
51     FolderTreeWidgetProxyModel *readableproxy = nullptr;
52     EntityCollectionOrderProxyModel *entityOrderProxy = nullptr;
53     QLineEdit *filterFolderLineEdit = nullptr;
54     QPointer<Akonadi::ETMViewStateSaver> saver;
55     QStringList expandedItems;
56     QString currentItem;
57     QLabel *label = nullptr;
58     bool dontKeyFilter = false;
59 };
60 
FolderTreeWidget(QWidget * parent,KXMLGUIClient * xmlGuiClient,FolderTreeWidget::TreeViewOptions options,FolderTreeWidgetProxyModel::FolderTreeWidgetProxyModelOptions optReadableProxy)61 FolderTreeWidget::FolderTreeWidget(QWidget *parent,
62                                    KXMLGUIClient *xmlGuiClient,
63                                    FolderTreeWidget::TreeViewOptions options,
64                                    FolderTreeWidgetProxyModel::FolderTreeWidgetProxyModelOptions optReadableProxy)
65     : QWidget(parent)
66     , d(new FolderTreeWidgetPrivate())
67 {
68     Akonadi::AttributeFactory::registerAttribute<PimCommon::ImapAclAttribute>();
69 
70     d->folderTreeView = new FolderTreeView(xmlGuiClient, this, options & ShowUnreadCount);
71     d->folderTreeView->showStatisticAnimation(options & ShowCollectionStatisticAnimation);
72 
73     connect(d->folderTreeView, &FolderTreeView::manualSortingChanged, this, &FolderTreeWidget::slotManualSortingChanged);
74 
75     auto lay = new QVBoxLayout(this);
76     lay->setContentsMargins({});
77 
78     d->label = new QLabel(i18n("You can start typing to filter the list of folders."), this);
79     lay->addWidget(d->label);
80 
81     d->filterFolderLineEdit = new QLineEdit(this);
82 
83     d->filterFolderLineEdit->setClearButtonEnabled(true);
84     d->filterFolderLineEdit->setPlaceholderText(i18nc("@info Displayed grayed-out inside the textbox, verb to search", "Search"));
85     lay->addWidget(d->filterFolderLineEdit);
86 
87     if (!(options & HideStatistics)) {
88         d->filterModel = new Akonadi::StatisticsProxyModel(this);
89         d->filterModel->setSourceModel(KernelIf->collectionModel());
90     }
91     if (options & HideHeaderViewMenu) {
92         d->folderTreeView->header()->setContextMenuPolicy(Qt::NoContextMenu);
93     }
94 
95     d->readableproxy = new FolderTreeWidgetProxyModel(this, optReadableProxy);
96     d->readableproxy->setSourceModel((options & HideStatistics) ? static_cast<QAbstractItemModel *>(KernelIf->collectionModel())
97                                                                 : static_cast<QAbstractItemModel *>(d->filterModel));
98     d->readableproxy->addContentMimeTypeInclusionFilter(KMime::Message::mimeType());
99 
100     connect(d->folderTreeView, &FolderTreeView::changeTooltipsPolicy, this, &FolderTreeWidget::slotChangeTooltipsPolicy);
101 
102     d->folderTreeView->setSelectionMode(QAbstractItemView::SingleSelection);
103     d->folderTreeView->setEditTriggers(QAbstractItemView::NoEditTriggers);
104     d->folderTreeView->installEventFilter(this);
105 
106     // Order proxy
107     d->entityOrderProxy = new EntityCollectionOrderProxyModel(this);
108     d->entityOrderProxy->setSourceModel(d->readableproxy);
109     d->entityOrderProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
110     KConfigGroup grp(KernelIf->config(), "CollectionTreeOrder");
111     d->entityOrderProxy->setOrderConfig(grp);
112     d->folderTreeView->setModel(d->entityOrderProxy);
113 
114     if (options & UseDistinctSelectionModel) {
115         d->folderTreeView->setSelectionModel(new QItemSelectionModel(d->entityOrderProxy, this));
116     }
117 
118     lay->addWidget(d->folderTreeView);
119 
120     d->dontKeyFilter = (options & DontKeyFilter);
121 
122     if ((options & UseLineEditForFiltering)) {
123         connect(d->filterFolderLineEdit, &QLineEdit::textChanged, this, &FolderTreeWidget::slotFilterFixedString);
124         d->label->hide();
125     } else {
126         d->filterFolderLineEdit->hide();
127         setAttribute(Qt::WA_InputMethodEnabled);
128     }
129 }
130 
131 FolderTreeWidget::~FolderTreeWidget() = default;
132 
slotFilterFixedString(const QString & text)133 void FolderTreeWidget::slotFilterFixedString(const QString &text)
134 {
135     delete d->saver;
136     if (d->oldFilterStr.isEmpty()) {
137         // Save it.
138         Akonadi::ETMViewStateSaver saver;
139         saver.setView(folderTreeView());
140         d->expandedItems = saver.expansionKeys();
141         d->currentItem = saver.currentIndexKey();
142     } else if (text.isEmpty()) {
143         d->saver = new Akonadi::ETMViewStateSaver;
144         d->saver->setView(folderTreeView());
145         QString currentIndex = d->saver->currentIndexKey();
146         if (d->saver->selectionKeys().isEmpty()) {
147             currentIndex = d->currentItem;
148         } else if (!currentIndex.isEmpty()) {
149             d->expandedItems << currentIndex;
150         }
151         d->saver->restoreExpanded(d->expandedItems);
152         d->saver->restoreCurrentItem(currentIndex);
153     } else {
154         d->folderTreeView->expandAll();
155     }
156     d->oldFilterStr = text;
157     d->entityOrderProxy->setFilterWildcard(text);
158 }
159 
disableContextMenuAndExtraColumn()160 void FolderTreeWidget::disableContextMenuAndExtraColumn()
161 {
162     d->folderTreeView->disableContextMenuAndExtraColumn();
163 }
164 
selectCollectionFolder(const Akonadi::Collection & collection,bool expand)165 void FolderTreeWidget::selectCollectionFolder(const Akonadi::Collection &collection, bool expand)
166 {
167     const QModelIndex index = Akonadi::EntityTreeModel::modelIndexForCollection(d->folderTreeView->model(), collection);
168 
169     d->folderTreeView->setCurrentIndex(index);
170     if (expand) {
171         d->folderTreeView->setExpanded(index, true);
172     }
173     d->folderTreeView->scrollTo(index);
174 }
175 
setSelectionMode(QAbstractItemView::SelectionMode mode)176 void FolderTreeWidget::setSelectionMode(QAbstractItemView::SelectionMode mode)
177 {
178     d->folderTreeView->setSelectionMode(mode);
179 }
180 
selectionMode() const181 QAbstractItemView::SelectionMode FolderTreeWidget::selectionMode() const
182 {
183     return d->folderTreeView->selectionMode();
184 }
185 
selectionModel() const186 QItemSelectionModel *FolderTreeWidget::selectionModel() const
187 {
188     return d->folderTreeView->selectionModel();
189 }
190 
currentIndex() const191 QModelIndex FolderTreeWidget::currentIndex() const
192 {
193     return d->folderTreeView->currentIndex();
194 }
195 
selectedCollection() const196 Akonadi::Collection FolderTreeWidget::selectedCollection() const
197 {
198     if (d->folderTreeView->selectionMode() == QAbstractItemView::SingleSelection) {
199         Akonadi::Collection::List lstCollection = selectedCollections();
200         if (lstCollection.isEmpty()) {
201             return Akonadi::Collection();
202         } else {
203             return lstCollection.at(0);
204         }
205     }
206 
207     return Akonadi::Collection();
208 }
209 
selectedCollections() const210 Akonadi::Collection::List FolderTreeWidget::selectedCollections() const
211 {
212     Akonadi::Collection::List collections;
213     const QItemSelectionModel *selectionModel = d->folderTreeView->selectionModel();
214     const QModelIndexList selectedIndexes = selectionModel->selectedIndexes();
215     for (const QModelIndex &index : selectedIndexes) {
216         if (index.isValid()) {
217             const auto collection = index.model()->data(index, Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
218             if (collection.isValid()) {
219                 collections.append(collection);
220             }
221         }
222     }
223 
224     return collections;
225 }
226 
folderTreeView() const227 FolderTreeView *FolderTreeWidget::folderTreeView() const
228 {
229     return d->folderTreeView;
230 }
231 
slotGeneralFontChanged()232 void FolderTreeWidget::slotGeneralFontChanged()
233 {
234     // Custom/System font support
235     if (MessageCore::MessageCoreSettings::self()->useDefaultFonts()) {
236         setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
237     }
238 }
239 
slotGeneralPaletteChanged()240 void FolderTreeWidget::slotGeneralPaletteChanged()
241 {
242     d->readableproxy->updatePalette();
243     d->folderTreeView->updatePalette();
244 }
245 
readConfig()246 void FolderTreeWidget::readConfig()
247 {
248     setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
249 
250     d->folderTreeView->readConfig();
251     d->folderTreeView->setDropActionMenuEnabled(SettingsIf->showPopupAfterDnD());
252     d->readableproxy->setWarningThreshold(SettingsIf->closeToQuotaThreshold());
253     d->readableproxy->readConfig();
254 
255     KConfigGroup readerConfig(KernelIf->config(), "AccountOrder");
256     QStringList listOrder;
257     if (readerConfig.readEntry("EnableAccountOrder", true)) {
258         listOrder = readerConfig.readEntry("order", QStringList());
259     }
260     d->entityOrderProxy->setTopLevelOrder(listOrder);
261 }
262 
restoreHeaderState(const QByteArray & data)263 void FolderTreeWidget::restoreHeaderState(const QByteArray &data)
264 {
265     d->folderTreeView->restoreHeaderState(data);
266 }
267 
slotChangeTooltipsPolicy(FolderTreeWidget::ToolTipDisplayPolicy policy)268 void FolderTreeWidget::slotChangeTooltipsPolicy(FolderTreeWidget::ToolTipDisplayPolicy policy)
269 {
270     changeToolTipsPolicyConfig(policy);
271 }
272 
changeToolTipsPolicyConfig(ToolTipDisplayPolicy policy)273 void FolderTreeWidget::changeToolTipsPolicyConfig(ToolTipDisplayPolicy policy)
274 {
275     switch (policy) {
276     case DisplayAlways:
277     case DisplayWhenTextElided: // Need to implement in the future
278         if (d->filterModel) {
279             d->filterModel->setToolTipEnabled(true);
280         }
281         break;
282     case DisplayNever:
283         if (d->filterModel) {
284             d->filterModel->setToolTipEnabled(false);
285         }
286     }
287     d->folderTreeView->setTooltipsPolicy(policy);
288 }
289 
statisticsProxyModel() const290 Akonadi::StatisticsProxyModel *FolderTreeWidget::statisticsProxyModel() const
291 {
292     return d->filterModel;
293 }
294 
folderTreeWidgetProxyModel() const295 FolderTreeWidgetProxyModel *FolderTreeWidget::folderTreeWidgetProxyModel() const
296 {
297     return d->readableproxy;
298 }
299 
entityOrderProxy() const300 EntityCollectionOrderProxyModel *FolderTreeWidget::entityOrderProxy() const
301 {
302     return d->entityOrderProxy;
303 }
304 
filterFolderLineEdit() const305 QLineEdit *FolderTreeWidget::filterFolderLineEdit() const
306 {
307     return d->filterFolderLineEdit;
308 }
309 
applyFilter(const QString & filter)310 void FolderTreeWidget::applyFilter(const QString &filter)
311 {
312     d->label->setText(filter.isEmpty() ? i18n("You can start typing to filter the list of folders.") : i18n("Path: (%1)", filter));
313 
314     HierarchicalFolderMatcher matcher;
315     matcher.setFilter(filter, d->entityOrderProxy->filterCaseSensitivity());
316     d->entityOrderProxy->setFolderMatcher(matcher);
317     d->folderTreeView->expandAll();
318     const QAbstractItemModel *const model = d->folderTreeView->model();
319     const QModelIndex current = d->folderTreeView->currentIndex();
320     const QModelIndex start = current.isValid() ? current : model->index(0, 0);
321     const QModelIndex firstMatch = matcher.findFirstMatch(model, start);
322     if (firstMatch.isValid()) {
323         d->folderTreeView->setCurrentIndex(firstMatch);
324         d->folderTreeView->scrollTo(firstMatch);
325     }
326 }
327 
clearFilter()328 void FolderTreeWidget::clearFilter()
329 {
330     d->filter.clear();
331     applyFilter(d->filter);
332     const QModelIndexList lst = d->folderTreeView->selectionModel()->selectedIndexes();
333     if (!lst.isEmpty()) {
334         d->folderTreeView->scrollTo(lst.first());
335     }
336 }
337 
slotManualSortingChanged(bool active)338 void FolderTreeWidget::slotManualSortingChanged(bool active)
339 {
340     d->entityOrderProxy->setManualSortingActive(active);
341     d->folderTreeView->setManualSortingActive(active);
342 }
343 
eventFilter(QObject * o,QEvent * e)344 bool FolderTreeWidget::eventFilter(QObject *o, QEvent *e)
345 {
346     Q_UNUSED(o)
347     if (d->dontKeyFilter) {
348         return false;
349     }
350 
351     if (e->type() == QEvent::KeyPress) {
352         const QKeyEvent *const ke = static_cast<QKeyEvent *>(e);
353         switch (ke->key()) {
354         case Qt::Key_Backspace: {
355             const int filterLength(d->filter.length());
356             if (filterLength > 0) {
357                 d->filter.truncate(filterLength - 1);
358                 applyFilter(d->filter);
359             }
360             return false;
361         }
362         case Qt::Key_Delete:
363             d->filter.clear();
364             applyFilter(d->filter);
365             return false;
366         default: {
367             const QString s = ke->text();
368             if (!s.isEmpty() && s.at(0).isPrint()) {
369                 d->filter += s;
370                 applyFilter(d->filter);
371                 return false;
372             }
373             break;
374         }
375         }
376     } else if (e->type() == QEvent::InputMethod) {
377         const QInputMethodEvent *const ime = static_cast<QInputMethodEvent *>(e);
378         d->filter += ime->commitString();
379         applyFilter(d->filter);
380         return false;
381     }
382     return false;
383 }
384 }
385