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 | |
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 | /** 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 | */ |
184 | bool 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 | |
205 | KTreeWidgetSearchLine::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 | |
220 | KTreeWidgetSearchLine::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 | |
230 | KTreeWidgetSearchLine::~KTreeWidgetSearchLine() = default; |
231 | |
232 | Qt::CaseSensitivity KTreeWidgetSearchLine::caseSensitivity() const |
233 | { |
234 | return d->caseSensitive; |
235 | } |
236 | |
237 | QList<int> KTreeWidgetSearchLine::searchColumns() const |
238 | { |
239 | if (d->canChooseColumns) { |
240 | return d->searchColumns; |
241 | } else { |
242 | return QList<int>(); |
243 | } |
244 | } |
245 | |
246 | bool KTreeWidgetSearchLine::keepParentsVisible() const |
247 | { |
248 | return d->keepParentsVisible; |
249 | } |
250 | |
251 | QTreeWidget *KTreeWidgetSearchLine::treeWidget() const |
252 | { |
253 | if (d->treeWidgets.count() == 1) { |
254 | return d->treeWidgets.first(); |
255 | } else { |
256 | return nullptr; |
257 | } |
258 | } |
259 | |
260 | QList<QTreeWidget *> KTreeWidgetSearchLine::treeWidgets() const |
261 | { |
262 | return d->treeWidgets; |
263 | } |
264 | |
265 | //////////////////////////////////////////////////////////////////////////////// |
266 | // public slots |
267 | //////////////////////////////////////////////////////////////////////////////// |
268 | |
269 | void 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 | |
281 | void 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 | |
297 | void 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 | |
306 | void 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 | |
332 | void 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 | |
341 | void 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 | |
350 | void KTreeWidgetSearchLine::setSearchColumns(const QList<int> &columns) |
351 | { |
352 | if (d->canChooseColumns) { |
353 | d->searchColumns = columns; |
354 | } |
355 | } |
356 | |
357 | void KTreeWidgetSearchLine::setTreeWidget(QTreeWidget *treeWidget) |
358 | { |
359 | setTreeWidgets(QList<QTreeWidget *>()); |
360 | addTreeWidget(treeWidget); |
361 | } |
362 | |
363 | void 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 | |
384 | bool 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 | |
413 | void KTreeWidgetSearchLine::(QContextMenuEvent *event) |
414 | { |
415 | QMenu * = QLineEdit::createStandardContextMenu(); |
416 | |
417 | if (d->canChooseColumns) { |
418 | popup->addSeparator(); |
419 | QMenu * = 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 = 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 | |
466 | void 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 | |
473 | void 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 | |
480 | bool 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 ; |
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 | |
525 | bool 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 | |
548 | void 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 | |
556 | void 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 | |