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