File indexing completed on 2024-05-05 05:37:09

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