File indexing completed on 2024-09-08 03:39:48
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"