Core: Add infrastructure to do additional filtering on search results

... and make use of it to let users filter C++ "find references" results
by access type.

Fixes: QTCREATORBUG-19373
Change-Id: Ib5cadde1cfd235026d8e69da51daa6374808d3f3
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Christian Kandeler
2020-12-11 16:14:43 +01:00
parent f6d4170c05
commit f3d7717b31
9 changed files with 358 additions and 55 deletions

View File

@@ -27,13 +27,68 @@
#include "searchresulttreeitems.h"
#include "searchresulttreeitemroles.h"
#include <utils/algorithm.h>
#include <QApplication>
#include <QFont>
#include <QFontMetrics>
#include <QDebug>
using namespace Core;
using namespace Core::Internal;
namespace Core {
namespace Internal {
class SearchResultTreeModel : public QAbstractItemModel
{
Q_OBJECT
public:
SearchResultTreeModel(QObject *parent = nullptr);
~SearchResultTreeModel() override;
void setShowReplaceUI(bool show);
void setTextEditorFont(const QFont &font, const SearchResultColors &colors);
Qt::ItemFlags flags(const QModelIndex &index) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QModelIndex next(const QModelIndex &idx, bool includeGenerated = false, bool *wrapped = nullptr) const;
QModelIndex prev(const QModelIndex &idx, bool includeGenerated = false, bool *wrapped = nullptr) const;
QList<QModelIndex> addResults(const QList<SearchResultItem> &items, SearchResult::AddMode mode);
static SearchResultTreeItem *treeItemAtIndex(const QModelIndex &idx);
signals:
void jumpToSearchResult(const QString &fileName, int lineNumber,
int searchTermStart, int searchTermLength);
public slots:
void clear();
private:
QModelIndex index(SearchResultTreeItem *item) const;
void addResultsToCurrentParent(const QList<SearchResultItem> &items, SearchResult::AddMode mode);
QSet<SearchResultTreeItem *> addPath(const QStringList &path);
QVariant data(const SearchResultTreeItem *row, int role) const;
bool setCheckState(const QModelIndex &idx, Qt::CheckState checkState, bool firstCall = true);
QModelIndex nextIndex(const QModelIndex &idx, bool *wrapped = nullptr) const;
QModelIndex prevIndex(const QModelIndex &idx, bool *wrapped = nullptr) const;
SearchResultTreeItem *m_rootItem;
SearchResultTreeItem *m_currentParent;
SearchResultColors m_colors;
QModelIndex m_currentIndex;
QStringList m_currentPath; // the path that belongs to the current parent
QFont m_textEditorFont;
bool m_showReplaceUI;
bool m_editorFontIsUsed;
};
SearchResultTreeModel::SearchResultTreeModel(QObject *parent)
: QAbstractItemModel(parent)
@@ -425,8 +480,6 @@ void SearchResultTreeModel::clear()
QModelIndex SearchResultTreeModel::nextIndex(const QModelIndex &idx, bool *wrapped) const
{
if (wrapped)
*wrapped = false;
// pathological
if (!idx.isValid())
return index(0, 0);
@@ -468,8 +521,6 @@ QModelIndex SearchResultTreeModel::next(const QModelIndex &idx, bool includeGene
QModelIndex SearchResultTreeModel::prevIndex(const QModelIndex &idx, bool *wrapped) const
{
if (wrapped)
*wrapped = false;
QModelIndex current = idx;
bool checkForChildren = true;
if (current.isValid()) {
@@ -502,3 +553,106 @@ QModelIndex SearchResultTreeModel::prev(const QModelIndex &idx, bool includeGene
} while (value != idx && !includeGenerated && treeItemAtIndex(value)->isGenerated());
return value;
}
SearchResultFilterModel::SearchResultFilterModel(QObject *parent) : QSortFilterProxyModel(parent)
{
setSourceModel(new SearchResultTreeModel(this));
}
void SearchResultFilterModel::setFilter(SearchResultFilter *filter)
{
if (m_filter)
m_filter->disconnect(this);
m_filter = filter;
if (m_filter) {
connect(m_filter, &SearchResultFilter::filterChanged,
this, [this] {
invalidateFilter();
emit filterInvalidated();
});
}
invalidateFilter();
}
void SearchResultFilterModel::setShowReplaceUI(bool show)
{
sourceModel()->setShowReplaceUI(show);
}
void SearchResultFilterModel::setTextEditorFont(const QFont &font, const SearchResultColors &colors)
{
sourceModel()->setTextEditorFont(font, colors);
}
QList<QModelIndex> SearchResultFilterModel::addResults(const QList<SearchResultItem> &items,
SearchResult::AddMode mode)
{
QList<QModelIndex> sourceIndexes = sourceModel()->addResults(items, mode);
sourceIndexes = Utils::filtered(sourceIndexes, [this](const QModelIndex &idx) {
return filterAcceptsRow(idx.row(), idx.parent());
});
return Utils::transform(sourceIndexes,
[this](const QModelIndex &idx) { return mapFromSource(idx); });
}
void SearchResultFilterModel::clear()
{
sourceModel()->clear();
}
QModelIndex SearchResultFilterModel::nextOrPrev(const QModelIndex &idx, bool *wrapped,
const std::function<QModelIndex (const QModelIndex &)> &func) const
{
if (wrapped)
*wrapped = false;
const QModelIndex sourceIndex = mapToSource(idx);
QModelIndex nextOrPrevSourceIndex = func(sourceIndex);
while (nextOrPrevSourceIndex != sourceIndex
&& !filterAcceptsRow(nextOrPrevSourceIndex.row(), nextOrPrevSourceIndex.parent())) {
nextOrPrevSourceIndex = func(nextOrPrevSourceIndex);
}
return mapFromSource(nextOrPrevSourceIndex);
}
QModelIndex SearchResultFilterModel::next(const QModelIndex &idx, bool includeGenerated,
bool *wrapped) const
{
return nextOrPrev(idx, wrapped, [this, includeGenerated, wrapped](const QModelIndex &index) {
return sourceModel()->next(index, includeGenerated, wrapped); });
}
QModelIndex SearchResultFilterModel::prev(const QModelIndex &idx, bool includeGenerated,
bool *wrapped) const
{
return nextOrPrev(idx, wrapped, [this, includeGenerated, wrapped](const QModelIndex &index) {
return sourceModel()->prev(index, includeGenerated, wrapped); });
}
bool SearchResultFilterModel::filterAcceptsRow(int source_row,
const QModelIndex &source_parent) const
{
const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
const SearchResultTreeItem * const item = SearchResultTreeModel::treeItemAtIndex(idx);
if (!item)
return false;
if (!m_filter)
return true;
if (item->item.userData.isValid())
return m_filter->matches(item->item);
const int childCount = sourceModel()->rowCount(idx);
for (int i = 0; i < childCount; ++i) {
if (filterAcceptsRow(i, idx))
return true;
}
return false;
}
SearchResultTreeModel *SearchResultFilterModel::sourceModel() const
{
return static_cast<SearchResultTreeModel *>(QSortFilterProxyModel::sourceModel());
}
} // namespace Internal
} // namespace Core
#include <searchresulttreemodel.moc>

View File

@@ -28,64 +28,43 @@
#include "searchresultwindow.h"
#include "searchresultcolor.h"
#include <QAbstractItemModel>
#include <QFont>
#include <QSortFilterProxyModel>
#include <functional>
namespace Core {
namespace Internal {
class SearchResultTreeItem;
class SearchResultTreeModel;
class SearchResultTreeModel : public QAbstractItemModel
class SearchResultFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
SearchResultTreeModel(QObject *parent = nullptr);
~SearchResultTreeModel() override;
SearchResultFilterModel(QObject *parent = nullptr);
void setFilter(SearchResultFilter *filter);
void setShowReplaceUI(bool show);
void setTextEditorFont(const QFont &font, const SearchResultColors &colors);
Qt::ItemFlags flags(const QModelIndex &index) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QModelIndex next(const QModelIndex &idx, bool includeGenerated = false, bool *wrapped = nullptr) const;
QModelIndex prev(const QModelIndex &idx, bool includeGenerated = false, bool *wrapped = nullptr) const;
QList<QModelIndex> addResults(const QList<SearchResultItem> &items, SearchResult::AddMode mode);
void clear();
QModelIndex next(const QModelIndex &idx, bool includeGenerated = false,
bool *wrapped = nullptr) const;
QModelIndex prev(const QModelIndex &idx, bool includeGenerated = false,
bool *wrapped = nullptr) const;
signals:
void jumpToSearchResult(const QString &fileName, int lineNumber,
int searchTermStart, int searchTermLength);
public slots:
void clear();
void filterInvalidated();
private:
QModelIndex index(SearchResultTreeItem *item) const;
void addResultsToCurrentParent(const QList<SearchResultItem> &items, SearchResult::AddMode mode);
QSet<SearchResultTreeItem *> addPath(const QStringList &path);
QVariant data(const SearchResultTreeItem *row, int role) const;
bool setCheckState(const QModelIndex &idx, Qt::CheckState checkState, bool firstCall = true);
QModelIndex nextIndex(const QModelIndex &idx, bool *wrapped = nullptr) const;
QModelIndex prevIndex(const QModelIndex &idx, bool *wrapped = nullptr) const;
static SearchResultTreeItem *treeItemAtIndex(const QModelIndex &idx);
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
SearchResultTreeItem *m_rootItem;
SearchResultTreeItem *m_currentParent;
SearchResultColors m_colors;
QModelIndex m_currentIndex;
QStringList m_currentPath; // the path that belongs to the current parent
QFont m_textEditorFont;
bool m_showReplaceUI;
bool m_editorFontIsUsed;
QModelIndex nextOrPrev(const QModelIndex &idx, bool *wrapped,
const std::function<QModelIndex(const QModelIndex &)> &func) const;
SearchResultTreeModel *sourceModel() const;
SearchResultFilter *m_filter = nullptr;
};
} // namespace Internal

View File

@@ -28,18 +28,39 @@
#include "searchresulttreemodel.h"
#include "searchresulttreeitemdelegate.h"
#include <utils/qtcassert.h>
#include <QHeaderView>
#include <QKeyEvent>
#include <QVBoxLayout>
namespace Core {
namespace Internal {
class FilterWidget : public QWidget
{
public:
FilterWidget(QWidget *parent, QWidget *content) : QWidget(parent, Qt::Popup)
{
setAttribute(Qt::WA_DeleteOnClose);
const auto layout = new QVBoxLayout(this);
layout->setContentsMargins(2, 2, 2, 2);
layout->setSpacing(2);
layout->addWidget(content);
setLayout(layout);
move(parent->mapToGlobal(QPoint(0, -sizeHint().height())));
}
};
SearchResultTreeView::SearchResultTreeView(QWidget *parent)
: Utils::TreeView(parent)
, m_model(new SearchResultTreeModel(this))
, m_model(new SearchResultFilterModel(this))
, m_autoExpandResults(false)
{
setModel(m_model);
connect(m_model, &SearchResultFilterModel::filterInvalidated,
this, &SearchResultTreeView::filterInvalidated);
setItemDelegate(new SearchResultTreeItemDelegate(8, this));
setIndentation(14);
setUniformRowHeights(true);
@@ -80,6 +101,27 @@ void SearchResultTreeView::addResults(const QList<SearchResultItem> &items, Sear
}
}
void SearchResultTreeView::setFilter(SearchResultFilter *filter)
{
m_filter = filter;
if (m_filter)
m_filter->setParent(this);
m_model->setFilter(filter);
emit filterChanged();
}
bool SearchResultTreeView::hasFilter() const
{
return m_filter;
}
void SearchResultTreeView::showFilterWidget(QWidget *parent)
{
QTC_ASSERT(hasFilter(), return);
const auto optionsWidget = new FilterWidget(parent, m_filter->createWidget());
optionsWidget->show();
}
void SearchResultTreeView::keyPressEvent(QKeyEvent *event)
{
if ((event->key() == Qt::Key_Return
@@ -118,7 +160,7 @@ void SearchResultTreeView::setTabWidth(int tabWidth)
doItemsLayout();
}
SearchResultTreeModel *SearchResultTreeView::model() const
SearchResultFilterModel *SearchResultTreeView::model() const
{
return m_model;
}

View File

@@ -34,7 +34,7 @@ class SearchResultColor;
namespace Internal {
class SearchResultTreeModel;
class SearchResultFilterModel;
class SearchResultTreeView : public Utils::TreeView
{
@@ -47,21 +47,27 @@ public:
void setTextEditorFont(const QFont &font, const SearchResultColors &colors);
void setTabWidth(int tabWidth);
SearchResultTreeModel *model() const;
SearchResultFilterModel *model() const;
void addResults(const QList<SearchResultItem> &items, SearchResult::AddMode mode);
void setFilter(SearchResultFilter *filter);
bool hasFilter() const;
void showFilterWidget(QWidget *parent);
void keyPressEvent(QKeyEvent *event) override;
bool event(QEvent *e) override;
signals:
void jumpToSearchResult(const SearchResultItem &item);
void filterInvalidated();
void filterChanged();
public slots:
void clear();
void emitJumpToSearchResult(const QModelIndex &index);
protected:
SearchResultTreeModel *m_model;
SearchResultFilterModel *m_model;
SearchResultFilter *m_filter = nullptr;
bool m_autoExpandResults;
};

View File

@@ -134,6 +134,10 @@ SearchResultWidget::SearchResultWidget(QWidget *parent) :
m_searchResultTreeView = new SearchResultTreeView(this);
m_searchResultTreeView->setFrameStyle(QFrame::NoFrame);
m_searchResultTreeView->setAttribute(Qt::WA_MacShowFocusRect, false);
connect(m_searchResultTreeView, &SearchResultTreeView::filterInvalidated,
this, &SearchResultWidget::filterInvalidated);
connect(m_searchResultTreeView, &SearchResultTreeView::filterChanged,
this, &SearchResultWidget::filterChanged);
auto agg = new Aggregation::Aggregate;
agg->add(m_searchResultTreeView);
agg->add(new ItemViewFind(m_searchResultTreeView,
@@ -443,6 +447,21 @@ void SearchResultWidget::setSearchAgainEnabled(bool enabled)
m_searchAgainButton->setEnabled(enabled);
}
void SearchResultWidget::setFilter(SearchResultFilter *filter)
{
m_searchResultTreeView->setFilter(filter);
}
bool SearchResultWidget::hasFilter() const
{
return m_searchResultTreeView->hasFilter();
}
void SearchResultWidget::showFilterWidget(QWidget *parent)
{
m_searchResultTreeView->showFilterWidget(parent);
}
void SearchResultWidget::setReplaceEnabled(bool enabled)
{
m_replaceButton->setEnabled(enabled);
@@ -513,7 +532,7 @@ void SearchResultWidget::searchAgain()
QList<SearchResultItem> SearchResultWidget::checkedItems() const
{
QList<SearchResultItem> result;
SearchResultTreeModel *model = m_searchResultTreeView->model();
SearchResultFilterModel *model = m_searchResultTreeView->model();
const int fileCount = model->rowCount();
for (int i = 0; i < fileCount; ++i) {
QModelIndex fileIndex = model->index(i, 0);

View File

@@ -90,7 +90,9 @@ public:
void setSearchAgainSupported(bool supported);
void setSearchAgainEnabled(bool enabled);
void setFilter(SearchResultFilter *filter);
bool hasFilter() const;
void showFilterWidget(QWidget *parent);
void setReplaceEnabled(bool enabled);
public slots:
@@ -107,6 +109,8 @@ signals:
void restarted();
void visibilityChanged(bool visible);
void requestPopup(bool focus);
void filterInvalidated();
void filterChanged();
void navigateStateChanged();

View File

@@ -110,10 +110,12 @@ namespace Internal {
void moveWidgetToTop();
void popupRequested(bool focus);
void handleExpandCollapseToolButton(bool checked);
void updateFilterButton();
SearchResultWindow *q;
QList<Internal::SearchResultWidget *> m_searchResultWidgets;
QToolButton *m_expandCollapseButton;
QToolButton *m_filterButton;
QToolButton *m_newSearchButton;
QAction *m_expandCollapseAction;
static const bool m_initiallyExpand;
@@ -168,6 +170,11 @@ namespace Internal {
cmd->setAttribute(Command::CA_UpdateText);
m_expandCollapseButton->setDefaultAction(cmd->action());
m_filterButton = new QToolButton(m_widget);
m_filterButton->setText(tr("Filter Results"));
m_filterButton->setIcon(Utils::Icons::FILTER.icon());
m_filterButton->setEnabled(false);
QAction *newSearchAction = new QAction(tr("New Search"), this);
newSearchAction->setIcon(Utils::Icons::NEWSEARCH_TOOLBAR.icon());
cmd = ActionManager::command(Constants::ADVANCED_FIND);
@@ -178,6 +185,13 @@ namespace Internal {
connect(m_expandCollapseAction, &QAction::toggled,
this, &SearchResultWindowPrivate::handleExpandCollapseToolButton);
connect(m_filterButton, &QToolButton::clicked, this, [this] {
if (!isSearchVisible())
return;
m_searchResultWidgets.at(visibleSearchIndex())->showFilterWidget(m_filterButton);
});
connect(m_widget, &QStackedWidget::currentChanged,
this, &SearchResultWindowPrivate::updateFilterButton);
}
void SearchResultWindowPrivate::setCurrentIndex(int index, bool focus)
@@ -445,7 +459,7 @@ QWidget *SearchResultWindow::outputWidget(QWidget *)
*/
QList<QWidget*> SearchResultWindow::toolBarWidgets() const
{
return {d->m_expandCollapseButton, d->m_newSearchButton, d->m_spacer,
return {d->m_expandCollapseButton, d->m_filterButton, d->m_newSearchButton, d->m_spacer,
d->m_historyLabel, d->m_spacer2, d->m_recentSearchesBox};
}
@@ -496,6 +510,12 @@ SearchResult *SearchResultWindow::startNewSearch(const QString &label,
}
}
auto widget = new SearchResultWidget;
connect(widget, &SearchResultWidget::filterInvalidated, this, [this, widget] {
if (widget == d->m_searchResultWidgets.at(d->visibleSearchIndex()))
d->handleExpandCollapseToolButton(d->m_expandCollapseButton->isChecked());
});
connect(widget, &SearchResultWidget::filterChanged,
d, &SearchResultWindowPrivate::updateFilterButton);
d->m_searchResultWidgets.prepend(widget);
d->m_widget->insertWidget(1, widget);
connect(widget, &SearchResultWidget::navigateStateChanged,
@@ -615,6 +635,12 @@ void SearchResultWindowPrivate::handleExpandCollapseToolButton(bool checked)
}
}
void SearchResultWindowPrivate::updateFilterButton()
{
m_filterButton->setEnabled(isSearchVisible()
&& m_searchResultWidgets.at(visibleSearchIndex())->hasFilter());
}
/*!
\internal
*/
@@ -833,6 +859,11 @@ void SearchResult::addResults(const QList<SearchResultItem> &items, AddMode mode
emit countChanged(m_widget->count());
}
void SearchResult::setFilter(SearchResultFilter *filter)
{
m_widget->setFilter(filter);
}
/*!
Notifies the \uicontrol {Search Results} output pane that the current search
has been \a canceled, and the UI should reflect that.

View File

@@ -45,6 +45,18 @@ namespace Internal {
}
class SearchResultWindow;
class CORE_EXPORT SearchResultFilter : public QObject
{
Q_OBJECT
public:
virtual QWidget *createWidget() = 0;
virtual bool matches(const SearchResultItem &item) const = 0;
signals:
void filterChanged();
};
class CORE_EXPORT SearchResult : public QObject
{
Q_OBJECT
@@ -77,6 +89,7 @@ public slots:
const QVariant &userData = QVariant(),
SearchResultColor::Style style = SearchResultColor::Style::Default);
void addResults(const QList<SearchResultItem> &items, AddMode mode);
void setFilter(SearchResultFilter *filter); // Takes ownership
void finishSearch(bool canceled);
void setTextToReplace(const QString &textToReplace);
void restart();

View File

@@ -49,6 +49,7 @@
#include <QCheckBox>
#include <QDir>
#include <QFutureWatcher>
#include <QVBoxLayout>
#include <functional>
@@ -169,6 +170,58 @@ static QList<QByteArray> fullIdForSymbol(CPlusPlus::Symbol *symbol)
namespace {
class Filter : public Core::SearchResultFilter
{
QWidget *createWidget() override
{
const auto widget = new QWidget;
const auto layout = new QVBoxLayout(widget);
layout->setContentsMargins(0, 0, 0, 0);
const auto readsCheckBox = new QCheckBox(tr("Reads"));
readsCheckBox->setChecked(m_showReads);
const auto writesCheckBox = new QCheckBox(tr("Writes"));
writesCheckBox->setChecked(m_showWrites);
const auto otherCheckBox = new QCheckBox(tr("Other"));
otherCheckBox->setChecked(m_showOther);
layout->addWidget(readsCheckBox);
layout->addWidget(writesCheckBox);
layout->addWidget(otherCheckBox);
connect(readsCheckBox, &QCheckBox::toggled,
this, [this](bool checked) { setValue(m_showReads, checked); });
connect(writesCheckBox, &QCheckBox::toggled,
this, [this](bool checked) { setValue(m_showWrites, checked); });
connect(otherCheckBox, &QCheckBox::toggled,
this, [this](bool checked) { setValue(m_showOther, checked); });
return widget;
}
bool matches(const SearchResultItem &item) const override
{
switch (static_cast<CPlusPlus::Usage::Type>(item.userData.toInt())) {
case CPlusPlus::Usage::Type::Read:
return m_showReads;
case CPlusPlus::Usage::Type::Write:
case CPlusPlus::Usage::Type::WritableRef:
case CPlusPlus::Usage::Type::Initialization:
return m_showWrites;
case CPlusPlus::Usage::Type::Declaration:
case CPlusPlus::Usage::Type::Other:
return m_showOther;
}
return false;
}
void setValue(bool &member, bool value)
{
member = value;
emit filterChanged();
}
bool m_showReads = true;
bool m_showWrites = true;
bool m_showOther = true;
};
class ProcessFile
{
const WorkingCopy workingCopy;
@@ -339,6 +392,7 @@ void CppFindReferences::findUsages(CPlusPlus::Symbol *symbol,
SearchResultWindow::PreserveCaseDisabled,
QLatin1String("CppEditor"));
search->setTextToReplace(replacement);
search->setFilter(new Filter);
auto renameFilesCheckBox = new QCheckBox();
renameFilesCheckBox->setVisible(false);
search->setAdditionalReplaceWidget(renameFilesCheckBox);
@@ -570,7 +624,8 @@ static void displayResults(SearchResult *search, QFutureWatcher<CPlusPlus::Usage
for (int index = first; index != last; ++index) {
const CPlusPlus::Usage result = watcher->future().resultAt(index);
search->addResult(result.path.toString(), result.line, result.lineText,
result.col, result.len, {}, colorStyleForUsageType(result.type));
result.col, result.len, int(result.type),
colorStyleForUsageType(result.type));
if (parameters.prettySymbolName.isEmpty())
continue;