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"