1 /*
2    SPDX-FileCopyrightText: 2003 Scott Wheeler <wheeler@kde.org>
3    SPDX-FileCopyrightText: 2005 Rafal Rzepecki <divide@users.sourceforge.net>
4    SPDX-FileCopyrightText: 2006 Hamish Rodda <rodda@kde.org>
5    SPDX-FileCopyrightText: 2007 Pino Toscano <pino@kde.org>
6 
7    SPDX-License-Identifier: LGPL-2.0-only
8 */
9 
10 #include "ktreeviewsearchline.h"
11 
12 #include <QApplication>
13 #include <QContextMenuEvent>
14 #include <QHBoxLayout>
15 #include <QHeaderView>
16 #include <QLabel>
17 #include <QList>
18 #include <QMenu>
19 #include <QTimer>
20 #include <QToolButton>
21 #include <QTreeView>
22 
23 #include <QDebug>
24 #include <klocalizedstring.h>
25 
26 class KTreeViewSearchLinePrivate
27 {
28 public:
KTreeViewSearchLinePrivate(KTreeViewSearchLine * _parent)29     KTreeViewSearchLinePrivate(KTreeViewSearchLine *_parent)
30         : parent(_parent)
31         , caseSensitive(Qt::CaseInsensitive)
32         , activeSearch(false)
33         , keepParentsVisible(true)
34         , canChooseColumns(true)
35         , queuedSearches(0)
36     {
37     }
38 
39     KTreeViewSearchLine *parent;
40     QList<QTreeView *> treeViews;
41     Qt::CaseSensitivity caseSensitive;
42     bool activeSearch;
43     bool keepParentsVisible;
44     bool canChooseColumns;
45     QString search;
46     int queuedSearches;
47     QList<int> searchColumns;
48 
49     void rowsInserted(QAbstractItemModel *model, const QModelIndex &parent, int start, int end) const;
50     void treeViewDeleted(QObject *treeView);
51     void slotColumnActivated(QAction *action);
52     void slotAllVisibleColumns();
53 
54     void checkColumns();
55     void checkItemParentsNotVisible(QTreeView *treeView);
56     bool checkItemParentsVisible(QTreeView *treeView, const QModelIndex &index);
57 };
58 
59 ////////////////////////////////////////////////////////////////////////////////
60 // private slots
61 ////////////////////////////////////////////////////////////////////////////////
rowsInserted(const QModelIndex & parentIndex,int start,int end) const62 void KTreeViewSearchLine::rowsInserted(const QModelIndex &parentIndex, int start, int end) const
63 {
64     QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(sender());
65     d->rowsInserted(model, parentIndex, start, end);
66 }
67 
rowsInserted(QAbstractItemModel * model,const QModelIndex & parentIndex,int start,int end) const68 void KTreeViewSearchLinePrivate::rowsInserted(QAbstractItemModel *model, const QModelIndex &parentIndex, int start, int end) const
69 {
70     // QAbstractItemModel* model = qobject_cast<QAbstractItemModel*>( parent->sender() );
71     if (!model) {
72         return;
73     }
74 
75     QTreeView *widget = nullptr;
76     foreach (QTreeView *tree, treeViews)
77         if (tree->model() == model) {
78             widget = tree;
79             break;
80         }
81 
82     if (!widget) {
83         return;
84     }
85 
86     for (int i = start; i <= end; ++i) {
87         widget->setRowHidden(i, parentIndex, !parent->itemMatches(parentIndex, i, parent->text()));
88     }
89 }
90 
treeViewDeleted(QObject * object)91 void KTreeViewSearchLinePrivate::treeViewDeleted(QObject *object)
92 {
93     treeViews.removeAll(static_cast<QTreeView *>(object));
94     parent->setEnabled(treeViews.isEmpty());
95 }
96 
slotColumnActivated(QAction * action)97 void KTreeViewSearchLinePrivate::slotColumnActivated(QAction *action)
98 {
99     if (!action) {
100         return;
101     }
102 
103     bool ok;
104     int column = action->data().toInt(&ok);
105 
106     if (!ok) {
107         return;
108     }
109 
110     if (action->isChecked()) {
111         if (!searchColumns.isEmpty()) {
112             if (!searchColumns.contains(column)) {
113                 searchColumns.append(column);
114             }
115 
116             if (searchColumns.count() == treeViews.first()->header()->count() - treeViews.first()->header()->hiddenSectionCount()) {
117                 searchColumns.clear();
118             }
119 
120         } else {
121             searchColumns.append(column);
122         }
123     } else {
124         if (searchColumns.isEmpty()) {
125             QHeaderView *const header = treeViews.first()->header();
126 
127             for (int i = 0; i < header->count(); i++) {
128                 if (i != column && !header->isSectionHidden(i)) {
129                     searchColumns.append(i);
130                 }
131             }
132 
133         } else if (searchColumns.contains(column)) {
134             searchColumns.removeAll(column);
135         }
136     }
137 
138     parent->updateSearch();
139 }
140 
slotAllVisibleColumns()141 void KTreeViewSearchLinePrivate::slotAllVisibleColumns()
142 {
143     if (searchColumns.isEmpty()) {
144         searchColumns.append(0);
145     } else {
146         searchColumns.clear();
147     }
148 
149     parent->updateSearch();
150 }
151 
152 ////////////////////////////////////////////////////////////////////////////////
153 // private methods
154 ////////////////////////////////////////////////////////////////////////////////
155 
checkColumns()156 void KTreeViewSearchLinePrivate::checkColumns()
157 {
158     canChooseColumns = parent->canChooseColumnsCheck();
159 }
160 
checkItemParentsNotVisible(QTreeView * treeView)161 void KTreeViewSearchLinePrivate::checkItemParentsNotVisible(QTreeView *treeView)
162 {
163     Q_UNUSED(treeView)
164 
165 // TODO: PORT ME
166 #if 0
167   QTreeWidgetItemIterator it( treeWidget );
168 
169   for ( ; *it; ++it ) {
170     QTreeWidgetItem *item = *it;
171     item->treeWidget()->setItemHidden( item, !parent->itemMatches( item, search ) );
172   }
173 #endif
174 }
175 
176 /** Check whether \p item, its siblings and their descendents should be shown. Show or hide the items as necessary.
177  *
178  *  \p item  The list view item to start showing / hiding items at. Typically, this is the first child of another item, or the
179  *              the first child of the list view.
180  *  \return \c true if an item which should be visible is found, \c false if all items found should be hidden. If this function
181  *             returns true and \p highestHiddenParent was not 0, highestHiddenParent will have been shown.
182  */
checkItemParentsVisible(QTreeView * treeView,const QModelIndex & index)183 bool KTreeViewSearchLinePrivate::checkItemParentsVisible(QTreeView *treeView, const QModelIndex &index)
184 {
185     bool childMatch = false;
186     const int rowcount = treeView->model()->rowCount(index);
187     for (int i = 0; i < rowcount; ++i) {
188         childMatch |= checkItemParentsVisible(treeView, treeView->model()->index(i, 0, index));
189     }
190 
191     // Should this item be shown? It should if any children should be, or if it matches.
192     const QModelIndex parentindex = index.parent();
193     if (childMatch || parent->itemMatches(parentindex, index.row(), search)) {
194         treeView->setRowHidden(index.row(), parentindex, false);
195         return true;
196     }
197 
198     treeView->setRowHidden(index.row(), parentindex, true);
199 
200     return false;
201 }
202 
203 ////////////////////////////////////////////////////////////////////////////////
204 // public methods
205 ////////////////////////////////////////////////////////////////////////////////
206 
KTreeViewSearchLine(QWidget * parent,QTreeView * treeView)207 KTreeViewSearchLine::KTreeViewSearchLine(QWidget *parent, QTreeView *treeView)
208     : KLineEdit(parent)
209     , d(new KTreeViewSearchLinePrivate(this))
210 {
211     connect(this, &QLineEdit::textChanged, this, &KTreeViewSearchLine::queueSearch);
212 
213     setClearButtonEnabled(true);
214     setTreeView(treeView);
215 
216     if (!treeView) {
217         setEnabled(false);
218     }
219 }
220 
KTreeViewSearchLine(QWidget * parent,const QList<QTreeView * > & treeViews)221 KTreeViewSearchLine::KTreeViewSearchLine(QWidget *parent, const QList<QTreeView *> &treeViews)
222     : KLineEdit(parent)
223     , d(new KTreeViewSearchLinePrivate(this))
224 {
225     connect(this, &QLineEdit::textChanged, this, &KTreeViewSearchLine::queueSearch);
226 
227     setClearButtonEnabled(true);
228     setTreeViews(treeViews);
229 }
230 
~KTreeViewSearchLine()231 KTreeViewSearchLine::~KTreeViewSearchLine()
232 {
233     delete d;
234 }
235 
caseSensitivity() const236 Qt::CaseSensitivity KTreeViewSearchLine::caseSensitivity() const
237 {
238     return d->caseSensitive;
239 }
240 
searchColumns() const241 QList<int> KTreeViewSearchLine::searchColumns() const
242 {
243     if (d->canChooseColumns) {
244         return d->searchColumns;
245     } else {
246         return QList<int>();
247     }
248 }
249 
keepParentsVisible() const250 bool KTreeViewSearchLine::keepParentsVisible() const
251 {
252     return d->keepParentsVisible;
253 }
254 
treeView() const255 QTreeView *KTreeViewSearchLine::treeView() const
256 {
257     if (d->treeViews.count() == 1) {
258         return d->treeViews.first();
259     } else {
260         return nullptr;
261     }
262 }
263 
treeViews() const264 QList<QTreeView *> KTreeViewSearchLine::treeViews() const
265 {
266     return d->treeViews;
267 }
268 
269 ////////////////////////////////////////////////////////////////////////////////
270 // public slots
271 ////////////////////////////////////////////////////////////////////////////////
272 
addTreeView(QTreeView * treeView)273 void KTreeViewSearchLine::addTreeView(QTreeView *treeView)
274 {
275     if (treeView) {
276         connectTreeView(treeView);
277 
278         d->treeViews.append(treeView);
279         setEnabled(!d->treeViews.isEmpty());
280 
281         d->checkColumns();
282     }
283 }
284 
removeTreeView(QTreeView * treeView)285 void KTreeViewSearchLine::removeTreeView(QTreeView *treeView)
286 {
287     if (treeView) {
288         int index = d->treeViews.indexOf(treeView);
289 
290         if (index != -1) {
291             d->treeViews.removeAt(index);
292             d->checkColumns();
293 
294             disconnectTreeView(treeView);
295 
296             setEnabled(!d->treeViews.isEmpty());
297         }
298     }
299 }
300 
updateSearch(const QString & pattern)301 void KTreeViewSearchLine::updateSearch(const QString &pattern)
302 {
303     d->search = pattern.isNull() ? text() : pattern;
304 
305     foreach (QTreeView *treeView, d->treeViews)
306         updateSearch(treeView);
307 }
308 
updateSearch(QTreeView * treeView)309 void KTreeViewSearchLine::updateSearch(QTreeView *treeView)
310 {
311     if (!treeView || !treeView->model()->rowCount()) {
312         return;
313     }
314 
315     // If there's a selected item that is visible, make sure that it's visible
316     // when the search changes too (assuming that it still matches).
317 
318     QModelIndex currentIndex = treeView->currentIndex();
319 
320     bool wasUpdateEnabled = treeView->updatesEnabled();
321     treeView->setUpdatesEnabled(false);
322     if (d->keepParentsVisible) {
323         for (int i = 0; i < treeView->model()->rowCount(); ++i) {
324             d->checkItemParentsVisible(treeView, treeView->rootIndex());
325         }
326     } else {
327         d->checkItemParentsNotVisible(treeView);
328     }
329     treeView->setUpdatesEnabled(wasUpdateEnabled);
330 
331     if (currentIndex.isValid()) {
332         treeView->scrollTo(currentIndex);
333     }
334 }
335 
setCaseSensitivity(Qt::CaseSensitivity caseSensitive)336 void KTreeViewSearchLine::setCaseSensitivity(Qt::CaseSensitivity caseSensitive)
337 {
338     if (d->caseSensitive != caseSensitive) {
339         d->caseSensitive = caseSensitive;
340         updateSearch();
341     }
342 }
343 
setKeepParentsVisible(bool visible)344 void KTreeViewSearchLine::setKeepParentsVisible(bool visible)
345 {
346     if (d->keepParentsVisible != visible) {
347         d->keepParentsVisible = visible;
348         updateSearch();
349     }
350 }
351 
setSearchColumns(const QList<int> & columns)352 void KTreeViewSearchLine::setSearchColumns(const QList<int> &columns)
353 {
354     if (d->canChooseColumns) {
355         d->searchColumns = columns;
356     }
357 }
358 
setTreeView(QTreeView * treeView)359 void KTreeViewSearchLine::setTreeView(QTreeView *treeView)
360 {
361     setTreeViews(QList<QTreeView *>());
362     addTreeView(treeView);
363 }
364 
setTreeViews(const QList<QTreeView * > & treeViews)365 void KTreeViewSearchLine::setTreeViews(const QList<QTreeView *> &treeViews)
366 {
367     foreach (QTreeView *treeView, d->treeViews)
368         disconnectTreeView(treeView);
369 
370     d->treeViews = treeViews;
371 
372     foreach (QTreeView *treeView, d->treeViews)
373         connectTreeView(treeView);
374 
375     d->checkColumns();
376 
377     setEnabled(!d->treeViews.isEmpty());
378 }
379 
380 ////////////////////////////////////////////////////////////////////////////////
381 // protected members
382 ////////////////////////////////////////////////////////////////////////////////
383 
itemMatches(const QModelIndex & index,int row,const QString & pattern) const384 bool KTreeViewSearchLine::itemMatches(const QModelIndex &index, int row, const QString &pattern) const
385 {
386     if (pattern.isEmpty()) {
387         return true;
388     }
389 
390     if (!index.isValid()) {
391         return false;
392     }
393 
394     // If the search column list is populated, search just the columns
395     // specifified.  If it is empty default to searching all of the columns.
396 
397     const int columncount = index.model()->columnCount(index);
398     if (!d->searchColumns.isEmpty()) {
399         QList<int>::ConstIterator it = d->searchColumns.constBegin();
400         for (; it != d->searchColumns.constEnd(); ++it) {
401             if (*it < columncount && index.model()->index(row, *it, index).data(Qt::DisplayRole).toString().indexOf(pattern, 0, d->caseSensitive) >= 0) {
402                 return true;
403             }
404         }
405     } else {
406         for (int i = 0; i < columncount; ++i) {
407             if (index.model()->index(row, i, index).data(Qt::DisplayRole).toString().indexOf(pattern, 0, d->caseSensitive) >= 0) {
408                 return true;
409             }
410         }
411     }
412 
413     return false;
414 }
415 
contextMenuEvent(QContextMenuEvent * event)416 void KTreeViewSearchLine::contextMenuEvent(QContextMenuEvent *event)
417 {
418     QMenu *popup = KLineEdit::createStandardContextMenu();
419 
420     if (d->canChooseColumns) {
421         popup->addSeparator();
422         QMenu *subMenu = popup->addMenu(i18n("Search Columns"));
423 
424         QAction *allVisibleColumnsAction = subMenu->addAction(i18n("All Visible Columns"), this, SLOT(slotAllVisibleColumns()));
425         allVisibleColumnsAction->setCheckable(true);
426         allVisibleColumnsAction->setChecked(!d->searchColumns.count());
427         subMenu->addSeparator();
428 
429         bool allColumnsAreSearchColumns = true;
430 
431         QActionGroup *group = new QActionGroup(popup);
432         group->setExclusive(false);
433         connect(group, SIGNAL(triggered(QAction *)), SLOT(slotColumnActivated(QAction *)));
434 
435         QHeaderView *const header = d->treeViews.first()->header();
436         for (int j = 0; j < header->count(); j++) {
437             int i = header->logicalIndex(j);
438 
439             if (header->isSectionHidden(i)) {
440                 continue;
441             }
442 
443             QString columnText = header->model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
444             QAction *columnAction = subMenu->addAction(qvariant_cast<QIcon>(header->model()->headerData(i, Qt::Horizontal, Qt::DecorationRole)), columnText);
445             columnAction->setCheckable(true);
446             columnAction->setChecked(d->searchColumns.isEmpty() || d->searchColumns.contains(i));
447             columnAction->setData(i);
448             columnAction->setActionGroup(group);
449 
450             if (d->searchColumns.isEmpty() || d->searchColumns.indexOf(i) != -1) {
451                 columnAction->setChecked(true);
452             } else {
453                 allColumnsAreSearchColumns = false;
454             }
455         }
456 
457         allVisibleColumnsAction->setChecked(allColumnsAreSearchColumns);
458 
459         // searchColumnsMenuActivated() relies on one possible "all" representation
460         if (allColumnsAreSearchColumns && !d->searchColumns.isEmpty()) {
461             d->searchColumns.clear();
462         }
463     }
464 
465     popup->exec(event->globalPos());
466     delete popup;
467 }
468 
connectTreeView(QTreeView * treeView)469 void KTreeViewSearchLine::connectTreeView(QTreeView *treeView)
470 {
471     connect(treeView, SIGNAL(destroyed(QObject *)), this, SLOT(treeViewDeleted(QObject *)));
472 
473     connect(treeView->model(), &QAbstractItemModel::rowsInserted, this, &KTreeViewSearchLine::rowsInserted);
474 }
475 
disconnectTreeView(QTreeView * treeView)476 void KTreeViewSearchLine::disconnectTreeView(QTreeView *treeView)
477 {
478     disconnect(treeView, SIGNAL(destroyed(QObject *)), this, SLOT(treeViewDeleted(QObject *)));
479 
480     disconnect(treeView->model(), &QAbstractItemModel::rowsInserted, this, &KTreeViewSearchLine::rowsInserted);
481 }
482 
canChooseColumnsCheck()483 bool KTreeViewSearchLine::canChooseColumnsCheck()
484 {
485     // This is true if either of the following is true:
486 
487     // there are no listviews connected
488     if (d->treeViews.isEmpty()) {
489         return false;
490     }
491 
492     const QTreeView *first = d->treeViews.first();
493 
494     const int numcols = first->model()->columnCount();
495     // the listviews have only one column,
496     if (numcols < 2) {
497         return false;
498     }
499 
500     QStringList headers;
501     for (int i = 0; i < numcols; ++i) {
502         headers.append(first->header()->model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString());
503     }
504 
505     QList<QTreeView *>::ConstIterator it = d->treeViews.constBegin();
506     for (++it /* skip the first one */; it != d->treeViews.constEnd(); ++it) {
507         // the listviews have different numbers of columns,
508         if ((*it)->model()->columnCount() != numcols) {
509             return false;
510         }
511 
512         // the listviews differ in column labels.
513         QStringList::ConstIterator jt;
514         int i;
515         for (i = 0, jt = headers.constBegin(); i < numcols; ++i, ++jt) {
516             Q_ASSERT(jt != headers.constEnd());
517 
518             if ((*it)->header()->model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString() != *jt) {
519                 return false;
520             }
521         }
522     }
523 
524     return true;
525 }
526 
527 ////////////////////////////////////////////////////////////////////////////////
528 // protected slots
529 ////////////////////////////////////////////////////////////////////////////////
530 
queueSearch(const QString & search)531 void KTreeViewSearchLine::queueSearch(const QString &search)
532 {
533     d->queuedSearches++;
534     d->search = search;
535 
536     QTimer::singleShot(200, this, &KTreeViewSearchLine::activateSearch);
537 }
538 
activateSearch()539 void KTreeViewSearchLine::activateSearch()
540 {
541     --(d->queuedSearches);
542 
543     if (d->queuedSearches == 0) {
544         updateSearch(d->search);
545     }
546 }
547 
548 ////////////////////////////////////////////////////////////////////////////////
549 // KTreeViewSearchLineWidget
550 ////////////////////////////////////////////////////////////////////////////////
551 
552 class KTreeViewSearchLineWidgetPrivate
553 {
554 public:
KTreeViewSearchLineWidgetPrivate()555     KTreeViewSearchLineWidgetPrivate()
556         : treeView(nullptr)
557         , searchLine(nullptr)
558     {
559     }
560 
561     QTreeView *treeView;
562     KTreeViewSearchLine *searchLine;
563 };
564 
KTreeViewSearchLineWidget(QWidget * parent,QTreeView * treeView)565 KTreeViewSearchLineWidget::KTreeViewSearchLineWidget(QWidget *parent, QTreeView *treeView)
566     : QWidget(parent)
567     , d(new KTreeViewSearchLineWidgetPrivate)
568 {
569     d->treeView = treeView;
570 
571     QTimer::singleShot(0, this, &KTreeViewSearchLineWidget::createWidgets);
572 }
573 
~KTreeViewSearchLineWidget()574 KTreeViewSearchLineWidget::~KTreeViewSearchLineWidget()
575 {
576     delete d;
577 }
578 
createSearchLine(QTreeView * treeView) const579 KTreeViewSearchLine *KTreeViewSearchLineWidget::createSearchLine(QTreeView *treeView) const
580 {
581     return new KTreeViewSearchLine(const_cast<KTreeViewSearchLineWidget *>(this), treeView);
582 }
583 
createWidgets()584 void KTreeViewSearchLineWidget::createWidgets()
585 {
586     QLabel *label = new QLabel(i18n("S&earch:"), this);
587     label->setObjectName(QLatin1String("kde toolbar widget"));
588 
589     searchLine()->show();
590 
591     label->setBuddy(d->searchLine);
592     label->show();
593 
594     QHBoxLayout *layout = new QHBoxLayout(this);
595     layout->setSpacing(5);
596     layout->setContentsMargins(0, 0, 0, 0);
597     layout->addWidget(label);
598     layout->addWidget(d->searchLine);
599 }
600 
searchLine() const601 KTreeViewSearchLine *KTreeViewSearchLineWidget::searchLine() const
602 {
603     if (!d->searchLine) {
604         d->searchLine = createSearchLine(d->treeView);
605     }
606 
607     return d->searchLine;
608 }
609 
610 #include "moc_ktreeviewsearchline.cpp"
611