File indexing completed on 2024-04-21 05:50:23

0001 /* This file is part of the KDE project
0002    Copyright (C) 2005 Daniel Teske <teske@squorn.de>
0003 
0004    This program is free software; you can redistribute it and/or
0005    modify it under the terms of the GNU General Public
0006    License version 2 or at your option version 3 as published by
0007    the Free Software Foundation.
0008 
0009    This program is distributed in the hope that it will be useful,
0010    but WITHOUT ANY WARRANTY; without even the implied warranty of
0011    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0012    General Public License for more details.
0013 
0014    You should have received a copy of the GNU General Public License
0015    along with this program; see the file COPYING.  If not, write to
0016    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0017    Boston, MA 02110-1301, USA.
0018 */
0019 
0020 #include "kebsearchline.h"
0021 #include "keditbookmarks_debug.h"
0022 #include <QContextMenuEvent>
0023 #include <QHBoxLayout>
0024 #include <QLabel>
0025 #include <QListView>
0026 #include <QTimer>
0027 #include <QTreeView>
0028 
0029 #include <KLocalizedString>
0030 
0031 #include <QHeaderView>
0032 #include <QMenu>
0033 
0034 ////////////////////////////////////////////////////////////////////////////////
0035 // public methods
0036 ////////////////////////////////////////////////////////////////////////////////
0037 class KViewSearchLine::KViewSearchLinePrivate
0038 {
0039 public:
0040     KViewSearchLinePrivate()
0041         : listView(nullptr)
0042         , treeView(nullptr)
0043         , caseSensitive(false)
0044         , activeSearch(false)
0045         , keepParentsVisible(true)
0046         , queuedSearches(0)
0047     {
0048     }
0049 
0050     QListView *listView;
0051     QTreeView *treeView;
0052     bool caseSensitive;
0053     bool activeSearch;
0054     bool keepParentsVisible;
0055     QString search;
0056     int queuedSearches;
0057     QList<int> searchColumns;
0058 };
0059 
0060 KViewSearchLine::KViewSearchLine(QWidget *parent, QAbstractItemView *v)
0061     : KLineEdit(parent)
0062 {
0063     d = new KViewSearchLinePrivate;
0064 
0065     setClearButtonEnabled(true);
0066 
0067     d->treeView = dynamic_cast<QTreeView *>(v);
0068     d->listView = dynamic_cast<QListView *>(v);
0069 
0070     connect(this, &KViewSearchLine::textChanged, this, &KViewSearchLine::queueSearch);
0071 
0072     if (view()) {
0073         connect(view(), &QObject::destroyed, this, &KViewSearchLine::listViewDeleted);
0074         connect(model(), &QAbstractItemModel::dataChanged, this, &KViewSearchLine::slotDataChanged);
0075         connect(model(), &QAbstractItemModel::rowsInserted, this, &KViewSearchLine::slotRowsInserted);
0076         connect(model(), &QAbstractItemModel::rowsRemoved, this, &KViewSearchLine::slotRowsRemoved);
0077         connect(model(), &QAbstractItemModel::columnsInserted, this, &KViewSearchLine::slotColumnsInserted);
0078         connect(model(), &QAbstractItemModel::columnsRemoved, this, &KViewSearchLine::slotColumnsRemoved);
0079         connect(model(), &QAbstractItemModel::modelReset, this, &KViewSearchLine::slotModelReset);
0080     } else
0081         setEnabled(false);
0082 }
0083 
0084 KViewSearchLine::KViewSearchLine(QWidget *parent)
0085     : KLineEdit(parent)
0086 {
0087     d = new KViewSearchLinePrivate;
0088 
0089     setClearButtonEnabled(true);
0090 
0091     d->treeView = nullptr;
0092     d->listView = nullptr;
0093 
0094     connect(this, &KViewSearchLine::textChanged, this, &KViewSearchLine::queueSearch);
0095 
0096     setEnabled(false);
0097 }
0098 
0099 KViewSearchLine::~KViewSearchLine()
0100 {
0101     delete d;
0102 }
0103 
0104 QAbstractItemView *KViewSearchLine::view() const
0105 {
0106     if (d->treeView)
0107         return d->treeView;
0108     else
0109         return d->listView;
0110 }
0111 
0112 bool KViewSearchLine::caseSensitive() const
0113 {
0114     return d->caseSensitive;
0115 }
0116 
0117 bool KViewSearchLine::keepParentsVisible() const
0118 {
0119     return d->keepParentsVisible;
0120 }
0121 
0122 ////////////////////////////////////////////////////////////////////////////////
0123 // public slots
0124 ////////////////////////////////////////////////////////////////////////////////
0125 
0126 void KViewSearchLine::updateSearch(const QString &s)
0127 {
0128     if (!view())
0129         return;
0130 
0131     d->search = s.isNull() ? text() : s;
0132 
0133     // If there's a selected item that is visible, make sure that it's visible
0134     // when the search changes too (assuming that it still matches).
0135     // FIXME reimplement
0136 
0137     if (d->keepParentsVisible)
0138         checkItemParentsVisible(model()->index(0, 0, QModelIndex()));
0139     else
0140         checkItemParentsNotVisible();
0141 }
0142 
0143 void KViewSearchLine::setCaseSensitive(bool cs)
0144 {
0145     d->caseSensitive = cs;
0146 }
0147 
0148 void KViewSearchLine::setKeepParentsVisible(bool v)
0149 {
0150     d->keepParentsVisible = v;
0151 }
0152 
0153 void KViewSearchLine::setSearchColumns(const QList<int> &columns)
0154 {
0155     d->searchColumns = columns;
0156 }
0157 
0158 void KViewSearchLine::setView(QAbstractItemView *v)
0159 {
0160     if (view()) {
0161         disconnect(view(), &QObject::destroyed, this, &KViewSearchLine::listViewDeleted);
0162         disconnect(model(), &QAbstractItemModel::dataChanged, this, &KViewSearchLine::slotDataChanged);
0163         disconnect(model(), &QAbstractItemModel::rowsInserted, this, &KViewSearchLine::slotRowsInserted);
0164         disconnect(model(), &QAbstractItemModel::rowsRemoved, this, &KViewSearchLine::slotRowsRemoved);
0165         disconnect(model(), &QAbstractItemModel::columnsInserted, this, &KViewSearchLine::slotColumnsInserted);
0166         disconnect(model(), &QAbstractItemModel::columnsRemoved, this, &KViewSearchLine::slotColumnsRemoved);
0167         disconnect(model(), &QAbstractItemModel::modelReset, this, &KViewSearchLine::slotModelReset);
0168     }
0169 
0170     d->treeView = dynamic_cast<QTreeView *>(v);
0171     d->listView = dynamic_cast<QListView *>(v);
0172 
0173     if (view()) {
0174         connect(view(), &QObject::destroyed, this, &KViewSearchLine::listViewDeleted);
0175 
0176         connect(model(), &QAbstractItemModel::dataChanged, this, &KViewSearchLine::slotDataChanged);
0177         connect(model(), &QAbstractItemModel::rowsInserted, this, &KViewSearchLine::slotRowsInserted);
0178         connect(model(), &QAbstractItemModel::rowsRemoved, this, &KViewSearchLine::slotRowsRemoved);
0179         connect(model(), &QAbstractItemModel::columnsInserted, this, &KViewSearchLine::slotColumnsInserted);
0180         connect(model(), &QAbstractItemModel::columnsRemoved, this, &KViewSearchLine::slotColumnsRemoved);
0181         connect(model(), &QAbstractItemModel::modelReset, this, &KViewSearchLine::slotModelReset);
0182     }
0183 
0184     setEnabled(bool(view()));
0185 }
0186 
0187 ////////////////////////////////////////////////////////////////////////////////
0188 // protected members
0189 ////////////////////////////////////////////////////////////////////////////////
0190 
0191 bool KViewSearchLine::itemMatches(const QModelIndex &item, const QString &s) const
0192 {
0193     if (s.isEmpty())
0194         return true;
0195 
0196     // If the search column list is populated, search just the columns
0197     // specified.  If it is empty default to searching all of the columns.
0198     if (d->treeView) {
0199         int columnCount = d->treeView->header()->count();
0200         int row = item.row();
0201         QModelIndex parent = item.parent();
0202         if (!d->searchColumns.isEmpty()) {
0203             QList<int>::const_iterator it, end;
0204             end = d->searchColumns.constEnd();
0205             for (it = d->searchColumns.constBegin(); it != end; ++it) {
0206                 if (*it < columnCount) {
0207                     const QString &text = model()->data(model()->index(row, *it, parent)).toString();
0208                     if (text.indexOf(s, 0, d->caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive) >= 0)
0209                         return true;
0210                 }
0211             }
0212         } else {
0213             for (int i = 0; i < columnCount; i++) {
0214                 if (d->treeView->isColumnHidden(i) == false) {
0215                     const QString &text = model()->data(model()->index(row, i, parent)).toString();
0216                     if (text.indexOf(s, 0, d->caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive) >= 0)
0217                         return true;
0218                 }
0219             }
0220         }
0221         return false;
0222     } else {
0223         QString text = model()->data(item).toString();
0224         if (text.indexOf(s, 0, d->caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive) >= 0)
0225             return true;
0226         else
0227             return false;
0228     }
0229 }
0230 
0231 void KViewSearchLine::contextMenuEvent(QContextMenuEvent *e)
0232 {
0233     qDeleteAll(actions);
0234     QMenu *popup = KLineEdit::createStandardContextMenu();
0235     if (d->treeView) {
0236         int columnCount = d->treeView->header()->count();
0237         actions.resize(columnCount + 1);
0238         if (columnCount) {
0239             QMenu *submenu = new QMenu(i18n("Search Columns"), popup);
0240             popup->addMenu(submenu);
0241             bool allVisibleColumsCheked = true;
0242             QAction *allVisibleAct = new QAction(i18n("All Visible Columns"), nullptr);
0243             allVisibleAct->setCheckable(true);
0244             submenu->addAction(allVisibleAct);
0245             submenu->addSeparator();
0246             for (int i = 0; i < columnCount; ++i) {
0247                 int logicalIndex = d->treeView->header()->logicalIndex(i);
0248                 QString columnText = model()->headerData(logicalIndex, Qt::Horizontal).toString();
0249                 if (columnText.isEmpty())
0250                     columnText = i18nc("Column number %1", "Column No. %1", i);
0251                 QAction *act = new QAction(columnText, nullptr);
0252                 act->setCheckable(true);
0253                 if (d->searchColumns.isEmpty() || d->searchColumns.contains(logicalIndex))
0254                     act->setChecked(true);
0255 
0256                 actions[logicalIndex] = act;
0257                 if (!d->treeView || (d->treeView->isColumnHidden(i) == false)) {
0258                     submenu->addAction(act);
0259                     allVisibleColumsCheked = allVisibleColumsCheked && act->isChecked();
0260                 }
0261             }
0262             actions[columnCount] = allVisibleAct;
0263             if (d->searchColumns.isEmpty() || allVisibleColumsCheked) {
0264                 allVisibleAct->setChecked(true);
0265                 d->searchColumns.clear();
0266             }
0267             connect(submenu, &QMenu::triggered, this, &KViewSearchLine::searchColumnsMenuActivated);
0268         }
0269     }
0270     popup->exec(e->globalPos());
0271     delete popup;
0272 }
0273 
0274 ////////////////////////////////////////////////////////////////////////////////
0275 // protected slots
0276 ////////////////////////////////////////////////////////////////////////////////
0277 
0278 void KViewSearchLine::queueSearch(const QString &search)
0279 {
0280     d->queuedSearches++;
0281     d->search = search;
0282     QTimer::singleShot(200, this, &KViewSearchLine::activateSearch);
0283 }
0284 
0285 void KViewSearchLine::activateSearch()
0286 {
0287     --(d->queuedSearches);
0288 
0289     if (d->queuedSearches == 0)
0290         updateSearch(d->search);
0291 }
0292 
0293 ////////////////////////////////////////////////////////////////////////////////
0294 // private slots
0295 ////////////////////////////////////////////////////////////////////////////////
0296 
0297 void KViewSearchLine::listViewDeleted()
0298 {
0299     d->treeView = nullptr;
0300     d->listView = nullptr;
0301     setEnabled(false);
0302 }
0303 
0304 void KViewSearchLine::searchColumnsMenuActivated(QAction *action)
0305 {
0306     int index = 0;
0307     int count = actions.count();
0308     for (int i = 0; i < count; ++i) {
0309         if (action == actions[i]) {
0310             index = i;
0311             break;
0312         }
0313     }
0314     int columnCount = d->treeView->header()->count();
0315     if (index == columnCount) {
0316         if (d->searchColumns.isEmpty()) // all columns was checked
0317             d->searchColumns.append(0);
0318         else
0319             d->searchColumns.clear();
0320     } else {
0321         if (d->searchColumns.contains(index))
0322             d->searchColumns.removeAll(index);
0323         else {
0324             if (d->searchColumns.isEmpty()) // all columns was checked
0325             {
0326                 for (int i = 0; i < columnCount; ++i)
0327                     if (i != index)
0328                         d->searchColumns.append(i);
0329             } else
0330                 d->searchColumns.append(index);
0331         }
0332     }
0333     updateSearch();
0334 }
0335 
0336 void KViewSearchLine::slotRowsRemoved(const QModelIndex &parent, int, int)
0337 {
0338     if (!d->keepParentsVisible)
0339         return;
0340 
0341     QModelIndex p = parent;
0342     while (p.isValid()) {
0343         int count = model()->rowCount(p);
0344         if (count && anyVisible(model()->index(0, 0, p), model()->index(count - 1, 0, p)))
0345             return;
0346         if (itemMatches(p, d->search))
0347             return;
0348         setVisible(p, false);
0349         p = p.parent();
0350     }
0351 }
0352 
0353 void KViewSearchLine::slotColumnsInserted(const QModelIndex &, int, int)
0354 {
0355     updateSearch();
0356 }
0357 
0358 void KViewSearchLine::slotColumnsRemoved(const QModelIndex &, int first, int last)
0359 {
0360     if (d->treeView)
0361         updateSearch();
0362     else {
0363         if (d->listView->modelColumn() >= first && d->listView->modelColumn() <= last) {
0364             if (d->listView->modelColumn() > last)
0365                 qCCritical(KEDITBOOKMARKS_LOG) << "Columns were removed, the modelColumn() doesn't exist anymore. "
0366                                                   "K4listViewSearchLine can't cope with that.";
0367             updateSearch();
0368         }
0369     }
0370 }
0371 
0372 void KViewSearchLine::slotModelReset()
0373 {
0374     // FIXME Is there a way to ensure that the view
0375     // has already responded to the reset signal?
0376     updateSearch();
0377 }
0378 
0379 void KViewSearchLine::slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
0380 {
0381     QModelIndex parent = topLeft.parent();
0382     int column = 0;
0383     if (d->listView)
0384         column = d->listView->modelColumn();
0385     bool match = recheck(model()->index(topLeft.row(), column, parent), model()->index(bottomRight.row(), column, parent));
0386     if (!d->keepParentsVisible)
0387         return;
0388     if (!parent.isValid()) // includes listview
0389         return;
0390     if (match) {
0391         QModelIndex p = parent;
0392         while (p.isValid()) {
0393             setVisible(p, true);
0394             p = p.parent();
0395         }
0396     } else // no match => might need to hide parents (this is ugly)
0397     {
0398         if (isVisible(parent) == false) // parent is already hidden
0399             return;
0400         // parent is visible => implies all parents visible
0401 
0402         // first check if all of the unchanged rows are hidden
0403         match = false;
0404         if (topLeft.row() >= 1)
0405             match = match || anyVisible(model()->index(0, 0, parent), model()->index(topLeft.row() - 1, 0, parent));
0406         int rowCount = model()->rowCount(parent);
0407         if (bottomRight.row() + 1 <= rowCount - 1)
0408             match = match || anyVisible(model()->index(bottomRight.row() + 1, 0, parent), model()->index(rowCount - 1, 0, parent));
0409         if (!match) // all child rows hidden
0410         {
0411             if (itemMatches(parent, d->search))
0412                 return;
0413             // and parent didn't match, hide it
0414             setVisible(parent, false);
0415 
0416             // need to check all the way up to root
0417             QModelIndex p = parent.parent();
0418             while (p.isValid()) {
0419                 // hide p if no children of p isVisible and it doesn't match
0420                 int count = model()->rowCount(p);
0421                 if (anyVisible(model()->index(0, 0, p), model()->index(count - 1, 0, p)))
0422                     return;
0423 
0424                 if (itemMatches(p, d->search))
0425                     return;
0426                 setVisible(p, false);
0427                 p = p.parent();
0428             }
0429         }
0430     }
0431 }
0432 
0433 ////////////////////////////////////////////////////////////////////////////////
0434 // private methods
0435 ////////////////////////////////////////////////////////////////////////////////
0436 QAbstractItemModel *KViewSearchLine::model() const
0437 {
0438     if (d->treeView)
0439         return d->treeView->model();
0440     else
0441         return d->listView->model();
0442 }
0443 
0444 bool KViewSearchLine::anyVisible(const QModelIndex &first, const QModelIndex &last)
0445 {
0446     Q_ASSERT(d->treeView);
0447     QModelIndex index = first;
0448     while (true) {
0449         if (isVisible(index))
0450             return true;
0451         if (index == last)
0452             break;
0453         index = nextRow(index);
0454     }
0455     return false;
0456 }
0457 
0458 bool KViewSearchLine::isVisible(const QModelIndex &index)
0459 {
0460     if (d->treeView)
0461         return !d->treeView->isRowHidden(index.row(), index.parent());
0462     else
0463         return d->listView->isRowHidden(index.row());
0464 }
0465 
0466 QModelIndex KViewSearchLine::nextRow(const QModelIndex &index)
0467 {
0468     return model()->index(index.row() + 1, index.column(), index.parent());
0469 }
0470 
0471 bool KViewSearchLine::recheck(const QModelIndex &first, const QModelIndex &last)
0472 {
0473     bool visible = false;
0474     QModelIndex index = first;
0475     while (true) {
0476         int rowCount = model()->rowCount(index);
0477         if (d->keepParentsVisible && rowCount && anyVisible(model()->index(0, 0, index), model()->index(rowCount - 1, 0, index))) {
0478             visible = true;
0479         } else // no children visible
0480         {
0481             bool match = itemMatches(index, d->search);
0482             setVisible(index, match);
0483             visible = visible || match;
0484         }
0485         if (index == last)
0486             break;
0487         index = nextRow(index);
0488     }
0489     return visible;
0490 }
0491 
0492 void KViewSearchLine::slotRowsInserted(const QModelIndex &parent, int first, int last)
0493 {
0494     bool visible = false;
0495     int column = 0;
0496     if (d->listView)
0497         column = d->listView->modelColumn();
0498 
0499     QModelIndex index = model()->index(first, column, parent);
0500     QModelIndex end = model()->index(last, column, parent);
0501     while (true) {
0502         if (itemMatches(index, d->search)) {
0503             visible = true;
0504             setVisible(index, true);
0505         } else
0506             setVisible(index, false);
0507         if (index == end)
0508             break;
0509         index = nextRow(index);
0510     }
0511 
0512     if (!d->keepParentsVisible)
0513         return;
0514     if (visible) {
0515         QModelIndex p = parent;
0516         while (p.isValid()) {
0517             setVisible(p, true);
0518             p = p.parent();
0519         }
0520     }
0521 }
0522 
0523 void KViewSearchLine::setVisible(const QModelIndex &index, bool v)
0524 {
0525     if (d->treeView)
0526         d->treeView->setRowHidden(index.row(), index.parent(), !v);
0527     else
0528         d->listView->setRowHidden(index.row(), !v);
0529 }
0530 
0531 void KViewSearchLine::checkItemParentsNotVisible()
0532 {
0533     int rowCount = model()->rowCount(QModelIndex());
0534     int column = 0;
0535     if (d->listView)
0536         column = d->listView->modelColumn();
0537     for (int i = 0; i < rowCount; ++i) {
0538         QModelIndex it = model()->index(i, column, QModelIndex());
0539         if (itemMatches(it, d->search))
0540             setVisible(it, true);
0541         else
0542             setVisible(it, false);
0543     }
0544 }
0545 
0546 /** Check whether \p index, its siblings and their descendants should be shown. Show or hide the items as necessary.
0547  *
0548  *  \p index  The list view item to start showing / hiding items at. Typically, this is the first child of another item, or the
0549  *              the first child of the list view.
0550  *  \return \c true if an item which should be visible is found, \c false if all items found should be hidden.
0551  */
0552 bool KViewSearchLine::checkItemParentsVisible(QModelIndex index)
0553 {
0554     bool visible = false;
0555     int rowCount = model()->rowCount(index.parent());
0556     int column = 0;
0557     if (d->listView)
0558         column = d->listView->modelColumn();
0559     for (int i = 0; i < rowCount; ++i) {
0560         index = model()->index(i, column, index.parent());
0561         if ((model()->rowCount(index) && checkItemParentsVisible(model()->index(0, column, index))) || itemMatches(index, d->search)) {
0562             visible = true;
0563             setVisible(index, true);
0564         } else
0565             setVisible(index, false);
0566     }
0567     return visible;
0568 }
0569 
0570 ////////////////////////////////////////////////////////////////////////////////
0571 // KViewSearchLineWidget
0572 ////////////////////////////////////////////////////////////////////////////////
0573 
0574 class KViewSearchLineWidget::KViewSearchLineWidgetPrivate
0575 {
0576 public:
0577     KViewSearchLineWidgetPrivate()
0578         : view(nullptr)
0579         , searchLine(nullptr)
0580         , layout(nullptr)
0581     {
0582     }
0583     QAbstractItemView *view;
0584     KViewSearchLine *searchLine;
0585     QHBoxLayout *layout;
0586 };
0587 
0588 KViewSearchLineWidget::KViewSearchLineWidget(QAbstractItemView *view, QWidget *parent)
0589     : QWidget(parent)
0590 {
0591     d = new KViewSearchLineWidgetPrivate;
0592     d->view = view;
0593 
0594     QTimer::singleShot(0, this, &KViewSearchLineWidget::createWidgets);
0595 }
0596 
0597 KViewSearchLineWidget::~KViewSearchLineWidget()
0598 {
0599     delete d->layout;
0600     delete d;
0601 }
0602 
0603 KViewSearchLine *KViewSearchLineWidget::createSearchLine(QAbstractItemView *view)
0604 {
0605     if (!d->searchLine)
0606         d->searchLine = new KViewSearchLine(nullptr, view);
0607     return d->searchLine;
0608 }
0609 
0610 void KViewSearchLineWidget::createWidgets()
0611 {
0612     d->layout = new QHBoxLayout(this);
0613     d->layout->setContentsMargins(0, 0, 0, 0);
0614 
0615     QLabel *label = new QLabel(i18n("S&earch:"));
0616     label->setObjectName(QStringLiteral("kde toolbar widget"));
0617     d->layout->addWidget(label);
0618 
0619     d->searchLine = createSearchLine(d->view);
0620     d->layout->addWidget(d->searchLine);
0621     d->searchLine->show();
0622 
0623     label->setBuddy(d->searchLine);
0624     label->show();
0625 }
0626 
0627 KViewSearchLine *KViewSearchLineWidget::searchLine() const
0628 {
0629     return d->searchLine;
0630 }
0631 
0632 #include "moc_kebsearchline.cpp"