1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2003 Scott Wheeler <wheeler@kde.org>
4 SPDX-FileCopyrightText: 2005 Rafal Rzepecki <divide@users.sourceforge.net>
5 SPDX-FileCopyrightText: 2006 Hamish Rodda <rodda@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-only
8*/
9
10#include "ktreewidgetsearchline.h"
11
12#include <QActionGroup>
13#include <QApplication>
14#include <QContextMenuEvent>
15#include <QHeaderView>
16#include <QList>
17#include <QMenu>
18#include <QTimer>
19#include <QTreeWidget>
20
21class KTreeWidgetSearchLinePrivate
22{
23public:
24 KTreeWidgetSearchLinePrivate(KTreeWidgetSearchLine *_q)
25 : q(_q)
26 {
27 }
28
29 KTreeWidgetSearchLine *const q;
30 QList<QTreeWidget *> treeWidgets;
31 Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive;
32 bool keepParentsVisible = true;
33 bool canChooseColumns = true;
34 QString search;
35 int queuedSearches = 0;
36 QList<int> searchColumns;
37
38 void _k_rowsInserted(const QModelIndex &parent, int start, int end) const;
39 void _k_treeWidgetDeleted(QObject *treeWidget);
40 void _k_slotColumnActivated(QAction *action);
41 void _k_slotAllVisibleColumns();
42 void _k_queueSearch(const QString &);
43 void _k_activateSearch();
44
45 void checkColumns();
46 void checkItemParentsNotVisible(QTreeWidget *treeWidget);
47 bool checkItemParentsVisible(QTreeWidgetItem *item);
48};
49
50////////////////////////////////////////////////////////////////////////////////
51// private slots
52////////////////////////////////////////////////////////////////////////////////
53
54// Hack to make a protected method public
55class QTreeWidgetWorkaround : public QTreeWidget
56{
57public:
58 QTreeWidgetItem *itemFromIndex(const QModelIndex &index) const
59 {
60 return QTreeWidget::itemFromIndex(index);
61 }
62};
63
64void KTreeWidgetSearchLinePrivate::_k_rowsInserted(const QModelIndex &parentIndex, int start, int end) const
65{
66 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: q->sender());
67 if (!model) {
68 return;
69 }
70
71 QTreeWidget *widget = nullptr;
72 for (QTreeWidget *tree : std::as_const(t: treeWidgets)) {
73 if (tree->model() == model) {
74 widget = tree;
75 break;
76 }
77 }
78
79 if (!widget) {
80 return;
81 }
82
83 QTreeWidgetWorkaround *widgetW = static_cast<QTreeWidgetWorkaround *>(widget);
84 for (int i = start; i <= end; ++i) {
85 if (QTreeWidgetItem *item = widgetW->itemFromIndex(index: model->index(row: i, column: 0, parent: parentIndex))) {
86 bool newHidden = !q->itemMatches(item, pattern: q->text());
87 if (item->isHidden() != newHidden) {
88 item->setHidden(newHidden);
89 Q_EMIT q->hiddenChanged(item, newHidden);
90 }
91 }
92 }
93}
94
95void KTreeWidgetSearchLinePrivate::_k_treeWidgetDeleted(QObject *object)
96{
97 treeWidgets.removeAll(t: static_cast<QTreeWidget *>(object));
98 q->setEnabled(treeWidgets.isEmpty());
99}
100
101void KTreeWidgetSearchLinePrivate::_k_slotColumnActivated(QAction *action)
102{
103 if (!action) {
104 return;
105 }
106
107 bool ok;
108 int column = action->data().toInt(ok: &ok);
109
110 if (!ok) {
111 return;
112 }
113
114 if (action->isChecked()) {
115 if (!searchColumns.isEmpty()) {
116 if (!searchColumns.contains(t: column)) {
117 searchColumns.append(t: column);
118 }
119
120 if (searchColumns.count() == treeWidgets.first()->header()->count() - treeWidgets.first()->header()->hiddenSectionCount()) {
121 searchColumns.clear();
122 }
123
124 } else {
125 searchColumns.append(t: column);
126 }
127 } else {
128 if (searchColumns.isEmpty()) {
129 QHeaderView *const header = treeWidgets.first()->header();
130
131 for (int i = 0; i < header->count(); i++) {
132 if (i != column && !header->isSectionHidden(logicalIndex: i)) {
133 searchColumns.append(t: i);
134 }
135 }
136
137 } else if (searchColumns.contains(t: column)) {
138 searchColumns.removeAll(t: column);
139 }
140 }
141
142 q->updateSearch();
143}
144
145void KTreeWidgetSearchLinePrivate::_k_slotAllVisibleColumns()
146{
147 if (searchColumns.isEmpty()) {
148 searchColumns.append(t: 0);
149 } else {
150 searchColumns.clear();
151 }
152
153 q->updateSearch();
154}
155
156////////////////////////////////////////////////////////////////////////////////
157// private methods
158////////////////////////////////////////////////////////////////////////////////
159
160void KTreeWidgetSearchLinePrivate::checkColumns()
161{
162 canChooseColumns = q->canChooseColumnsCheck();
163}
164
165void KTreeWidgetSearchLinePrivate::checkItemParentsNotVisible(QTreeWidget *treeWidget)
166{
167 for (QTreeWidgetItemIterator it(treeWidget); *it; ++it) {
168 QTreeWidgetItem *item = *it;
169 bool newHidden = !q->itemMatches(item, pattern: search);
170 if (item->isHidden() != newHidden) {
171 item->setHidden(newHidden);
172 Q_EMIT q->hiddenChanged(item, newHidden);
173 }
174 }
175}
176
177/** Check whether \p item, its siblings and their descendants should be shown. Show or hide the items as necessary.
178 *
179 * \p item The list view item to start showing / hiding items at. Typically, this is the first child of another item, or
180 * the first child of the list view.
181 * \return \c true if an item which should be visible is found, \c false if all items found should be hidden. If this function
182 * returns true and \p highestHiddenParent was not 0, highestHiddenParent will have been shown.
183 */
184bool KTreeWidgetSearchLinePrivate::checkItemParentsVisible(QTreeWidgetItem *item)
185{
186 bool childMatch = false;
187 for (int i = 0; i < item->childCount(); ++i) {
188 childMatch |= checkItemParentsVisible(item: item->child(index: i));
189 }
190
191 // Should this item be shown? It should if any children should be, or if it matches.
192 bool newHidden = !childMatch && !q->itemMatches(item, pattern: search);
193 if (item->isHidden() != newHidden) {
194 item->setHidden(newHidden);
195 Q_EMIT q->hiddenChanged(item, newHidden);
196 }
197
198 return !newHidden;
199}
200
201////////////////////////////////////////////////////////////////////////////////
202// public methods
203////////////////////////////////////////////////////////////////////////////////
204
205KTreeWidgetSearchLine::KTreeWidgetSearchLine(QWidget *q, QTreeWidget *treeWidget)
206 : QLineEdit(q)
207 , d(new KTreeWidgetSearchLinePrivate(this))
208{
209 connect(sender: this, SIGNAL(textChanged(QString)), receiver: this, SLOT(_k_queueSearch(QString)));
210
211 setClearButtonEnabled(true);
212 setPlaceholderText(tr(s: "Search...", c: "@info:placeholder"));
213 setTreeWidget(treeWidget);
214
215 if (!treeWidget) {
216 setEnabled(false);
217 }
218}
219
220KTreeWidgetSearchLine::KTreeWidgetSearchLine(QWidget *q, const QList<QTreeWidget *> &treeWidgets)
221 : QLineEdit(q)
222 , d(new KTreeWidgetSearchLinePrivate(this))
223{
224 connect(sender: this, SIGNAL(textChanged(QString)), receiver: this, SLOT(_k_queueSearch(QString)));
225
226 setClearButtonEnabled(true);
227 setTreeWidgets(treeWidgets);
228}
229
230KTreeWidgetSearchLine::~KTreeWidgetSearchLine() = default;
231
232Qt::CaseSensitivity KTreeWidgetSearchLine::caseSensitivity() const
233{
234 return d->caseSensitive;
235}
236
237QList<int> KTreeWidgetSearchLine::searchColumns() const
238{
239 if (d->canChooseColumns) {
240 return d->searchColumns;
241 } else {
242 return QList<int>();
243 }
244}
245
246bool KTreeWidgetSearchLine::keepParentsVisible() const
247{
248 return d->keepParentsVisible;
249}
250
251QTreeWidget *KTreeWidgetSearchLine::treeWidget() const
252{
253 if (d->treeWidgets.count() == 1) {
254 return d->treeWidgets.first();
255 } else {
256 return nullptr;
257 }
258}
259
260QList<QTreeWidget *> KTreeWidgetSearchLine::treeWidgets() const
261{
262 return d->treeWidgets;
263}
264
265////////////////////////////////////////////////////////////////////////////////
266// public slots
267////////////////////////////////////////////////////////////////////////////////
268
269void KTreeWidgetSearchLine::addTreeWidget(QTreeWidget *treeWidget)
270{
271 if (treeWidget) {
272 connectTreeWidget(treeWidget);
273
274 d->treeWidgets.append(t: treeWidget);
275 setEnabled(!d->treeWidgets.isEmpty());
276
277 d->checkColumns();
278 }
279}
280
281void KTreeWidgetSearchLine::removeTreeWidget(QTreeWidget *treeWidget)
282{
283 if (treeWidget) {
284 int index = d->treeWidgets.indexOf(t: treeWidget);
285
286 if (index != -1) {
287 d->treeWidgets.removeAt(i: index);
288 d->checkColumns();
289
290 disconnectTreeWidget(treeWidget);
291
292 setEnabled(!d->treeWidgets.isEmpty());
293 }
294 }
295}
296
297void KTreeWidgetSearchLine::updateSearch(const QString &pattern)
298{
299 d->search = pattern.isNull() ? text() : pattern;
300
301 for (QTreeWidget *treeWidget : std::as_const(t&: d->treeWidgets)) {
302 updateSearch(treeWidget);
303 }
304}
305
306void KTreeWidgetSearchLine::updateSearch(QTreeWidget *treeWidget)
307{
308 if (!treeWidget || !treeWidget->topLevelItemCount()) {
309 return;
310 }
311
312 // If there's a selected item that is visible, make sure that it's visible
313 // when the search changes too (assuming that it still matches).
314
315 QTreeWidgetItem *currentItem = treeWidget->currentItem();
316
317 if (d->keepParentsVisible) {
318 for (int i = 0; i < treeWidget->topLevelItemCount(); ++i) {
319 d->checkItemParentsVisible(item: treeWidget->topLevelItem(index: i));
320 }
321 } else {
322 d->checkItemParentsNotVisible(treeWidget);
323 }
324
325 if (currentItem) {
326 treeWidget->scrollToItem(item: currentItem);
327 }
328
329 Q_EMIT searchUpdated(searchString: d->search);
330}
331
332void KTreeWidgetSearchLine::setCaseSensitivity(Qt::CaseSensitivity caseSensitive)
333{
334 if (d->caseSensitive != caseSensitive) {
335 d->caseSensitive = caseSensitive;
336 Q_EMIT caseSensitivityChanged(caseSensitivity: d->caseSensitive);
337 updateSearch();
338 }
339}
340
341void KTreeWidgetSearchLine::setKeepParentsVisible(bool visible)
342{
343 if (d->keepParentsVisible != visible) {
344 d->keepParentsVisible = visible;
345 Q_EMIT keepParentsVisibleChanged(keepParentsVisible: d->keepParentsVisible);
346 updateSearch();
347 }
348}
349
350void KTreeWidgetSearchLine::setSearchColumns(const QList<int> &columns)
351{
352 if (d->canChooseColumns) {
353 d->searchColumns = columns;
354 }
355}
356
357void KTreeWidgetSearchLine::setTreeWidget(QTreeWidget *treeWidget)
358{
359 setTreeWidgets(QList<QTreeWidget *>());
360 addTreeWidget(treeWidget);
361}
362
363void KTreeWidgetSearchLine::setTreeWidgets(const QList<QTreeWidget *> &treeWidgets)
364{
365 for (QTreeWidget *treeWidget : std::as_const(t&: d->treeWidgets)) {
366 disconnectTreeWidget(treeWidget);
367 }
368
369 d->treeWidgets = treeWidgets;
370
371 for (QTreeWidget *treeWidget : std::as_const(t&: d->treeWidgets)) {
372 connectTreeWidget(treeWidget);
373 }
374
375 d->checkColumns();
376
377 setEnabled(!d->treeWidgets.isEmpty());
378}
379
380////////////////////////////////////////////////////////////////////////////////
381// protected members
382////////////////////////////////////////////////////////////////////////////////
383
384bool KTreeWidgetSearchLine::itemMatches(const QTreeWidgetItem *item, const QString &pattern) const
385{
386 if (pattern.isEmpty()) {
387 return true;
388 }
389
390 // If the search column list is populated, search just the columns
391 // specified. If it is empty default to searching all of the columns.
392
393 if (!d->searchColumns.isEmpty()) {
394 QList<int>::ConstIterator it = d->searchColumns.constBegin();
395 for (; it != d->searchColumns.constEnd(); ++it) {
396 if (*it < item->treeWidget()->columnCount() //
397 && item->text(column: *it).indexOf(s: pattern, from: 0, cs: d->caseSensitive) >= 0) {
398 return true;
399 }
400 }
401 } else {
402 for (int i = 0; i < item->treeWidget()->columnCount(); i++) {
403 if (item->treeWidget()->columnWidth(column: i) > 0 //
404 && item->text(column: i).indexOf(s: pattern, from: 0, cs: d->caseSensitive) >= 0) {
405 return true;
406 }
407 }
408 }
409
410 return false;
411}
412
413void KTreeWidgetSearchLine::contextMenuEvent(QContextMenuEvent *event)
414{
415 QMenu *popup = QLineEdit::createStandardContextMenu();
416
417 if (d->canChooseColumns) {
418 popup->addSeparator();
419 QMenu *subMenu = popup->addMenu(title: tr(s: "Search Columns", c: "@title:menu"));
420
421 QAction *allVisibleColumnsAction = subMenu->addAction(text: tr(s: "All Visible Columns", c: "@optipn:check"), receiver: this, SLOT(_k_slotAllVisibleColumns()));
422 allVisibleColumnsAction->setCheckable(true);
423 allVisibleColumnsAction->setChecked(d->searchColumns.isEmpty());
424 subMenu->addSeparator();
425
426 bool allColumnsAreSearchColumns = true;
427
428 QActionGroup *group = new QActionGroup(popup);
429 group->setExclusive(false);
430 connect(asender: group, SIGNAL(triggered(QAction *)), SLOT(_k_slotColumnActivated(QAction *)));
431
432 QHeaderView *const header = d->treeWidgets.first()->header();
433 for (int j = 0; j < header->count(); j++) {
434 int i = header->logicalIndex(visualIndex: j);
435
436 if (header->isSectionHidden(logicalIndex: i)) {
437 continue;
438 }
439
440 QString columnText = d->treeWidgets.first()->headerItem()->text(column: i);
441 QAction *columnAction = subMenu->addAction(icon: d->treeWidgets.first()->headerItem()->icon(column: i), text: columnText);
442 columnAction->setCheckable(true);
443 columnAction->setChecked(d->searchColumns.isEmpty() || d->searchColumns.contains(t: i));
444 columnAction->setData(i);
445 columnAction->setActionGroup(group);
446
447 if (d->searchColumns.isEmpty() || d->searchColumns.indexOf(t: i) != -1) {
448 columnAction->setChecked(true);
449 } else {
450 allColumnsAreSearchColumns = false;
451 }
452 }
453
454 allVisibleColumnsAction->setChecked(allColumnsAreSearchColumns);
455
456 // searchColumnsMenuActivated() relies on one possible "all" representation
457 if (allColumnsAreSearchColumns && !d->searchColumns.isEmpty()) {
458 d->searchColumns.clear();
459 }
460 }
461
462 popup->exec(pos: event->globalPos());
463 delete popup;
464}
465
466void KTreeWidgetSearchLine::connectTreeWidget(QTreeWidget *treeWidget)
467{
468 connect(sender: treeWidget, SIGNAL(destroyed(QObject *)), receiver: this, SLOT(_k_treeWidgetDeleted(QObject *)));
469
470 connect(sender: treeWidget->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), receiver: this, SLOT(_k_rowsInserted(QModelIndex, int, int)));
471}
472
473void KTreeWidgetSearchLine::disconnectTreeWidget(QTreeWidget *treeWidget)
474{
475 disconnect(sender: treeWidget, SIGNAL(destroyed(QObject *)), receiver: this, SLOT(_k_treeWidgetDeleted(QObject *)));
476
477 disconnect(sender: treeWidget->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), receiver: this, SLOT(_k_rowsInserted(QModelIndex, int, int)));
478}
479
480bool KTreeWidgetSearchLine::canChooseColumnsCheck()
481{
482 // This is true if either of the following is true:
483
484 // there are no listviews connected
485 if (d->treeWidgets.isEmpty()) {
486 return false;
487 }
488
489 const QTreeWidget *first = d->treeWidgets.first();
490
491 const int numcols = first->columnCount();
492 // the listviews have only one column,
493 if (numcols < 2) {
494 return false;
495 }
496
497 QStringList headers;
498 headers.reserve(asize: numcols);
499 for (int i = 0; i < numcols; ++i) {
500 headers.append(t: first->headerItem()->text(column: i));
501 }
502
503 QList<QTreeWidget *>::ConstIterator it = d->treeWidgets.constBegin();
504 for (++it /* skip the first one */; it != d->treeWidgets.constEnd(); ++it) {
505 // the listviews have different numbers of columns,
506 if ((*it)->columnCount() != numcols) {
507 return false;
508 }
509
510 // the listviews differ in column labels.
511 QStringList::ConstIterator jt;
512 int i;
513 for (i = 0, jt = headers.constBegin(); i < numcols; ++i, ++jt) {
514 Q_ASSERT(jt != headers.constEnd());
515
516 if ((*it)->headerItem()->text(column: i) != *jt) {
517 return false;
518 }
519 }
520 }
521
522 return true;
523}
524
525bool KTreeWidgetSearchLine::event(QEvent *event)
526{
527 if (event->type() == QEvent::KeyPress) {
528 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
529 if (keyEvent->matches(key: QKeySequence::MoveToNextLine) || keyEvent->matches(key: QKeySequence::SelectNextLine)
530 || keyEvent->matches(key: QKeySequence::MoveToPreviousLine) || keyEvent->matches(key: QKeySequence::SelectPreviousLine)
531 || keyEvent->matches(key: QKeySequence::MoveToNextPage) || keyEvent->matches(key: QKeySequence::SelectNextPage)
532 || keyEvent->matches(key: QKeySequence::MoveToPreviousPage) || keyEvent->matches(key: QKeySequence::SelectPreviousPage) || keyEvent->key() == Qt::Key_Enter
533 || keyEvent->key() == Qt::Key_Return) {
534 QTreeWidget *first = d->treeWidgets.first();
535 if (first) {
536 QApplication::sendEvent(receiver: first, event);
537 return true;
538 }
539 }
540 }
541 return QLineEdit::event(event);
542}
543
544////////////////////////////////////////////////////////////////////////////////
545// protected slots
546////////////////////////////////////////////////////////////////////////////////
547
548void KTreeWidgetSearchLinePrivate::_k_queueSearch(const QString &_search)
549{
550 queuedSearches++;
551 search = _search;
552
553 QTimer::singleShot(msec: 200, receiver: q, SLOT(_k_activateSearch()));
554}
555
556void KTreeWidgetSearchLinePrivate::_k_activateSearch()
557{
558 --queuedSearches;
559
560 if (queuedSearches == 0) {
561 q->updateSearch(pattern: search);
562 }
563}
564
565#include "moc_ktreewidgetsearchline.cpp"
566

source code of kitemviews/src/ktreewidgetsearchline.cpp