KItemViews

ktreewidgetsearchline.cpp
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;
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 *>(q->sender());
67 if (!model) {
68 return;
69 }
70
71 QTreeWidget *widget = nullptr;
72 for (QTreeWidget *tree : std::as_const(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(model->index(i, 0, parentIndex))) {
86 bool newHidden = !q->itemMatches(item, 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(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);
109
110 if (!ok) {
111 return;
112 }
113
114 if (action->isChecked()) {
115 if (!searchColumns.isEmpty()) {
116 if (!searchColumns.contains(column)) {
117 searchColumns.append(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(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(i)) {
133 searchColumns.append(i);
134 }
135 }
136
137 } else if (searchColumns.contains(column)) {
138 searchColumns.removeAll(column);
139 }
140 }
141
142 q->updateSearch();
143}
144
145void KTreeWidgetSearchLinePrivate::_k_slotAllVisibleColumns()
146{
147 if (searchColumns.isEmpty()) {
148 searchColumns.append(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, 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->child(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, 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
206 : QLineEdit(q)
207 , d(new KTreeWidgetSearchLinePrivate(this))
208{
209 connect(this, SIGNAL(textChanged(QString)), this, SLOT(_k_queueSearch(QString)));
210
212 setPlaceholderText(tr("Search...", "@info:placeholder"));
214
215 if (!treeWidget) {
216 setEnabled(false);
217 }
218}
219
221 : QLineEdit(q)
222 , d(new KTreeWidgetSearchLinePrivate(this))
223{
224 connect(this, SIGNAL(textChanged(QString)), this, SLOT(_k_queueSearch(QString)));
225
228}
229
231
233{
234 return d->caseSensitive;
235}
236
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
252{
253 if (d->treeWidgets.count() == 1) {
254 return d->treeWidgets.first();
255 } else {
256 return nullptr;
257 }
258}
259
261{
262 return d->treeWidgets;
263}
264
265////////////////////////////////////////////////////////////////////////////////
266// public slots
267////////////////////////////////////////////////////////////////////////////////
268
270{
271 if (treeWidget) {
273
274 d->treeWidgets.append(treeWidget);
275 setEnabled(!d->treeWidgets.isEmpty());
276
277 d->checkColumns();
278 }
279}
280
282{
283 if (treeWidget) {
284 int index = d->treeWidgets.indexOf(treeWidget);
285
286 if (index != -1) {
287 d->treeWidgets.removeAt(index);
288 d->checkColumns();
289
291
292 setEnabled(!d->treeWidgets.isEmpty());
293 }
294 }
295}
296
298{
299 d->search = pattern.isNull() ? text() : pattern;
300
301 for (QTreeWidget *treeWidget : std::as_const(d->treeWidgets)) {
303 }
304}
305
307{
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(treeWidget->topLevelItem(i));
320 }
321 } else {
322 d->checkItemParentsNotVisible(treeWidget);
323 }
324
325 if (currentItem) {
326 treeWidget->scrollToItem(currentItem);
327 }
328
329 Q_EMIT searchUpdated(d->search);
330}
331
333{
334 if (d->caseSensitive != caseSensitive) {
335 d->caseSensitive = caseSensitive;
336 Q_EMIT caseSensitivityChanged(d->caseSensitive);
337 updateSearch();
338 }
339}
340
342{
343 if (d->keepParentsVisible != visible) {
344 d->keepParentsVisible = visible;
345 Q_EMIT keepParentsVisibleChanged(d->keepParentsVisible);
346 updateSearch();
347 }
348}
349
351{
352 if (d->canChooseColumns) {
353 d->searchColumns = columns;
354 }
355}
356
362
364{
365 for (QTreeWidget *treeWidget : std::as_const(d->treeWidgets)) {
367 }
368
369 d->treeWidgets = treeWidgets;
370
371 for (QTreeWidget *treeWidget : std::as_const(d->treeWidgets)) {
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(*it).indexOf(pattern, 0, 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(i) > 0 //
404 && item->text(i).indexOf(pattern, 0, d->caseSensitive) >= 0) {
405 return true;
406 }
407 }
408 }
409
410 return false;
411}
412
414{
416
417 if (d->canChooseColumns) {
418 popup->addSeparator();
419 QMenu *subMenu = popup->addMenu(tr("Search Columns", "@title:menu"));
420
421 QAction *allVisibleColumnsAction = subMenu->addAction(tr("All Visible Columns", "@optipn:check"), 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(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(j);
435
436 if (header->isSectionHidden(i)) {
437 continue;
438 }
439
440 QString columnText = d->treeWidgets.first()->headerItem()->text(i);
441 QAction *columnAction = subMenu->addAction(d->treeWidgets.first()->headerItem()->icon(i), columnText);
442 columnAction->setCheckable(true);
443 columnAction->setChecked(d->searchColumns.isEmpty() || d->searchColumns.contains(i));
444 columnAction->setData(i);
445 columnAction->setActionGroup(group);
446
447 if (d->searchColumns.isEmpty() || d->searchColumns.indexOf(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(event->globalPos());
463 delete popup;
464}
465
467{
468 connect(treeWidget, SIGNAL(destroyed(QObject *)), this, SLOT(_k_treeWidgetDeleted(QObject *)));
469
470 connect(treeWidget->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(_k_rowsInserted(QModelIndex, int, int)));
471}
472
474{
475 disconnect(treeWidget, SIGNAL(destroyed(QObject *)), this, SLOT(_k_treeWidgetDeleted(QObject *)));
476
477 disconnect(treeWidget->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(_k_rowsInserted(QModelIndex, int, int)));
478}
479
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(numcols);
499 for (int i = 0; i < numcols; ++i) {
500 headers.append(first->headerItem()->text(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.
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(i) != *jt) {
517 return false;
518 }
519 }
520 }
521
522 return true;
523}
524
526{
527 if (event->type() == QEvent::KeyPress) {
528 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
529 if (keyEvent->matches(QKeySequence::MoveToNextLine) || keyEvent->matches(QKeySequence::SelectNextLine)
530 || keyEvent->matches(QKeySequence::MoveToPreviousLine) || keyEvent->matches(QKeySequence::SelectPreviousLine)
531 || keyEvent->matches(QKeySequence::MoveToNextPage) || keyEvent->matches(QKeySequence::SelectNextPage)
532 || keyEvent->matches(QKeySequence::MoveToPreviousPage) || keyEvent->matches(QKeySequence::SelectPreviousPage) || keyEvent->key() == Qt::Key_Enter
533 || keyEvent->key() == Qt::Key_Return) {
534 QTreeWidget *first = d->treeWidgets.first();
535 if (first) {
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(200, q, SLOT(_k_activateSearch()));
554}
555
556void KTreeWidgetSearchLinePrivate::_k_activateSearch()
557{
558 --queuedSearches;
559
560 if (queuedSearches == 0) {
561 q->updateSearch(search);
562 }
563}
564
565#include "moc_ktreewidgetsearchline.cpp"
This class makes it easy to add a search line for filtering the items in listviews based on a simple ...
virtual void disconnectTreeWidget(QTreeWidget *)
Disconnects signals of a listviews from the search line.
virtual bool canChooseColumnsCheck()
Checks columns in all listviews and decides whether choosing columns to filter on makes any sense.
void searchUpdated(const QString &searchString)
This signal is emitted when user finished entering filter text or when he made a pause long enough,...
KTreeWidgetSearchLine(QWidget *parent=nullptr, QTreeWidget *treeWidget=nullptr)
Constructs a KTreeWidgetSearchLine with treeWidget being the QTreeWidget to be filtered.
void removeTreeWidget(QTreeWidget *treeWidget)
Removes a QTreeWidget from the list of listviews filtered by this search line.
void setTreeWidgets(const QList< QTreeWidget * > &treeWidgets)
Sets QTreeWidgets that are filtered by this search line, replacing any previously filtered listviews.
~KTreeWidgetSearchLine() override
Destroys the KTreeWidgetSearchLine.
virtual void updateSearch(const QString &pattern=QString())
Updates search to only make visible the items that match pattern.
void addTreeWidget(QTreeWidget *treeWidget)
Adds a QTreeWidget to the list of listviews filtered by this search line.
void setCaseSensitivity(Qt::CaseSensitivity caseSensitivity)
Make the search case sensitive or case insensitive.
virtual bool itemMatches(const QTreeWidgetItem *item, const QString &pattern) const
Returns true if item matches the search pattern.
void contextMenuEvent(QContextMenuEvent *) override
Re-implemented for internal reasons.
bool event(QEvent *event) override
Re-implemented for internal reasons.
QList< QTreeWidget * > treeWidgets() const
Returns the list of pointers to listviews that are currently filtered by the search.
QList< int > searchColumns() const
Returns the current list of columns that will be searched.
void hiddenChanged(QTreeWidgetItem *, bool)
This signal is emitted whenever an item gets hidden or unhidden due to it not matching or matching th...
virtual void connectTreeWidget(QTreeWidget *)
Connects signals of this listview to the appropriate slots of the search line.
void setSearchColumns(const QList< int > &columns)
Sets the list of columns to be searched.
void setTreeWidget(QTreeWidget *treeWidget)
Sets the QTreeWidget that is filtered by this search line, replacing any previously filtered listview...
void setKeepParentsVisible(bool value)
When a search is active on a list that's organized into a tree view if a parent or ancesestor of an i...
Qt::CaseSensitivity caseSensitivity() const
Returns true if the search is case sensitive.
QTreeWidget * treeWidget() const
Returns the listview that is currently filtered by the search.
Q_SCRIPTABLE Q_NOREPLY void start()
const QList< QKeySequence > & end()
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
QAbstractItemModel * model() const const
void setCheckable(bool)
bool isChecked() const const
QVariant data() const const
void setActionGroup(QActionGroup *group)
void setData(const QVariant &data)
void setExclusive(bool b)
bool sendEvent(QObject *receiver, QEvent *event)
int count() const const
bool isSectionHidden(int logicalIndex) const const
int logicalIndex(int visualIndex) const const
void setClearButtonEnabled(bool enable)
QMenu * createStandardContextMenu()
virtual bool event(QEvent *e) override
void setPlaceholderText(const QString &)
void textChanged(const QString &text)
typedef ConstIterator
void append(QList< T > &&value)
void clear()
const_iterator constBegin() const const
const_iterator constEnd() const const
bool contains(const AT &value) const const
qsizetype count() const const
T & first()
bool isEmpty() const const
qsizetype removeAll(const AT &t)
void reserve(qsizetype size)
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addMenu(QMenu *menu)
QAction * addSeparator()
QAction * exec()
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void destroyed(QObject *obj)
bool disconnect(const QMetaObject::Connection &connection)
QObject * sender() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool isNull() const const
CaseSensitivity
Key_Enter
int columnWidth(int column) const const
QTreeWidgetItem * currentItem() const const
QTreeWidgetItem * headerItem() const const
QTreeWidgetItem * itemFromIndex(const QModelIndex &index) const const
void scrollToItem(const QTreeWidgetItem *item, QAbstractItemView::ScrollHint hint)
QTreeWidgetItem * topLevelItem(int index) const const
QTreeWidgetItem * child(int index) const const
int childCount() const const
bool isHidden() const const
void setHidden(bool hide)
QString text(int column) const const
QTreeWidget * treeWidget() const const
int toInt(bool *ok) const const
void setEnabled(bool)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:17:32 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.