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 | |
21 | class KTreeWidgetSearchLinePrivate |
22 | { |
23 | public: |
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 |
55 | class QTreeWidgetWorkaround : public QTreeWidget |
56 | { |
57 | public: |
58 | QTreeWidgetItem *itemFromIndex(const QModelIndex &index) const |
59 | { |
60 | return QTreeWidget::itemFromIndex(index); |
61 | } |
62 | }; |
63 | |
64 | void 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 | |
95 | void KTreeWidgetSearchLinePrivate::_k_treeWidgetDeleted(QObject *object) |
96 | { |
97 | treeWidgets.removeAll(t: static_cast<QTreeWidget *>(object)); |
98 | q->setEnabled(treeWidgets.isEmpty()); |
99 | } |
100 | |
101 | void 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 = 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 | |
145 | void 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 | |
160 | void KTreeWidgetSearchLinePrivate::checkColumns() |
161 | { |
162 | canChooseColumns = q->canChooseColumnsCheck(); |
163 | } |
164 | |
165 | void 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 | */ |
185 | bool 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 | |
206 | KTreeWidgetSearchLine::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 | |
221 | KTreeWidgetSearchLine::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 | |
231 | KTreeWidgetSearchLine::~KTreeWidgetSearchLine() = default; |
232 | |
233 | Qt::CaseSensitivity KTreeWidgetSearchLine::caseSensitivity() const |
234 | { |
235 | return d->caseSensitive; |
236 | } |
237 | |
238 | QList<int> KTreeWidgetSearchLine::searchColumns() const |
239 | { |
240 | if (d->canChooseColumns) { |
241 | return d->searchColumns; |
242 | } else { |
243 | return QList<int>(); |
244 | } |
245 | } |
246 | |
247 | bool KTreeWidgetSearchLine::keepParentsVisible() const |
248 | { |
249 | return d->keepParentsVisible; |
250 | } |
251 | |
252 | QTreeWidget *KTreeWidgetSearchLine::treeWidget() const |
253 | { |
254 | if (d->treeWidgets.count() == 1) { |
255 | return d->treeWidgets.first(); |
256 | } else { |
257 | return nullptr; |
258 | } |
259 | } |
260 | |
261 | QList<QTreeWidget *> KTreeWidgetSearchLine::treeWidgets() const |
262 | { |
263 | return d->treeWidgets; |
264 | } |
265 | |
266 | //////////////////////////////////////////////////////////////////////////////// |
267 | // public slots |
268 | //////////////////////////////////////////////////////////////////////////////// |
269 | |
270 | void 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 | |
282 | void 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 | |
298 | void 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 | |
307 | void 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 | |
333 | void 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 | |
342 | void 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 | |
351 | void KTreeWidgetSearchLine::setSearchColumns(const QList<int> &columns) |
352 | { |
353 | if (d->canChooseColumns) { |
354 | d->searchColumns = columns; |
355 | } |
356 | } |
357 | |
358 | void KTreeWidgetSearchLine::setTreeWidget(QTreeWidget *treeWidget) |
359 | { |
360 | setTreeWidgets(QList<QTreeWidget *>()); |
361 | addTreeWidget(treeWidget); |
362 | } |
363 | |
364 | void 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 | |
385 | bool 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 | |
414 | void KTreeWidgetSearchLine::(QContextMenuEvent *event) |
415 | { |
416 | QMenu * = QLineEdit::createStandardContextMenu(); |
417 | |
418 | if (d->canChooseColumns) { |
419 | popup->addSeparator(); |
420 | QMenu * = 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 = 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 | |
467 | void 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 | |
474 | void 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 | |
481 | bool 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 ; |
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 | |
526 | bool 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 | |
549 | void 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 | |
557 | void 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 | |