File indexing completed on 2024-04-28 15:51:45

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 <QApplication>
0013 #include <QContextMenuEvent>
0014 #include <QHBoxLayout>
0015 #include <QHeaderView>
0016 #include <QLabel>
0017 #include <QList>
0018 #include <QMenu>
0019 #include <QRegularExpression>
0020 #include <QTimer>
0021 #include <QToolButton>
0022 #include <QTreeView>
0023 
0024 #include <KLocalizedString>
0025 #include <KToolBar>
0026 #include <QDebug>
0027 
0028 class KTreeViewSearchLine::Private
0029 {
0030 public:
0031     explicit Private(KTreeViewSearchLine *_parent)
0032         : parent(_parent)
0033         , treeView(nullptr)
0034         , caseSensitive(Qt::CaseInsensitive)
0035         , regularExpression(false)
0036         , activeSearch(false)
0037         , queuedSearches(0)
0038     {
0039     }
0040 
0041     KTreeViewSearchLine *parent;
0042     QTreeView *treeView;
0043     Qt::CaseSensitivity caseSensitive;
0044     bool regularExpression;
0045     bool activeSearch;
0046     QString search;
0047     int queuedSearches;
0048 
0049     void rowsInserted(const QModelIndex &parent, int start, int end) const;
0050     void treeViewDeleted(QObject *object);
0051     void slotCaseSensitive();
0052     void slotRegularExpression();
0053 
0054     void checkItemParentsNotVisible(QTreeView *treeView);
0055     bool filterItems(QTreeView *treeView, const QModelIndex &index);
0056 };
0057 
0058 ////////////////////////////////////////////////////////////////////////////////
0059 // private slots
0060 ////////////////////////////////////////////////////////////////////////////////
0061 
0062 void KTreeViewSearchLine::Private::rowsInserted(const QModelIndex &parentIndex, int start, int end) const
0063 {
0064     QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(parent->sender());
0065     if (!model) {
0066         return;
0067     }
0068 
0069     QTreeView *widget = nullptr;
0070     if (treeView->model() == model) {
0071         widget = treeView;
0072     }
0073 
0074     if (!widget) {
0075         return;
0076     }
0077 
0078     for (int i = start; i <= end; ++i) {
0079         widget->setRowHidden(i, parentIndex, !parent->itemMatches(parentIndex, i, parent->text()));
0080     }
0081 }
0082 
0083 void KTreeViewSearchLine::Private::treeViewDeleted(QObject *object)
0084 {
0085     if (object == treeView) {
0086         treeView = nullptr;
0087         parent->setEnabled(false);
0088     }
0089 }
0090 
0091 void KTreeViewSearchLine::Private::slotCaseSensitive()
0092 {
0093     if (caseSensitive == Qt::CaseSensitive) {
0094         parent->setCaseSensitivity(Qt::CaseInsensitive);
0095     } else {
0096         parent->setCaseSensitivity(Qt::CaseSensitive);
0097     }
0098 
0099     parent->updateSearch();
0100 }
0101 
0102 void KTreeViewSearchLine::Private::slotRegularExpression()
0103 {
0104     if (regularExpression) {
0105         parent->setRegularExpression(false);
0106     } else {
0107         parent->setRegularExpression(true);
0108     }
0109 
0110     parent->updateSearch();
0111 }
0112 
0113 ////////////////////////////////////////////////////////////////////////////////
0114 // private methods
0115 ////////////////////////////////////////////////////////////////////////////////
0116 
0117 /** Check whether \p item, its siblings and their descendants should be shown. Show or hide the items as necessary.
0118  *
0119  *  \p item  The list view item to start showing / hiding items at. Typically, this is the first child of another item, or the
0120  *              the first child of the list view.
0121  *  \return \c true if an item which should be visible is found, \c false if all items found should be hidden. If this function
0122  *             returns true and \p highestHiddenParent was not 0, highestHiddenParent will have been shown.
0123  */
0124 bool KTreeViewSearchLine::Private::filterItems(QTreeView *treeView, const QModelIndex &index)
0125 {
0126     bool childMatch = false;
0127     const int rowcount = treeView->model()->rowCount(index);
0128     for (int i = 0; i < rowcount; ++i) {
0129         childMatch |= filterItems(treeView, treeView->model()->index(i, 0, index));
0130     }
0131 
0132     // Should this item be shown? It should if any children should be, or if it matches.
0133     const QModelIndex parentindex = index.parent();
0134     if (childMatch || parent->itemMatches(parentindex, index.row(), search)) {
0135         treeView->setRowHidden(index.row(), parentindex, false);
0136         return true;
0137     }
0138 
0139     treeView->setRowHidden(index.row(), parentindex, true);
0140 
0141     return false;
0142 }
0143 
0144 ////////////////////////////////////////////////////////////////////////////////
0145 // public methods
0146 ////////////////////////////////////////////////////////////////////////////////
0147 
0148 KTreeViewSearchLine::KTreeViewSearchLine(QWidget *parent, QTreeView *treeView)
0149     : KLineEdit(parent)
0150     , d(new Private(this))
0151 {
0152     connect(this, &KTreeViewSearchLine::textChanged, this, &KTreeViewSearchLine::queueSearch);
0153 
0154     setClearButtonEnabled(true);
0155     setTreeView(treeView);
0156 
0157     if (!treeView) {
0158         setEnabled(false);
0159     }
0160 }
0161 
0162 KTreeViewSearchLine::~KTreeViewSearchLine()
0163 {
0164     delete d;
0165 }
0166 
0167 Qt::CaseSensitivity KTreeViewSearchLine::caseSensitivity() const
0168 {
0169     return d->caseSensitive;
0170 }
0171 
0172 bool KTreeViewSearchLine::regularExpression() const
0173 {
0174     return d->regularExpression;
0175 }
0176 
0177 QTreeView *KTreeViewSearchLine::treeView() const
0178 {
0179     return d->treeView;
0180 }
0181 
0182 ////////////////////////////////////////////////////////////////////////////////
0183 // public slots
0184 ////////////////////////////////////////////////////////////////////////////////
0185 
0186 void KTreeViewSearchLine::updateSearch(const QString &pattern)
0187 {
0188     d->search = pattern.isNull() ? text() : pattern;
0189 
0190     updateSearch(d->treeView);
0191 }
0192 
0193 void KTreeViewSearchLine::updateSearch(QTreeView *treeView)
0194 {
0195     if (!treeView || !treeView->model()->rowCount()) {
0196         return;
0197     }
0198 
0199     // If there's a selected item that is visible, make sure that it's visible
0200     // when the search changes too (assuming that it still matches).
0201 
0202     QModelIndex currentIndex = treeView->currentIndex();
0203 
0204     bool wasUpdateEnabled = treeView->updatesEnabled();
0205     treeView->setUpdatesEnabled(false);
0206     d->filterItems(treeView, treeView->rootIndex());
0207     treeView->setUpdatesEnabled(wasUpdateEnabled);
0208 
0209     if (currentIndex.isValid()) {
0210         treeView->scrollTo(currentIndex);
0211     }
0212 }
0213 
0214 void KTreeViewSearchLine::setCaseSensitivity(Qt::CaseSensitivity caseSensitivity)
0215 {
0216     if (d->caseSensitive != caseSensitivity) {
0217         d->caseSensitive = caseSensitivity;
0218         updateSearch();
0219         Q_EMIT searchOptionsChanged();
0220     }
0221 }
0222 
0223 void KTreeViewSearchLine::setRegularExpression(bool value)
0224 {
0225     if (d->regularExpression != value) {
0226         d->regularExpression = value;
0227         updateSearch();
0228         Q_EMIT searchOptionsChanged();
0229     }
0230 }
0231 
0232 void KTreeViewSearchLine::setTreeView(QTreeView *treeView)
0233 {
0234     disconnectTreeView(d->treeView);
0235     d->treeView = treeView;
0236     connectTreeView(treeView);
0237 
0238     setEnabled(treeView != nullptr);
0239 }
0240 
0241 ////////////////////////////////////////////////////////////////////////////////
0242 // protected members
0243 ////////////////////////////////////////////////////////////////////////////////
0244 
0245 bool KTreeViewSearchLine::itemMatches(const QModelIndex &parentIndex, int row, const QString &pattern) const
0246 {
0247     if (pattern.isEmpty()) {
0248         return true;
0249     }
0250 
0251     if (!parentIndex.isValid() && parentIndex != d->treeView->rootIndex()) {
0252         return false;
0253     }
0254 
0255     // Construct a regular expression object with the right options.
0256     QRegularExpression re;
0257     if (d->regularExpression) {
0258         re.setPattern(pattern);
0259         re.setPatternOptions(d->caseSensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption);
0260     }
0261 
0262     // If the search column list is populated, search just the columns
0263     // specified.  If it is empty default to searching all of the columns.
0264     QAbstractItemModel *model = d->treeView->model();
0265     const int columncount = model->columnCount(parentIndex);
0266     for (int i = 0; i < columncount; ++i) {
0267         const QString str = model->data(model->index(row, i, parentIndex), Qt::DisplayRole).toString();
0268         if (d->regularExpression) {
0269             return str.contains(re);
0270         } else {
0271             return str.contains(pattern, d->caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
0272         }
0273     }
0274 
0275     return false;
0276 }
0277 
0278 void KTreeViewSearchLine::contextMenuEvent(QContextMenuEvent *event)
0279 {
0280     QMenu *popup = KLineEdit::createStandardContextMenu();
0281 
0282     popup->addSeparator();
0283     QMenu *optionsSubMenu = popup->addMenu(i18n("Search Options"));
0284     QAction *caseSensitiveAction = optionsSubMenu->addAction(i18nc("Enable case sensitive search in the side navigation panels", "Case Sensitive"), this, [this] { d->slotCaseSensitive(); });
0285     caseSensitiveAction->setCheckable(true);
0286     caseSensitiveAction->setChecked(d->caseSensitive);
0287     QAction *regularExpressionAction = optionsSubMenu->addAction(i18nc("Enable regular expression search in the side navigation panels", "Regular Expression"), this, [this] { d->slotRegularExpression(); });
0288     regularExpressionAction->setCheckable(true);
0289     regularExpressionAction->setChecked(d->regularExpression);
0290 
0291     popup->exec(event->globalPos());
0292     delete popup;
0293 }
0294 
0295 void KTreeViewSearchLine::connectTreeView(QTreeView *treeView)
0296 {
0297     if (treeView) {
0298         connect(treeView, &QTreeView::destroyed, this, &KTreeViewSearchLine::treeViewDeleted);
0299 
0300         connect(treeView->model(), &QAbstractItemModel::rowsInserted, this, &KTreeViewSearchLine::rowsInserted);
0301     }
0302 }
0303 
0304 void KTreeViewSearchLine::disconnectTreeView(QTreeView *treeView)
0305 {
0306     if (treeView) {
0307         disconnect(treeView, &QTreeView::destroyed, this, &KTreeViewSearchLine::treeViewDeleted);
0308 
0309         disconnect(treeView->model(), &QAbstractItemModel::rowsInserted, this, &KTreeViewSearchLine::rowsInserted);
0310     }
0311 }
0312 
0313 ////////////////////////////////////////////////////////////////////////////////
0314 // protected slots
0315 ////////////////////////////////////////////////////////////////////////////////
0316 
0317 void KTreeViewSearchLine::queueSearch(const QString &search)
0318 {
0319     d->queuedSearches++;
0320     d->search = search;
0321 
0322     QTimer::singleShot(200, this, &KTreeViewSearchLine::activateSearch);
0323 }
0324 
0325 void KTreeViewSearchLine::activateSearch()
0326 {
0327     --(d->queuedSearches);
0328 
0329     if (d->queuedSearches == 0) {
0330         updateSearch(d->search);
0331     }
0332 }
0333 
0334 ////////////////////////////////////////////////////////////////////////////////
0335 // private functions
0336 ////////////////////////////////////////////////////////////////////////////////
0337 
0338 void KTreeViewSearchLine::rowsInserted(const QModelIndex &parent, int start, int end) const
0339 {
0340     d->rowsInserted(parent, start, end);
0341 }
0342 
0343 void KTreeViewSearchLine::treeViewDeleted(QObject *treeView)
0344 {
0345     d->treeViewDeleted(treeView);
0346 }
0347 
0348 ////////////////////////////////////////////////////////////////////////////////
0349 // KTreeViewSearchLineWidget
0350 ////////////////////////////////////////////////////////////////////////////////
0351 
0352 class KTreeViewSearchLineWidget::Private
0353 {
0354 public:
0355     Private()
0356         : treeView(nullptr)
0357         , searchLine(nullptr)
0358     {
0359     }
0360 
0361     QTreeView *treeView;
0362     KTreeViewSearchLine *searchLine;
0363 };
0364 
0365 KTreeViewSearchLineWidget::KTreeViewSearchLineWidget(QWidget *parent, QTreeView *treeView)
0366     : QWidget(parent)
0367     , d(new Private)
0368 {
0369     d->treeView = treeView;
0370 
0371     QTimer::singleShot(0, this, &KTreeViewSearchLineWidget::createWidgets);
0372 }
0373 
0374 KTreeViewSearchLineWidget::~KTreeViewSearchLineWidget()
0375 {
0376     delete d;
0377 }
0378 
0379 KTreeViewSearchLine *KTreeViewSearchLineWidget::createSearchLine(QTreeView *treeView) const
0380 {
0381     return new KTreeViewSearchLine(const_cast<KTreeViewSearchLineWidget *>(this), treeView);
0382 }
0383 
0384 void KTreeViewSearchLineWidget::createWidgets()
0385 {
0386     QLabel *label = new QLabel(i18n("S&earch:"), this);
0387     label->setObjectName(QStringLiteral("kde toolbar widget"));
0388 
0389     searchLine()->show();
0390 
0391     label->setBuddy(d->searchLine);
0392     label->show();
0393 
0394     QHBoxLayout *layout = new QHBoxLayout(this);
0395     layout->setSpacing(5);
0396     layout->setContentsMargins(0, 0, 0, 0);
0397     layout->addWidget(label);
0398     layout->addWidget(d->searchLine);
0399 }
0400 
0401 KTreeViewSearchLine *KTreeViewSearchLineWidget::searchLine() const
0402 {
0403     if (!d->searchLine) {
0404         d->searchLine = createSearchLine(d->treeView);
0405     }
0406 
0407     return d->searchLine;
0408 }
0409 
0410 #include "moc_ktreeviewsearchline.cpp"