diff options
author | Nikolai Kosjar <nikolai.kosjar@qt.io> | 2019-09-27 12:04:55 +0200 |
---|---|---|
committer | Nikolai Kosjar <nikolai.kosjar@qt.io> | 2019-10-01 14:42:01 +0000 |
commit | a0a39a047018ca38dedfbd5045cfb4c5d4aea4d8 (patch) | |
tree | c096e6478a90dea1bc73228aa1baebfeadf90c83 | |
parent | 9fd4b256386ee88ce94b23767bf52a83980a66bd (diff) |
ClangTools: Make file selection dialog searchable/filterable
Allow filtering for open and edited documents.
Add also search functionality (triggerable by e.g. Ctrl+F).
Change-Id: Ib41400abfd3b81371afddd56b88ff1b9bf8b9bcd
Reviewed-by: Cristian Adam <cristian.adam@qt.io>
Reviewed-by: Nikolai Kosjar <nikolai.kosjar@qt.io>
-rw-r--r-- | src/plugins/clangtools/clangfileinfo.h | 23 | ||||
-rw-r--r-- | src/plugins/clangtools/clangselectablefilesdialog.cpp | 227 | ||||
-rw-r--r-- | src/plugins/clangtools/clangselectablefilesdialog.h | 18 | ||||
-rw-r--r-- | src/plugins/clangtools/clangselectablefilesdialog.ui | 54 | ||||
-rw-r--r-- | src/plugins/clangtools/clangtool.cpp | 80 | ||||
-rw-r--r-- | src/plugins/clangtools/clangtool.h | 5 | ||||
-rw-r--r-- | src/plugins/clangtools/clangtoolsutils.h | 1 |
7 files changed, 271 insertions, 137 deletions
diff --git a/src/plugins/clangtools/clangfileinfo.h b/src/plugins/clangtools/clangfileinfo.h index eaeaecce12b..093fc28dee1 100644 --- a/src/plugins/clangtools/clangfileinfo.h +++ b/src/plugins/clangtools/clangfileinfo.h @@ -48,12 +48,33 @@ public: CppTools::ProjectFile::Kind kind; CppTools::ProjectPart::Ptr projectPart; }; +using FileInfos = std::vector<FileInfo>; inline bool operator==(const FileInfo &lhs, const FileInfo &rhs) { return lhs.file == rhs.file; } -using FileInfos = std::vector<FileInfo>; +class FileInfoSelection { +public: + QSet<Utils::FilePath> dirs; + QSet<Utils::FilePath> files; +}; + +class FileInfoProvider { +public: + QString displayName; + FileInfos fileInfos; + FileInfoSelection selection; + + enum ExpandPolicy { + All, + Limited, + } expandPolicy = All; + + using OnSelectionAccepted = std::function<void(const FileInfoSelection &selection)>; + OnSelectionAccepted onSelectionAccepted; +}; +using FileInfoProviders = std::vector<FileInfoProvider>; } // namespace Internal } // namespace ClangTools diff --git a/src/plugins/clangtools/clangselectablefilesdialog.cpp b/src/plugins/clangtools/clangselectablefilesdialog.cpp index 418615f7dab..f879e5099d2 100644 --- a/src/plugins/clangtools/clangselectablefilesdialog.cpp +++ b/src/plugins/clangtools/clangselectablefilesdialog.cpp @@ -27,22 +27,19 @@ #include "ui_clangselectablefilesdialog.h" -#include "clangtoolsprojectsettings.h" -#include "clangtoolssettings.h" -#include "clangtoolsutils.h" - -#include <cpptools/compileroptionsbuilder.h> -#include <cpptools/cppmodelmanager.h> -#include <cpptools/cpptoolsreuse.h> +#include <coreplugin/actionmanager/actionmanager.h> +#include <coreplugin/find/itemviewfind.h> #include <cpptools/projectinfo.h> -#include <cpptools/projectpart.h> - #include <projectexplorer/selectablefilesmodel.h> +#include <texteditor/textdocument.h> #include <utils/algorithm.h> #include <utils/qtcassert.h> +#include <QDialogButtonBox> #include <QPushButton> +#include <QSortFilterProxyModel> +#include <QStandardItem> using namespace CppTools; using namespace Utils; @@ -95,48 +92,84 @@ class SelectableFilesModel : public ProjectExplorer::SelectableFilesModel Q_OBJECT public: - SelectableFilesModel(const CppTools::ProjectInfo &projectInfo, const FileInfos &allFileInfos) + SelectableFilesModel() : ProjectExplorer::SelectableFilesModel(nullptr) + {} + + void buildTree(ProjectExplorer::Project *project, const FileInfos &fileInfos) { - buildTree(projectInfo.project(), allFileInfos); + beginResetModel(); + m_root->fullPath = project->projectFilePath(); + m_root->name = project->projectFilePath().fileName(); + m_root->isDir = true; + + FileInfos outOfBaseDirFiles; + Tree *projectDirTree = buildProjectDirTree(project->projectDirectory(), + fileInfos, + outOfBaseDirFiles); + if (outOfBaseDirFiles.empty()) { + // Showing the project file and beneath the project dir is pointless in this case, + // so get rid of the root node and modify the project dir node as the new root node. + projectDirTree->name = m_root->name; + projectDirTree->fullPath = m_root->fullPath; + projectDirTree->parent = m_root->parent; + + delete m_root; // OK, it has no files / child dirs. + + m_root = projectDirTree; + } else { + // Set up project dir node as sub node of the project file node + linkDirNode(m_root, projectDirTree); + + // Add files outside of the base directory to a separate node + Tree *externalFilesNode = createDirNode(SelectableFilesDialog::tr( + "Files outside of the base directory"), + FilePath::fromString("/")); + linkDirNode(m_root, externalFilesNode); + for (const FileInfo &fileInfo : outOfBaseDirFiles) + linkFileNode(externalFilesNode, createFileNode(fileInfo, true)); + } + endResetModel(); } // Returns the minimal selection that can restore all selected files. // // For example, if a directory node if fully checked, there is no need to // save all the children of that node. - void minimalSelection(QSet<FilePath> &checkedDirs, QSet<FilePath> &checkedFiles) const + void minimalSelection(FileInfoSelection &selection) const { + selection.dirs.clear(); + selection.files.clear(); traverse(index(0, 0, QModelIndex()), [&](const QModelIndex &index){ auto node = static_cast<Tree *>(index.internalPointer()); if (node->checked == Qt::Checked) { if (node->isDir) { - checkedDirs += node->fullPath; + selection.dirs += node->fullPath; return false; // Do not descend further. } - checkedFiles += node->fullPath; + selection.files += node->fullPath; } return true; }); } - void restoreMinimalSelection(const QSet<FilePath> &dirs, const QSet<FilePath> &files) + void restoreMinimalSelection(const FileInfoSelection &selection) { - if (dirs.isEmpty() && files.isEmpty()) + if (selection.dirs.isEmpty() && selection.files.isEmpty()) return; traverse(index(0, 0, QModelIndex()), [&](const QModelIndex &index){ auto node = static_cast<Tree *>(index.internalPointer()); - if (node->isDir && dirs.contains(node->fullPath)) { + if (node->isDir && selection.dirs.contains(node->fullPath)) { setData(index, Qt::Checked, Qt::CheckStateRole); return false; // Do not descend further. } - if (!node->isDir && files.contains(node->fullPath)) + if (!node->isDir && selection.files.contains(node->fullPath)) setData(index, Qt::Checked, Qt::CheckStateRole); return true; @@ -183,40 +216,6 @@ private: } } - void buildTree(ProjectExplorer::Project *project, const FileInfos &fileInfos) - { - m_root->fullPath = project->projectFilePath(); - m_root->name = project->projectFilePath().fileName(); - m_root->isDir = true; - - FileInfos outOfBaseDirFiles; - Tree *projectDirTree = buildProjectDirTree(project->projectDirectory(), - fileInfos, - outOfBaseDirFiles); - if (outOfBaseDirFiles.empty()) { - // Showing the project file and beneath the project dir is pointless in this case, - // so get rid of the root node and modify the project dir node as the new root node. - projectDirTree->name = m_root->name; - projectDirTree->fullPath = m_root->fullPath; - projectDirTree->parent = m_root->parent; - - delete m_root; // OK, it has no files / child dirs. - - m_root = projectDirTree; - } else { - // Set up project dir node as sub node of the project file node - linkDirNode(m_root, projectDirTree); - - // Add files outside of the base directory to a separate node - Tree *externalFilesNode = createDirNode(SelectableFilesDialog::tr( - "Files outside of the base directory"), - FilePath::fromString("/")); - linkDirNode(m_root, externalFilesNode); - for (const FileInfo &fileInfo : outOfBaseDirFiles) - linkFileNode(externalFilesNode, createFileNode(fileInfo, true)); - } - } - Tree *buildProjectDirTree(const FilePath &projectDir, const FileInfos &fileInfos, FileInfos &outOfBaseDirFiles) const @@ -265,52 +264,128 @@ private: } }; +class FileFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + FileFilterModel(QObject *parent = nullptr) + : QSortFilterProxyModel(parent) + {} + +private: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override + { + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + const int rowCount = sourceModel()->rowCount(index); + if (rowCount == 0) // No children -> file node! + return sourceModel()->data(index).toString().contains(filterRegExp()); + for (int row = 0; row < rowCount; ++row) { + if (filterAcceptsRow(row, index)) + return true; + } + return false; + } +}; + SelectableFilesDialog::SelectableFilesDialog(const ProjectInfo &projectInfo, - const FileInfos &allFileInfos) + const FileInfoProviders &fileInfoProviders, + int initialProviderIndex) : QDialog(nullptr) , m_ui(new Ui::SelectableFilesDialog) - , m_filesModel(new SelectableFilesModel(projectInfo, allFileInfos)) + , m_filesModel(new SelectableFilesModel) + , m_fileInfoProviders(fileInfoProviders) , m_project(projectInfo.project()) , m_analyzeButton(new QPushButton(tr("Analyze"), this)) { m_ui->setupUi(this); - m_ui->filesView->setModel(m_filesModel.get()); - m_ui->filesView->expandToDepth(2); - - m_ui->buttons->setStandardButtons(QDialogButtonBox::Cancel); - m_ui->buttons->addButton(m_analyzeButton, QDialogButtonBox::AcceptRole); - - ClangToolsProjectSettings *settings = ClangToolsProjectSettingsManager::getSettings(m_project); - - // Restore selection - if (settings->selectedDirs().isEmpty() && settings->selectedFiles().isEmpty()) - m_filesModel->selectAllFiles(); // Initially, all files are selected - else // Restore selection - m_filesModel->restoreMinimalSelection(settings->selectedDirs(), settings->selectedFiles()); - + // Files View + // Make find actions available in this dialog, e.g. Strg+F for the view. + addAction(Core::ActionManager::command(Core::Constants::FIND_IN_DOCUMENT)->action()); + addAction(Core::ActionManager::command(Core::Constants::FIND_NEXT)->action()); + addAction(Core::ActionManager::command(Core::Constants::FIND_PREVIOUS)->action()); + m_fileView = new QTreeView; + m_fileView->setHeaderHidden(true); + m_fileView->setModel(m_filesModel.get()); + m_ui->verticalLayout->addWidget( + Core::ItemViewFind::createSearchableWrapper(m_fileView, Core::ItemViewFind::LightColored)); + + // Filter combo box + for (const FileInfoProvider &provider : m_fileInfoProviders) { + m_ui->fileFilterComboBox->addItem(provider.displayName); + + // Disable item if it has no file infos + auto *model = qobject_cast<QStandardItemModel *>(m_ui->fileFilterComboBox->model()); + QStandardItem *item = model->item(m_ui->fileFilterComboBox->count() - 1); + item->setFlags(provider.fileInfos.empty() ? item->flags() & ~Qt::ItemIsEnabled + : item->flags() | Qt::ItemIsEnabled); + } + int providerIndex = initialProviderIndex; + if (m_fileInfoProviders[providerIndex].fileInfos.empty()) + providerIndex = 0; + m_ui->fileFilterComboBox->setCurrentIndex(providerIndex); + onFileFilterChanged(providerIndex); + connect(m_ui->fileFilterComboBox, + QOverload<int>::of(&QComboBox::currentIndexChanged), + this, + &SelectableFilesDialog::onFileFilterChanged); + + // Buttons + m_buttons = new QDialogButtonBox; + m_buttons->setStandardButtons(QDialogButtonBox::Cancel); + m_buttons->addButton(m_analyzeButton, QDialogButtonBox::AcceptRole);\ + connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); m_analyzeButton->setEnabled(m_filesModel->hasCheckedFiles()); connect(m_filesModel.get(), &QAbstractItemModel::dataChanged, [this]() { m_analyzeButton->setEnabled(m_filesModel->hasCheckedFiles()); }); + m_ui->verticalLayout->addWidget(m_buttons); } SelectableFilesDialog::~SelectableFilesDialog() = default; -FileInfos SelectableFilesDialog::filteredFileInfos() const +FileInfos SelectableFilesDialog::fileInfos() const { return m_filesModel->selectedFileInfos(); } -void SelectableFilesDialog::accept() +int SelectableFilesDialog::currentProviderIndex() const { - ClangToolsProjectSettings *settings = ClangToolsProjectSettingsManager::getSettings(m_project); + return m_ui->fileFilterComboBox->currentIndex(); +} - QSet<FilePath> checkedDirs; - QSet<FilePath> checkedFiles; - m_filesModel->minimalSelection(checkedDirs, checkedFiles); - settings->setSelectedDirs(checkedDirs); - settings->setSelectedFiles(checkedFiles); +void SelectableFilesDialog::onFileFilterChanged(int index) +{ + // Remember previous filter/selection + if (m_previousProviderIndex != -1) + m_filesModel->minimalSelection(m_fileInfoProviders[m_previousProviderIndex].selection); + m_previousProviderIndex = index; + + // Reset model + const FileInfoProvider &provider = m_fileInfoProviders[index]; + m_filesModel->buildTree(m_project, provider.fileInfos); + + // Expand + if (provider.expandPolicy == FileInfoProvider::All) + m_fileView->expandAll(); + else + m_fileView->expandToDepth(2); + + // Handle selection + if (provider.selection.dirs.isEmpty() && provider.selection.files.isEmpty()) + m_filesModel->selectAllFiles(); // Initially, all files are selected + else + m_filesModel->restoreMinimalSelection(provider.selection); +} + +void SelectableFilesDialog::accept() +{ + FileInfoSelection selection; + m_filesModel->minimalSelection(selection); + FileInfoProvider &provider = m_fileInfoProviders[m_ui->fileFilterComboBox->currentIndex()]; + provider.onSelectionAccepted(selection); QDialog::accept(); } diff --git a/src/plugins/clangtools/clangselectablefilesdialog.h b/src/plugins/clangtools/clangselectablefilesdialog.h index e639d061992..dbd0125d8bb 100644 --- a/src/plugins/clangtools/clangselectablefilesdialog.h +++ b/src/plugins/clangtools/clangselectablefilesdialog.h @@ -27,13 +27,10 @@ #include "clangfileinfo.h" -#include <coreplugin/id.h> - #include <QDialog> -#include <memory> - QT_BEGIN_NAMESPACE +class QDialogButtonBox; class QPushButton; QT_END_NAMESPACE @@ -51,18 +48,25 @@ class SelectableFilesDialog : public QDialog public: explicit SelectableFilesDialog(const CppTools::ProjectInfo &projectInfo, - const FileInfos &allFileInfos); + const FileInfoProviders &fileInfoProviders, + int initialProviderIndex); ~SelectableFilesDialog() override; - FileInfos filteredFileInfos() const; + FileInfos fileInfos() const; + int currentProviderIndex() const; private: + void onFileFilterChanged(int index); void accept() override; std::unique_ptr<Ui::SelectableFilesDialog> m_ui; + QTreeView *m_fileView = nullptr; + QDialogButtonBox *m_buttons = nullptr; std::unique_ptr<SelectableFilesModel> m_filesModel; - Core::Id m_customDiagnosticConfig; + FileInfoProviders m_fileInfoProviders; + int m_previousProviderIndex = -1; + ProjectExplorer::Project *m_project; QPushButton *m_analyzeButton = nullptr; }; diff --git a/src/plugins/clangtools/clangselectablefilesdialog.ui b/src/plugins/clangtools/clangselectablefilesdialog.ui index 834f3fc1894..f78460e84c2 100644 --- a/src/plugins/clangtools/clangselectablefilesdialog.ui +++ b/src/plugins/clangtools/clangselectablefilesdialog.ui @@ -15,57 +15,17 @@ </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QTreeView" name="filesView"> - <property name="headerHidden"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QDialogButtonBox" name="buttons"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + <widget class="QComboBox" name="fileFilterComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> </widget> </item> </layout> </widget> <resources/> - <connections> - <connection> - <sender>buttons</sender> - <signal>accepted()</signal> - <receiver>ClangTools::Internal::SelectableFilesDialog</receiver> - <slot>accept()</slot> - <hints> - <hint type="sourcelabel"> - <x>248</x> - <y>254</y> - </hint> - <hint type="destinationlabel"> - <x>157</x> - <y>274</y> - </hint> - </hints> - </connection> - <connection> - <sender>buttons</sender> - <signal>rejected()</signal> - <receiver>ClangTools::Internal::SelectableFilesDialog</receiver> - <slot>reject()</slot> - <hints> - <hint type="sourcelabel"> - <x>316</x> - <y>260</y> - </hint> - <hint type="destinationlabel"> - <x>286</x> - <y>274</y> - </hint> - </hints> - </connection> - </connections> + <connections/> </ui> diff --git a/src/plugins/clangtools/clangtool.cpp b/src/plugins/clangtools/clangtool.cpp index 90c66834acb..6b645ad9520 100644 --- a/src/plugins/clangtools/clangtool.cpp +++ b/src/plugins/clangtools/clangtool.cpp @@ -55,6 +55,8 @@ #include <projectexplorer/session.h> #include <projectexplorer/target.h> +#include <texteditor/textdocument.h> + #include <utils/algorithm.h> #include <utils/fancylineedit.h> #include <utils/fancymainwindow.h> @@ -505,7 +507,11 @@ void ClangTool::startTool(ClangTool::FileSelection fileSelection, ProjectExplorerPlugin::startRunControl(runControl); } -Diagnostics ClangTool::read(OutputFileFormat outputFileFormat, const QString &logFilePath, const QString &mainFilePath, const QSet<FilePath> &projectFiles, QString *errorMessage) const +Diagnostics ClangTool::read(OutputFileFormat outputFileFormat, + const QString &logFilePath, + const QString &mainFilePath, + const QSet<FilePath> &projectFiles, + QString *errorMessage) const { const auto acceptFromFilePath = [projectFiles](const Utils::FilePath &filePath) { return projectFiles.contains(filePath); @@ -522,7 +528,7 @@ Diagnostics ClangTool::read(OutputFileFormat outputFileFormat, const QString &lo errorMessage); } -FileInfos ClangTool::collectFileInfos(Project *project, FileSelection fileSelection) const +FileInfos ClangTool::collectFileInfos(Project *project, FileSelection fileSelection) { auto projectInfo = CppTools::CppModelManager::instance()->projectInfo(project); QTC_ASSERT(projectInfo.isValid(), return FileInfos()); @@ -533,10 +539,14 @@ FileInfos ClangTool::collectFileInfos(Project *project, FileSelection fileSelect return allFileInfos; if (fileSelection == FileSelection::AskUser) { - SelectableFilesDialog dialog(projectInfo, allFileInfos); + static int initialProviderIndex = 0; + SelectableFilesDialog dialog(projectInfo, + fileInfoProviders(project, allFileInfos), + initialProviderIndex); if (dialog.exec() == QDialog::Rejected) return FileInfos(); - return dialog.filteredFileInfos(); + initialProviderIndex = dialog.currentProviderIndex(); + return dialog.fileInfos(); } if (fileSelection == FileSelection::CurrentFile) { @@ -605,6 +615,68 @@ void ClangTool::loadDiagnosticsFromFiles() onNewDiagnosticsAvailable(diagnostics); } +using DocumentPredicate = std::function<bool(Core::IDocument *)>; + +static FileInfos fileInfosMatchingDocuments(const FileInfos &fileInfos, + const DocumentPredicate &predicate) +{ + QSet<Utils::FilePath> documentPaths; + for (const Core::DocumentModel::Entry *e : Core::DocumentModel::entries()) { + if (predicate(e->document)) + documentPaths.insert(e->fileName()); + } + + return Utils::filtered(fileInfos, [documentPaths](const FileInfo &fileInfo) { + return documentPaths.contains(fileInfo.file); + }); +} + +static FileInfos fileInfosMatchingOpenedDocuments(const FileInfos &fileInfos) +{ + // Note that (initially) suspended text documents are still IDocuments, not yet TextDocuments. + return fileInfosMatchingDocuments(fileInfos, [](Core::IDocument *) { return true; }); +} + +static FileInfos fileInfosMatchingEditedDocuments(const FileInfos &fileInfos) +{ + return fileInfosMatchingDocuments(fileInfos, [](Core::IDocument *document) { + if (auto textDocument = qobject_cast<TextEditor::TextDocument*>(document)) + return textDocument->document()->revision() > 1; + return false; + }); +} + +FileInfoProviders ClangTool::fileInfoProviders(ProjectExplorer::Project *project, + const FileInfos &allFileInfos) +{ + ClangToolsProjectSettings *s = ClangToolsProjectSettingsManager::getSettings(project); + static FileInfoSelection openedFilesSelection; + static FileInfoSelection editeddFilesSelection; + + return { + {ClangTool::tr("All Files"), + allFileInfos, + FileInfoSelection{s->selectedDirs(), s->selectedFiles()}, + FileInfoProvider::Limited, + [s](const FileInfoSelection &selection) { + s->setSelectedDirs(selection.dirs); + s->setSelectedFiles(selection.files); + }}, + + {ClangTool::tr("Opened Files"), + fileInfosMatchingOpenedDocuments(allFileInfos), + openedFilesSelection, + FileInfoProvider::All, + [](const FileInfoSelection &selection) { openedFilesSelection = selection; }}, + + {ClangTool::tr("Edited Files"), + fileInfosMatchingEditedDocuments(allFileInfos), + editeddFilesSelection, + FileInfoProvider::All, + [](const FileInfoSelection &selection) { editeddFilesSelection = selection; }}, + }; +} + QSet<Diagnostic> ClangTool::diagnostics() const { return Utils::filtered(m_diagnosticModel->diagnostics(), [](const Diagnostic &diagnostic) { diff --git a/src/plugins/clangtools/clangtool.h b/src/plugins/clangtools/clangtool.h index 422c24ff95b..bde31d355dc 100644 --- a/src/plugins/clangtools/clangtool.h +++ b/src/plugins/clangtools/clangtool.h @@ -88,7 +88,7 @@ public: QString *errorMessage) const; FileInfos collectFileInfos(ProjectExplorer::Project *project, - FileSelection fileSelection) const; + FileSelection fileSelection); // For testing. QSet<Diagnostic> diagnostics() const; @@ -112,6 +112,9 @@ private: void initDiagnosticView(); void loadDiagnosticsFromFiles(); + FileInfoProviders fileInfoProviders(ProjectExplorer::Project *project, + const FileInfos &allFileInfos); + ClangToolsDiagnosticModel *m_diagnosticModel = nullptr; QPointer<Debugger::DetailedErrorView> m_diagnosticView; diff --git a/src/plugins/clangtools/clangtoolsutils.h b/src/plugins/clangtools/clangtoolsutils.h index d0b4829725f..825bf74d8c3 100644 --- a/src/plugins/clangtools/clangtoolsutils.h +++ b/src/plugins/clangtools/clangtoolsutils.h @@ -28,7 +28,6 @@ #include <coreplugin/id.h> #include <cpptools/clangdiagnosticconfig.h> -#include <QVersionNumber> #include <QtGlobal> QT_BEGIN_NAMESPACE |