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