File indexing completed on 2024-04-14 03:54:02

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2003 Scott Wheeler <wheeler@kde.org>
0004     SPDX-FileCopyrightText: 2005 Rafal Rzepecki <divide@users.sourceforge.net>
0005     SPDX-FileCopyrightText: 2006 Hamish Rodda <rodda@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-only
0008 */
0009 
0010 #include "ktreewidgetsearchline.h"
0011 
0012 #include <QActionGroup>
0013 #include <QApplication>
0014 #include <QContextMenuEvent>
0015 #include <QHeaderView>
0016 #include <QList>
0017 #include <QMenu>
0018 #include <QTimer>
0019 #include <QTreeWidget>
0020 
0021 class KTreeWidgetSearchLinePrivate
0022 {
0023 public:
0024     KTreeWidgetSearchLinePrivate(KTreeWidgetSearchLine *_q)
0025         : q(_q)
0026     {
0027     }
0028 
0029     KTreeWidgetSearchLine *const q;
0030     QList<QTreeWidget *> treeWidgets;
0031     Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive;
0032     bool keepParentsVisible = true;
0033     bool canChooseColumns = true;
0034     QString search;
0035     int queuedSearches = 0;
0036     QList<int> searchColumns;
0037 
0038     void _k_rowsInserted(const QModelIndex &parent, int start, int end) const;
0039     void _k_treeWidgetDeleted(QObject *treeWidget);
0040     void _k_slotColumnActivated(QAction *action);
0041     void _k_slotAllVisibleColumns();
0042     void _k_queueSearch(const QString &);
0043     void _k_activateSearch();
0044 
0045     void checkColumns();
0046     void checkItemParentsNotVisible(QTreeWidget *treeWidget);
0047     bool checkItemParentsVisible(QTreeWidgetItem *item);
0048 };
0049 
0050 ////////////////////////////////////////////////////////////////////////////////
0051 // private slots
0052 ////////////////////////////////////////////////////////////////////////////////
0053 
0054 // Hack to make a protected method public
0055 class QTreeWidgetWorkaround : public QTreeWidget
0056 {
0057 public:
0058     QTreeWidgetItem *itemFromIndex(const QModelIndex &index) const
0059     {
0060         return QTreeWidget::itemFromIndex(index);
0061     }
0062 };
0063 
0064 void KTreeWidgetSearchLinePrivate::_k_rowsInserted(const QModelIndex &parentIndex, int start, int end) const
0065 {
0066     QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender());
0067     if (!model) {
0068         return;
0069     }
0070 
0071     QTreeWidget *widget = nullptr;
0072     for (QTreeWidget *tree : std::as_const(treeWidgets)) {
0073         if (tree->model() == model) {
0074             widget = tree;
0075             break;
0076         }
0077     }
0078 
0079     if (!widget) {
0080         return;
0081     }
0082 
0083     QTreeWidgetWorkaround *widgetW = static_cast<QTreeWidgetWorkaround *>(widget);
0084     for (int i = start; i <= end; ++i) {
0085         if (QTreeWidgetItem *item = widgetW->itemFromIndex(model->index(i, 0, parentIndex))) {
0086             bool newHidden = !q->itemMatches(item, q->text());
0087             if (item->isHidden() != newHidden) {
0088                 item->setHidden(newHidden);
0089                 Q_EMIT q->hiddenChanged(item, newHidden);
0090             }
0091         }
0092     }
0093 }
0094 
0095 void KTreeWidgetSearchLinePrivate::_k_treeWidgetDeleted(QObject *object)
0096 {
0097     treeWidgets.removeAll(static_cast<QTreeWidget *>(object));
0098     q->setEnabled(treeWidgets.isEmpty());
0099 }
0100 
0101 void KTreeWidgetSearchLinePrivate::_k_slotColumnActivated(QAction *action)
0102 {
0103     if (!action) {
0104         return;
0105     }
0106 
0107     bool ok;
0108     int column = action->data().toInt(&ok);
0109 
0110     if (!ok) {
0111         return;
0112     }
0113 
0114     if (action->isChecked()) {
0115         if (!searchColumns.isEmpty()) {
0116             if (!searchColumns.contains(column)) {
0117                 searchColumns.append(column);
0118             }
0119 
0120             if (searchColumns.count() == treeWidgets.first()->header()->count() - treeWidgets.first()->header()->hiddenSectionCount()) {
0121                 searchColumns.clear();
0122             }
0123 
0124         } else {
0125             searchColumns.append(column);
0126         }
0127     } else {
0128         if (searchColumns.isEmpty()) {
0129             QHeaderView *const header = treeWidgets.first()->header();
0130 
0131             for (int i = 0; i < header->count(); i++) {
0132                 if (i != column && !header->isSectionHidden(i)) {
0133                     searchColumns.append(i);
0134                 }
0135             }
0136 
0137         } else if (searchColumns.contains(column)) {
0138             searchColumns.removeAll(column);
0139         }
0140     }
0141 
0142     q->updateSearch();
0143 }
0144 
0145 void KTreeWidgetSearchLinePrivate::_k_slotAllVisibleColumns()
0146 {
0147     if (searchColumns.isEmpty()) {
0148         searchColumns.append(0);
0149     } else {
0150         searchColumns.clear();
0151     }
0152 
0153     q->updateSearch();
0154 }
0155 
0156 ////////////////////////////////////////////////////////////////////////////////
0157 // private methods
0158 ////////////////////////////////////////////////////////////////////////////////
0159 
0160 void KTreeWidgetSearchLinePrivate::checkColumns()
0161 {
0162     canChooseColumns = q->canChooseColumnsCheck();
0163 }
0164 
0165 void KTreeWidgetSearchLinePrivate::checkItemParentsNotVisible(QTreeWidget *treeWidget)
0166 {
0167     for (QTreeWidgetItemIterator it(treeWidget); *it; ++it) {
0168         QTreeWidgetItem *item = *it;
0169         bool newHidden = !q->itemMatches(item, search);
0170         if (item->isHidden() != newHidden) {
0171             item->setHidden(newHidden);
0172             Q_EMIT q->hiddenChanged(item, newHidden);
0173         }
0174     }
0175 }
0176 
0177 /** Check whether \p item, its siblings and their descendants should be shown. Show or hide the items as necessary.
0178  *
0179  *  \p item  The list view item to start showing / hiding items at. Typically, this is the first child of another item, or
0180  *              the first child of the list view.
0181  *  \return \c true if an item which should be visible is found, \c false if all items found should be hidden. If this function
0182  *             returns true and \p highestHiddenParent was not 0, highestHiddenParent will have been shown.
0183  */
0184 bool KTreeWidgetSearchLinePrivate::checkItemParentsVisible(QTreeWidgetItem *item)
0185 {
0186     bool childMatch = false;
0187     for (int i = 0; i < item->childCount(); ++i) {
0188         childMatch |= checkItemParentsVisible(item->child(i));
0189     }
0190 
0191     // Should this item be shown? It should if any children should be, or if it matches.
0192     bool newHidden = !childMatch && !q->itemMatches(item, search);
0193     if (item->isHidden() != newHidden) {
0194         item->setHidden(newHidden);
0195         Q_EMIT q->hiddenChanged(item, newHidden);
0196     }
0197 
0198     return !newHidden;
0199 }
0200 
0201 ////////////////////////////////////////////////////////////////////////////////
0202 // public methods
0203 ////////////////////////////////////////////////////////////////////////////////
0204 
0205 KTreeWidgetSearchLine::KTreeWidgetSearchLine(QWidget *q, QTreeWidget *treeWidget)
0206     : QLineEdit(q)
0207     , d(new KTreeWidgetSearchLinePrivate(this))
0208 {
0209     connect(this, SIGNAL(textChanged(QString)), this, SLOT(_k_queueSearch(QString)));
0210 
0211     setClearButtonEnabled(true);
0212     setPlaceholderText(tr("Search...", "@info:placeholder"));
0213     setTreeWidget(treeWidget);
0214 
0215     if (!treeWidget) {
0216         setEnabled(false);
0217     }
0218 }
0219 
0220 KTreeWidgetSearchLine::KTreeWidgetSearchLine(QWidget *q, const QList<QTreeWidget *> &treeWidgets)
0221     : QLineEdit(q)
0222     , d(new KTreeWidgetSearchLinePrivate(this))
0223 {
0224     connect(this, SIGNAL(textChanged(QString)), this, SLOT(_k_queueSearch(QString)));
0225 
0226     setClearButtonEnabled(true);
0227     setTreeWidgets(treeWidgets);
0228 }
0229 
0230 KTreeWidgetSearchLine::~KTreeWidgetSearchLine() = default;
0231 
0232 Qt::CaseSensitivity KTreeWidgetSearchLine::caseSensitivity() const
0233 {
0234     return d->caseSensitive;
0235 }
0236 
0237 QList<int> KTreeWidgetSearchLine::searchColumns() const
0238 {
0239     if (d->canChooseColumns) {
0240         return d->searchColumns;
0241     } else {
0242         return QList<int>();
0243     }
0244 }
0245 
0246 bool KTreeWidgetSearchLine::keepParentsVisible() const
0247 {
0248     return d->keepParentsVisible;
0249 }
0250 
0251 QTreeWidget *KTreeWidgetSearchLine::treeWidget() const
0252 {
0253     if (d->treeWidgets.count() == 1) {
0254         return d->treeWidgets.first();
0255     } else {
0256         return nullptr;
0257     }
0258 }
0259 
0260 QList<QTreeWidget *> KTreeWidgetSearchLine::treeWidgets() const
0261 {
0262     return d->treeWidgets;
0263 }
0264 
0265 ////////////////////////////////////////////////////////////////////////////////
0266 // public slots
0267 ////////////////////////////////////////////////////////////////////////////////
0268 
0269 void KTreeWidgetSearchLine::addTreeWidget(QTreeWidget *treeWidget)
0270 {
0271     if (treeWidget) {
0272         connectTreeWidget(treeWidget);
0273 
0274         d->treeWidgets.append(treeWidget);
0275         setEnabled(!d->treeWidgets.isEmpty());
0276 
0277         d->checkColumns();
0278     }
0279 }
0280 
0281 void KTreeWidgetSearchLine::removeTreeWidget(QTreeWidget *treeWidget)
0282 {
0283     if (treeWidget) {
0284         int index = d->treeWidgets.indexOf(treeWidget);
0285 
0286         if (index != -1) {
0287             d->treeWidgets.removeAt(index);
0288             d->checkColumns();
0289 
0290             disconnectTreeWidget(treeWidget);
0291 
0292             setEnabled(!d->treeWidgets.isEmpty());
0293         }
0294     }
0295 }
0296 
0297 void KTreeWidgetSearchLine::updateSearch(const QString &pattern)
0298 {
0299     d->search = pattern.isNull() ? text() : pattern;
0300 
0301     for (QTreeWidget *treeWidget : std::as_const(d->treeWidgets)) {
0302         updateSearch(treeWidget);
0303     }
0304 }
0305 
0306 void KTreeWidgetSearchLine::updateSearch(QTreeWidget *treeWidget)
0307 {
0308     if (!treeWidget || !treeWidget->topLevelItemCount()) {
0309         return;
0310     }
0311 
0312     // If there's a selected item that is visible, make sure that it's visible
0313     // when the search changes too (assuming that it still matches).
0314 
0315     QTreeWidgetItem *currentItem = treeWidget->currentItem();
0316 
0317     if (d->keepParentsVisible) {
0318         for (int i = 0; i < treeWidget->topLevelItemCount(); ++i) {
0319             d->checkItemParentsVisible(treeWidget->topLevelItem(i));
0320         }
0321     } else {
0322         d->checkItemParentsNotVisible(treeWidget);
0323     }
0324 
0325     if (currentItem) {
0326         treeWidget->scrollToItem(currentItem);
0327     }
0328 
0329     Q_EMIT searchUpdated(d->search);
0330 }
0331 
0332 void KTreeWidgetSearchLine::setCaseSensitivity(Qt::CaseSensitivity caseSensitive)
0333 {
0334     if (d->caseSensitive != caseSensitive) {
0335         d->caseSensitive = caseSensitive;
0336         Q_EMIT caseSensitivityChanged(d->caseSensitive);
0337         updateSearch();
0338     }
0339 }
0340 
0341 void KTreeWidgetSearchLine::setKeepParentsVisible(bool visible)
0342 {
0343     if (d->keepParentsVisible != visible) {
0344         d->keepParentsVisible = visible;
0345         Q_EMIT keepParentsVisibleChanged(d->keepParentsVisible);
0346         updateSearch();
0347     }
0348 }
0349 
0350 void KTreeWidgetSearchLine::setSearchColumns(const QList<int> &columns)
0351 {
0352     if (d->canChooseColumns) {
0353         d->searchColumns = columns;
0354     }
0355 }
0356 
0357 void KTreeWidgetSearchLine::setTreeWidget(QTreeWidget *treeWidget)
0358 {
0359     setTreeWidgets(QList<QTreeWidget *>());
0360     addTreeWidget(treeWidget);
0361 }
0362 
0363 void KTreeWidgetSearchLine::setTreeWidgets(const QList<QTreeWidget *> &treeWidgets)
0364 {
0365     for (QTreeWidget *treeWidget : std::as_const(d->treeWidgets)) {
0366         disconnectTreeWidget(treeWidget);
0367     }
0368 
0369     d->treeWidgets = treeWidgets;
0370 
0371     for (QTreeWidget *treeWidget : std::as_const(d->treeWidgets)) {
0372         connectTreeWidget(treeWidget);
0373     }
0374 
0375     d->checkColumns();
0376 
0377     setEnabled(!d->treeWidgets.isEmpty());
0378 }
0379 
0380 ////////////////////////////////////////////////////////////////////////////////
0381 // protected members
0382 ////////////////////////////////////////////////////////////////////////////////
0383 
0384 bool KTreeWidgetSearchLine::itemMatches(const QTreeWidgetItem *item, const QString &pattern) const
0385 {
0386     if (pattern.isEmpty()) {
0387         return true;
0388     }
0389 
0390     // If the search column list is populated, search just the columns
0391     // specified.  If it is empty default to searching all of the columns.
0392 
0393     if (!d->searchColumns.isEmpty()) {
0394         QList<int>::ConstIterator it = d->searchColumns.constBegin();
0395         for (; it != d->searchColumns.constEnd(); ++it) {
0396             if (*it < item->treeWidget()->columnCount() //
0397                 && item->text(*it).indexOf(pattern, 0, d->caseSensitive) >= 0) {
0398                 return true;
0399             }
0400         }
0401     } else {
0402         for (int i = 0; i < item->treeWidget()->columnCount(); i++) {
0403             if (item->treeWidget()->columnWidth(i) > 0 //
0404                 && item->text(i).indexOf(pattern, 0, d->caseSensitive) >= 0) {
0405                 return true;
0406             }
0407         }
0408     }
0409 
0410     return false;
0411 }
0412 
0413 void KTreeWidgetSearchLine::contextMenuEvent(QContextMenuEvent *event)
0414 {
0415     QMenu *popup = QLineEdit::createStandardContextMenu();
0416 
0417     if (d->canChooseColumns) {
0418         popup->addSeparator();
0419         QMenu *subMenu = popup->addMenu(tr("Search Columns", "@title:menu"));
0420 
0421         QAction *allVisibleColumnsAction = subMenu->addAction(tr("All Visible Columns", "@optipn:check"), this, SLOT(_k_slotAllVisibleColumns()));
0422         allVisibleColumnsAction->setCheckable(true);
0423         allVisibleColumnsAction->setChecked(d->searchColumns.isEmpty());
0424         subMenu->addSeparator();
0425 
0426         bool allColumnsAreSearchColumns = true;
0427 
0428         QActionGroup *group = new QActionGroup(popup);
0429         group->setExclusive(false);
0430         connect(group, SIGNAL(triggered(QAction *)), SLOT(_k_slotColumnActivated(QAction *)));
0431 
0432         QHeaderView *const header = d->treeWidgets.first()->header();
0433         for (int j = 0; j < header->count(); j++) {
0434             int i = header->logicalIndex(j);
0435 
0436             if (header->isSectionHidden(i)) {
0437                 continue;
0438             }
0439 
0440             QString columnText = d->treeWidgets.first()->headerItem()->text(i);
0441             QAction *columnAction = subMenu->addAction(d->treeWidgets.first()->headerItem()->icon(i), columnText);
0442             columnAction->setCheckable(true);
0443             columnAction->setChecked(d->searchColumns.isEmpty() || d->searchColumns.contains(i));
0444             columnAction->setData(i);
0445             columnAction->setActionGroup(group);
0446 
0447             if (d->searchColumns.isEmpty() || d->searchColumns.indexOf(i) != -1) {
0448                 columnAction->setChecked(true);
0449             } else {
0450                 allColumnsAreSearchColumns = false;
0451             }
0452         }
0453 
0454         allVisibleColumnsAction->setChecked(allColumnsAreSearchColumns);
0455 
0456         // searchColumnsMenuActivated() relies on one possible "all" representation
0457         if (allColumnsAreSearchColumns && !d->searchColumns.isEmpty()) {
0458             d->searchColumns.clear();
0459         }
0460     }
0461 
0462     popup->exec(event->globalPos());
0463     delete popup;
0464 }
0465 
0466 void KTreeWidgetSearchLine::connectTreeWidget(QTreeWidget *treeWidget)
0467 {
0468     connect(treeWidget, SIGNAL(destroyed(QObject *)), this, SLOT(_k_treeWidgetDeleted(QObject *)));
0469 
0470     connect(treeWidget->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(_k_rowsInserted(QModelIndex, int, int)));
0471 }
0472 
0473 void KTreeWidgetSearchLine::disconnectTreeWidget(QTreeWidget *treeWidget)
0474 {
0475     disconnect(treeWidget, SIGNAL(destroyed(QObject *)), this, SLOT(_k_treeWidgetDeleted(QObject *)));
0476 
0477     disconnect(treeWidget->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(_k_rowsInserted(QModelIndex, int, int)));
0478 }
0479 
0480 bool KTreeWidgetSearchLine::canChooseColumnsCheck()
0481 {
0482     // This is true if either of the following is true:
0483 
0484     // there are no listviews connected
0485     if (d->treeWidgets.isEmpty()) {
0486         return false;
0487     }
0488 
0489     const QTreeWidget *first = d->treeWidgets.first();
0490 
0491     const int numcols = first->columnCount();
0492     // the listviews have only one column,
0493     if (numcols < 2) {
0494         return false;
0495     }
0496 
0497     QStringList headers;
0498     headers.reserve(numcols);
0499     for (int i = 0; i < numcols; ++i) {
0500         headers.append(first->headerItem()->text(i));
0501     }
0502 
0503     QList<QTreeWidget *>::ConstIterator it = d->treeWidgets.constBegin();
0504     for (++it /* skip the first one */; it != d->treeWidgets.constEnd(); ++it) {
0505         // the listviews have different numbers of columns,
0506         if ((*it)->columnCount() != numcols) {
0507             return false;
0508         }
0509 
0510         // the listviews differ in column labels.
0511         QStringList::ConstIterator jt;
0512         int i;
0513         for (i = 0, jt = headers.constBegin(); i < numcols; ++i, ++jt) {
0514             Q_ASSERT(jt != headers.constEnd());
0515 
0516             if ((*it)->headerItem()->text(i) != *jt) {
0517                 return false;
0518             }
0519         }
0520     }
0521 
0522     return true;
0523 }
0524 
0525 bool KTreeWidgetSearchLine::event(QEvent *event)
0526 {
0527     if (event->type() == QEvent::KeyPress) {
0528         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0529         if (keyEvent->matches(QKeySequence::MoveToNextLine) || keyEvent->matches(QKeySequence::SelectNextLine)
0530             || keyEvent->matches(QKeySequence::MoveToPreviousLine) || keyEvent->matches(QKeySequence::SelectPreviousLine)
0531             || keyEvent->matches(QKeySequence::MoveToNextPage) || keyEvent->matches(QKeySequence::SelectNextPage)
0532             || keyEvent->matches(QKeySequence::MoveToPreviousPage) || keyEvent->matches(QKeySequence::SelectPreviousPage) || keyEvent->key() == Qt::Key_Enter
0533             || keyEvent->key() == Qt::Key_Return) {
0534             QTreeWidget *first = d->treeWidgets.first();
0535             if (first) {
0536                 QApplication::sendEvent(first, event);
0537                 return true;
0538             }
0539         }
0540     }
0541     return QLineEdit::event(event);
0542 }
0543 
0544 ////////////////////////////////////////////////////////////////////////////////
0545 // protected slots
0546 ////////////////////////////////////////////////////////////////////////////////
0547 
0548 void KTreeWidgetSearchLinePrivate::_k_queueSearch(const QString &_search)
0549 {
0550     queuedSearches++;
0551     search = _search;
0552 
0553     QTimer::singleShot(200, q, SLOT(_k_activateSearch()));
0554 }
0555 
0556 void KTreeWidgetSearchLinePrivate::_k_activateSearch()
0557 {
0558     --queuedSearches;
0559 
0560     if (queuedSearches == 0) {
0561         q->updateSearch(search);
0562     }
0563 }
0564 
0565 #include "moc_ktreewidgetsearchline.cpp"