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-or-later
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/*
178 * Check whether item, its siblings and their descendants should be shown. Show or hide the items as necessary.
179 *
180 * item The list view item to start showing / hiding items at. Typically, this is the first child of another item, or
181 * the first child of the list view.
182 * Returns true if an item which should be visible is found, false if all items found should be hidden. If this function
183 * returns true and highestHiddenParent was not 0, highestHiddenParent will have been shown.
184 */
185bool KTreeWidgetSearchLinePrivate::checkItemParentsVisible(QTreeWidgetItem *item)
186{
187 bool childMatch = false;
188 for (int i = 0; i < item->childCount(); ++i) {
189 childMatch |= checkItemParentsVisible(item: item->child(index: i));
190 }
191
192 // Should this item be shown? It should if any children should be, or if it matches.
193 bool newHidden = !childMatch && !q->itemMatches(item, pattern: search);
194 if (item->isHidden() != newHidden) {
195 item->setHidden(newHidden);
196 Q_EMIT q->hiddenChanged(item, newHidden);
197 }
198
199 return !newHidden;
200}
201
202////////////////////////////////////////////////////////////////////////////////
203// public methods
204////////////////////////////////////////////////////////////////////////////////
205
206KTreeWidgetSearchLine::KTreeWidgetSearchLine(QWidget *q, QTreeWidget *treeWidget)
207 : QLineEdit(q)
208 , d(new KTreeWidgetSearchLinePrivate(this))
209{
210 connect(sender: this, SIGNAL(textChanged(QString)), receiver: this, SLOT(_k_queueSearch(QString)));
211
212 setClearButtonEnabled(true);
213 setPlaceholderText(tr(s: "Search…", c: "@info:placeholder"));
214 setTreeWidget(treeWidget);
215
216 if (!treeWidget) {
217 setEnabled(false);
218 }
219}
220
221KTreeWidgetSearchLine::KTreeWidgetSearchLine(QWidget *q, const QList<QTreeWidget *> &treeWidgets)
222 : QLineEdit(q)
223 , d(new KTreeWidgetSearchLinePrivate(this))
224{
225 connect(sender: this, SIGNAL(textChanged(QString)), receiver: this, SLOT(_k_queueSearch(QString)));
226
227 setClearButtonEnabled(true);
228 setTreeWidgets(treeWidgets);
229}
230
231KTreeWidgetSearchLine::~KTreeWidgetSearchLine() = default;
232
233Qt::CaseSensitivity KTreeWidgetSearchLine::caseSensitivity() const
234{
235 return d->caseSensitive;
236}
237
238QList<int> KTreeWidgetSearchLine::searchColumns() const
239{
240 if (d->canChooseColumns) {
241 return d->searchColumns;
242 } else {
243 return QList<int>();
244 }
245}
246
247bool KTreeWidgetSearchLine::keepParentsVisible() const
248{
249 return d->keepParentsVisible;
250}
251
252QTreeWidget *KTreeWidgetSearchLine::treeWidget() const
253{
254 if (d->treeWidgets.count() == 1) {
255 return d->treeWidgets.first();
256 } else {
257 return nullptr;
258 }
259}
260
261QList<QTreeWidget *> KTreeWidgetSearchLine::treeWidgets() const
262{
263 return d->treeWidgets;
264}
265
266////////////////////////////////////////////////////////////////////////////////
267// public slots
268////////////////////////////////////////////////////////////////////////////////
269
270void KTreeWidgetSearchLine::addTreeWidget(QTreeWidget *treeWidget)
271{
272 if (treeWidget) {
273 connectTreeWidget(treeWidget);
274
275 d->treeWidgets.append(t: treeWidget);
276 setEnabled(!d->treeWidgets.isEmpty());
277
278 d->checkColumns();
279 }
280}
281
282void KTreeWidgetSearchLine::removeTreeWidget(QTreeWidget *treeWidget)
283{
284 if (treeWidget) {
285 int index = d->treeWidgets.indexOf(t: treeWidget);
286
287 if (index != -1) {
288 d->treeWidgets.removeAt(i: index);
289 d->checkColumns();
290
291 disconnectTreeWidget(treeWidget);
292
293 setEnabled(!d->treeWidgets.isEmpty());
294 }
295 }
296}
297
298void KTreeWidgetSearchLine::updateSearch(const QString &pattern)
299{
300 d->search = pattern.isNull() ? text() : pattern;
301
302 for (QTreeWidget *treeWidget : std::as_const(t&: d->treeWidgets)) {
303 updateSearch(treeWidget);
304 }
305}
306
307void KTreeWidgetSearchLine::updateSearch(QTreeWidget *treeWidget)
308{
309 if (!treeWidget || !treeWidget->topLevelItemCount()) {
310 return;
311 }
312
313 // If there's a selected item that is visible, make sure that it's visible
314 // when the search changes too (assuming that it still matches).
315
316 QTreeWidgetItem *currentItem = treeWidget->currentItem();
317
318 if (d->keepParentsVisible) {
319 for (int i = 0; i < treeWidget->topLevelItemCount(); ++i) {
320 d->checkItemParentsVisible(item: treeWidget->topLevelItem(index: i));
321 }
322 } else {
323 d->checkItemParentsNotVisible(treeWidget);
324 }
325
326 if (currentItem) {
327 treeWidget->scrollToItem(item: currentItem);
328 }
329
330 Q_EMIT searchUpdated(searchString: d->search);
331}
332
333void KTreeWidgetSearchLine::setCaseSensitivity(Qt::CaseSensitivity caseSensitive)
334{
335 if (d->caseSensitive != caseSensitive) {
336 d->caseSensitive = caseSensitive;
337 Q_EMIT caseSensitivityChanged(caseSensitivity: d->caseSensitive);
338 updateSearch();
339 }
340}
341
342void KTreeWidgetSearchLine::setKeepParentsVisible(bool visible)
343{
344 if (d->keepParentsVisible != visible) {
345 d->keepParentsVisible = visible;
346 Q_EMIT keepParentsVisibleChanged(keepParentsVisible: d->keepParentsVisible);
347 updateSearch();
348 }
349}
350
351void KTreeWidgetSearchLine::setSearchColumns(const QList<int> &columns)
352{
353 if (d->canChooseColumns) {
354 d->searchColumns = columns;
355 }
356}
357
358void KTreeWidgetSearchLine::setTreeWidget(QTreeWidget *treeWidget)
359{
360 setTreeWidgets(QList<QTreeWidget *>());
361 addTreeWidget(treeWidget);
362}
363
364void KTreeWidgetSearchLine::setTreeWidgets(const QList<QTreeWidget *> &treeWidgets)
365{
366 for (QTreeWidget *treeWidget : std::as_const(t&: d->treeWidgets)) {
367 disconnectTreeWidget(treeWidget);
368 }
369
370 d->treeWidgets = treeWidgets;
371
372 for (QTreeWidget *treeWidget : std::as_const(t&: d->treeWidgets)) {
373 connectTreeWidget(treeWidget);
374 }
375
376 d->checkColumns();
377
378 setEnabled(!d->treeWidgets.isEmpty());
379}
380
381////////////////////////////////////////////////////////////////////////////////
382// protected members
383////////////////////////////////////////////////////////////////////////////////
384
385bool KTreeWidgetSearchLine::itemMatches(const QTreeWidgetItem *item, const QString &pattern) const
386{
387 if (pattern.isEmpty()) {
388 return true;
389 }
390
391 // If the search column list is populated, search just the columns
392 // specified. If it is empty default to searching all of the columns.
393
394 if (!d->searchColumns.isEmpty()) {
395 QList<int>::ConstIterator it = d->searchColumns.constBegin();
396 for (; it != d->searchColumns.constEnd(); ++it) {
397 if (*it < item->treeWidget()->columnCount() //
398 && item->text(column: *it).indexOf(s: pattern, from: 0, cs: d->caseSensitive) >= 0) {
399 return true;
400 }
401 }
402 } else {
403 for (int i = 0; i < item->treeWidget()->columnCount(); i++) {
404 if (item->treeWidget()->columnWidth(column: i) > 0 //
405 && item->text(column: i).indexOf(s: pattern, from: 0, cs: d->caseSensitive) >= 0) {
406 return true;
407 }
408 }
409 }
410
411 return false;
412}
413
414void KTreeWidgetSearchLine::contextMenuEvent(QContextMenuEvent *event)
415{
416 QMenu *popup = QLineEdit::createStandardContextMenu();
417
418 if (d->canChooseColumns) {
419 popup->addSeparator();
420 QMenu *subMenu = popup->addMenu(title: tr(s: "Search Columns", c: "@title:menu"));
421
422 QAction *allVisibleColumnsAction = subMenu->addAction(text: tr(s: "All Visible Columns", c: "@optipn:check"), receiver: this, SLOT(_k_slotAllVisibleColumns()));
423 allVisibleColumnsAction->setCheckable(true);
424 allVisibleColumnsAction->setChecked(d->searchColumns.isEmpty());
425 subMenu->addSeparator();
426
427 bool allColumnsAreSearchColumns = true;
428
429 QActionGroup *group = new QActionGroup(popup);
430 group->setExclusive(false);
431 connect(asender: group, SIGNAL(triggered(QAction *)), SLOT(_k_slotColumnActivated(QAction *)));
432
433 QHeaderView *const header = d->treeWidgets.first()->header();
434 for (int j = 0; j < header->count(); j++) {
435 int i = header->logicalIndex(visualIndex: j);
436
437 if (header->isSectionHidden(logicalIndex: i)) {
438 continue;
439 }
440
441 QString columnText = d->treeWidgets.first()->headerItem()->text(column: i);
442 QAction *columnAction = subMenu->addAction(icon: d->treeWidgets.first()->headerItem()->icon(column: i), text: columnText);
443 columnAction->setCheckable(true);
444 columnAction->setChecked(d->searchColumns.isEmpty() || d->searchColumns.contains(t: i));
445 columnAction->setData(i);
446 columnAction->setActionGroup(group);
447
448 if (d->searchColumns.isEmpty() || d->searchColumns.indexOf(t: i) != -1) {
449 columnAction->setChecked(true);
450 } else {
451 allColumnsAreSearchColumns = false;
452 }
453 }
454
455 allVisibleColumnsAction->setChecked(allColumnsAreSearchColumns);
456
457 // searchColumnsMenuActivated() relies on one possible "all" representation
458 if (allColumnsAreSearchColumns && !d->searchColumns.isEmpty()) {
459 d->searchColumns.clear();
460 }
461 }
462
463 popup->exec(pos: event->globalPos());
464 delete popup;
465}
466
467void KTreeWidgetSearchLine::connectTreeWidget(QTreeWidget *treeWidget)
468{
469 connect(sender: treeWidget, SIGNAL(destroyed(QObject *)), receiver: this, SLOT(_k_treeWidgetDeleted(QObject *)));
470
471 connect(sender: treeWidget->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), receiver: this, SLOT(_k_rowsInserted(QModelIndex, int, int)));
472}
473
474void KTreeWidgetSearchLine::disconnectTreeWidget(QTreeWidget *treeWidget)
475{
476 disconnect(sender: treeWidget, SIGNAL(destroyed(QObject *)), receiver: this, SLOT(_k_treeWidgetDeleted(QObject *)));
477
478 disconnect(sender: treeWidget->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), receiver: this, SLOT(_k_rowsInserted(QModelIndex, int, int)));
479}
480
481bool KTreeWidgetSearchLine::canChooseColumnsCheck()
482{
483 // This is true if either of the following is true:
484
485 // there are no listviews connected
486 if (d->treeWidgets.isEmpty()) {
487 return false;
488 }
489
490 const QTreeWidget *first = d->treeWidgets.first();
491
492 const int numcols = first->columnCount();
493 // the listviews have only one column,
494 if (numcols < 2) {
495 return false;
496 }
497
498 QStringList headers;
499 headers.reserve(asize: numcols);
500 for (int i = 0; i < numcols; ++i) {
501 headers.append(t: first->headerItem()->text(column: i));
502 }
503
504 QList<QTreeWidget *>::ConstIterator it = d->treeWidgets.constBegin();
505 for (++it /* skip the first one */; it != d->treeWidgets.constEnd(); ++it) {
506 // the listviews have different numbers of columns,
507 if ((*it)->columnCount() != numcols) {
508 return false;
509 }
510
511 // the listviews differ in column labels.
512 QStringList::ConstIterator jt;
513 int i;
514 for (i = 0, jt = headers.constBegin(); i < numcols; ++i, ++jt) {
515 Q_ASSERT(jt != headers.constEnd());
516
517 if ((*it)->headerItem()->text(column: i) != *jt) {
518 return false;
519 }
520 }
521 }
522
523 return true;
524}
525
526bool KTreeWidgetSearchLine::event(QEvent *event)
527{
528 if (event->type() == QEvent::KeyPress) {
529 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
530 if (keyEvent->matches(key: QKeySequence::MoveToNextLine) || keyEvent->matches(key: QKeySequence::SelectNextLine)
531 || keyEvent->matches(key: QKeySequence::MoveToPreviousLine) || keyEvent->matches(key: QKeySequence::SelectPreviousLine)
532 || keyEvent->matches(key: QKeySequence::MoveToNextPage) || keyEvent->matches(key: QKeySequence::SelectNextPage)
533 || keyEvent->matches(key: QKeySequence::MoveToPreviousPage) || keyEvent->matches(key: QKeySequence::SelectPreviousPage) || keyEvent->key() == Qt::Key_Enter
534 || keyEvent->key() == Qt::Key_Return) {
535 QTreeWidget *first = d->treeWidgets.first();
536 if (first) {
537 QApplication::sendEvent(receiver: first, event);
538 return true;
539 }
540 }
541 }
542 return QLineEdit::event(event);
543}
544
545////////////////////////////////////////////////////////////////////////////////
546// protected slots
547////////////////////////////////////////////////////////////////////////////////
548
549void KTreeWidgetSearchLinePrivate::_k_queueSearch(const QString &_search)
550{
551 queuedSearches++;
552 search = _search;
553
554 QTimer::singleShot(msec: 200, receiver: q, SLOT(_k_activateSearch()));
555}
556
557void KTreeWidgetSearchLinePrivate::_k_activateSearch()
558{
559 --queuedSearches;
560
561 if (queuedSearches == 0) {
562 q->updateSearch(pattern: search);
563 }
564}
565
566#include "moc_ktreewidgetsearchline.cpp"
567

source code of kitemviews/src/ktreewidgetsearchline.cpp