File indexing completed on 2024-10-13 12:16: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         updateSearch();
0337     }
0338 }
0339 
0340 void KTreeWidgetSearchLine::setKeepParentsVisible(bool visible)
0341 {
0342     if (d->keepParentsVisible != visible) {
0343         d->keepParentsVisible = visible;
0344         updateSearch();
0345     }
0346 }
0347 
0348 void KTreeWidgetSearchLine::setSearchColumns(const QList<int> &columns)
0349 {
0350     if (d->canChooseColumns) {
0351         d->searchColumns = columns;
0352     }
0353 }
0354 
0355 void KTreeWidgetSearchLine::setTreeWidget(QTreeWidget *treeWidget)
0356 {
0357     setTreeWidgets(QList<QTreeWidget *>());
0358     addTreeWidget(treeWidget);
0359 }
0360 
0361 void KTreeWidgetSearchLine::setTreeWidgets(const QList<QTreeWidget *> &treeWidgets)
0362 {
0363     for (QTreeWidget *treeWidget : std::as_const(d->treeWidgets)) {
0364         disconnectTreeWidget(treeWidget);
0365     }
0366 
0367     d->treeWidgets = treeWidgets;
0368 
0369     for (QTreeWidget *treeWidget : std::as_const(d->treeWidgets)) {
0370         connectTreeWidget(treeWidget);
0371     }
0372 
0373     d->checkColumns();
0374 
0375     setEnabled(!d->treeWidgets.isEmpty());
0376 }
0377 
0378 ////////////////////////////////////////////////////////////////////////////////
0379 // protected members
0380 ////////////////////////////////////////////////////////////////////////////////
0381 
0382 bool KTreeWidgetSearchLine::itemMatches(const QTreeWidgetItem *item, const QString &pattern) const
0383 {
0384     if (pattern.isEmpty()) {
0385         return true;
0386     }
0387 
0388     // If the search column list is populated, search just the columns
0389     // specified.  If it is empty default to searching all of the columns.
0390 
0391     if (!d->searchColumns.isEmpty()) {
0392         QList<int>::ConstIterator it = d->searchColumns.constBegin();
0393         for (; it != d->searchColumns.constEnd(); ++it) {
0394             if (*it < item->treeWidget()->columnCount() //
0395                 && item->text(*it).indexOf(pattern, 0, d->caseSensitive) >= 0) {
0396                 return true;
0397             }
0398         }
0399     } else {
0400         for (int i = 0; i < item->treeWidget()->columnCount(); i++) {
0401             if (item->treeWidget()->columnWidth(i) > 0 //
0402                 && item->text(i).indexOf(pattern, 0, d->caseSensitive) >= 0) {
0403                 return true;
0404             }
0405         }
0406     }
0407 
0408     return false;
0409 }
0410 
0411 void KTreeWidgetSearchLine::contextMenuEvent(QContextMenuEvent *event)
0412 {
0413     QMenu *popup = QLineEdit::createStandardContextMenu();
0414 
0415     if (d->canChooseColumns) {
0416         popup->addSeparator();
0417         QMenu *subMenu = popup->addMenu(tr("Search Columns", "@title:menu"));
0418 
0419         QAction *allVisibleColumnsAction = subMenu->addAction(tr("All Visible Columns", "@optipn:check"), this, SLOT(_k_slotAllVisibleColumns()));
0420         allVisibleColumnsAction->setCheckable(true);
0421         allVisibleColumnsAction->setChecked(d->searchColumns.isEmpty());
0422         subMenu->addSeparator();
0423 
0424         bool allColumnsAreSearchColumns = true;
0425 
0426         QActionGroup *group = new QActionGroup(popup);
0427         group->setExclusive(false);
0428         connect(group, SIGNAL(triggered(QAction *)), SLOT(_k_slotColumnActivated(QAction *)));
0429 
0430         QHeaderView *const header = d->treeWidgets.first()->header();
0431         for (int j = 0; j < header->count(); j++) {
0432             int i = header->logicalIndex(j);
0433 
0434             if (header->isSectionHidden(i)) {
0435                 continue;
0436             }
0437 
0438             QString columnText = d->treeWidgets.first()->headerItem()->text(i);
0439             QAction *columnAction = subMenu->addAction(d->treeWidgets.first()->headerItem()->icon(i), columnText);
0440             columnAction->setCheckable(true);
0441             columnAction->setChecked(d->searchColumns.isEmpty() || d->searchColumns.contains(i));
0442             columnAction->setData(i);
0443             columnAction->setActionGroup(group);
0444 
0445             if (d->searchColumns.isEmpty() || d->searchColumns.indexOf(i) != -1) {
0446                 columnAction->setChecked(true);
0447             } else {
0448                 allColumnsAreSearchColumns = false;
0449             }
0450         }
0451 
0452         allVisibleColumnsAction->setChecked(allColumnsAreSearchColumns);
0453 
0454         // searchColumnsMenuActivated() relies on one possible "all" representation
0455         if (allColumnsAreSearchColumns && !d->searchColumns.isEmpty()) {
0456             d->searchColumns.clear();
0457         }
0458     }
0459 
0460     popup->exec(event->globalPos());
0461     delete popup;
0462 }
0463 
0464 void KTreeWidgetSearchLine::connectTreeWidget(QTreeWidget *treeWidget)
0465 {
0466     connect(treeWidget, SIGNAL(destroyed(QObject *)), this, SLOT(_k_treeWidgetDeleted(QObject *)));
0467 
0468     connect(treeWidget->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(_k_rowsInserted(QModelIndex, int, int)));
0469 }
0470 
0471 void KTreeWidgetSearchLine::disconnectTreeWidget(QTreeWidget *treeWidget)
0472 {
0473     disconnect(treeWidget, SIGNAL(destroyed(QObject *)), this, SLOT(_k_treeWidgetDeleted(QObject *)));
0474 
0475     disconnect(treeWidget->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(_k_rowsInserted(QModelIndex, int, int)));
0476 }
0477 
0478 bool KTreeWidgetSearchLine::canChooseColumnsCheck()
0479 {
0480     // This is true if either of the following is true:
0481 
0482     // there are no listviews connected
0483     if (d->treeWidgets.isEmpty()) {
0484         return false;
0485     }
0486 
0487     const QTreeWidget *first = d->treeWidgets.first();
0488 
0489     const int numcols = first->columnCount();
0490     // the listviews have only one column,
0491     if (numcols < 2) {
0492         return false;
0493     }
0494 
0495     QStringList headers;
0496     headers.reserve(numcols);
0497     for (int i = 0; i < numcols; ++i) {
0498         headers.append(first->headerItem()->text(i));
0499     }
0500 
0501     QList<QTreeWidget *>::ConstIterator it = d->treeWidgets.constBegin();
0502     for (++it /* skip the first one */; it != d->treeWidgets.constEnd(); ++it) {
0503         // the listviews have different numbers of columns,
0504         if ((*it)->columnCount() != numcols) {
0505             return false;
0506         }
0507 
0508         // the listviews differ in column labels.
0509         QStringList::ConstIterator jt;
0510         int i;
0511         for (i = 0, jt = headers.constBegin(); i < numcols; ++i, ++jt) {
0512             Q_ASSERT(jt != headers.constEnd());
0513 
0514             if ((*it)->headerItem()->text(i) != *jt) {
0515                 return false;
0516             }
0517         }
0518     }
0519 
0520     return true;
0521 }
0522 
0523 bool KTreeWidgetSearchLine::event(QEvent *event)
0524 {
0525     if (event->type() == QEvent::KeyPress) {
0526         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0527         if (keyEvent->matches(QKeySequence::MoveToNextLine) || keyEvent->matches(QKeySequence::SelectNextLine)
0528             || keyEvent->matches(QKeySequence::MoveToPreviousLine) || keyEvent->matches(QKeySequence::SelectPreviousLine)
0529             || keyEvent->matches(QKeySequence::MoveToNextPage) || keyEvent->matches(QKeySequence::SelectNextPage)
0530             || keyEvent->matches(QKeySequence::MoveToPreviousPage) || keyEvent->matches(QKeySequence::SelectPreviousPage) || keyEvent->key() == Qt::Key_Enter
0531             || keyEvent->key() == Qt::Key_Return) {
0532             QTreeWidget *first = d->treeWidgets.first();
0533             if (first) {
0534                 QApplication::sendEvent(first, event);
0535                 return true;
0536             }
0537         }
0538     }
0539     return QLineEdit::event(event);
0540 }
0541 
0542 ////////////////////////////////////////////////////////////////////////////////
0543 // protected slots
0544 ////////////////////////////////////////////////////////////////////////////////
0545 
0546 void KTreeWidgetSearchLinePrivate::_k_queueSearch(const QString &_search)
0547 {
0548     queuedSearches++;
0549     search = _search;
0550 
0551     QTimer::singleShot(200, q, SLOT(_k_activateSearch()));
0552 }
0553 
0554 void KTreeWidgetSearchLinePrivate::_k_activateSearch()
0555 {
0556     --queuedSearches;
0557 
0558     if (queuedSearches == 0) {
0559         q->updateSearch(search);
0560     }
0561 }
0562 
0563 #include "moc_ktreewidgetsearchline.cpp"