// Copyright (C) 2018 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "clangselectablefilesdialog.h" #include "clangtoolstr.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace CppEditor; using namespace Utils; using namespace ProjectExplorer; namespace ClangTools { namespace Internal { class TreeWithFileInfo : public Tree { public: FileInfo info; }; static void linkDirNode(Tree *parentNode, Tree *childNode) { parentNode->childDirectories.append(childNode); childNode->parent = parentNode; } static void linkFileNode(Tree *parentNode, Tree *childNode) { childNode->parent = parentNode; parentNode->files.append(childNode); parentNode->visibleFiles.append(childNode); } static Tree *createDirNode(const QString &name, const FilePath &filePath = FilePath()) { auto node = new Tree; node->name = name; node->fullPath = filePath; node->isDir = true; return node; } static Tree *createFileNode(const FileInfo &fileInfo, bool displayFullPath = false) { auto node = new TreeWithFileInfo; node->name = displayFullPath ? fileInfo.file.toUserOutput() : fileInfo.file.fileName(); node->fullPath = fileInfo.file; node->info = fileInfo; return node; } class SelectableFilesModel : public ProjectExplorer::SelectableFilesModel { Q_OBJECT public: SelectableFilesModel() : ProjectExplorer::SelectableFilesModel(nullptr) {} void buildTree(ProjectExplorer::Project *project, const FileInfos &fileInfos) { 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(Tr::tr("Files outside of the base directory"), "/"); 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(FileInfoSelection &selection) const { selection.dirs.clear(); selection.files.clear(); traverse(index(0, 0, QModelIndex()), [&](const QModelIndex &index){ auto node = static_cast(index.internalPointer()); if (node->checked == Qt::Checked) { if (node->isDir) { selection.dirs += node->fullPath; return false; // Do not descend further. } selection.files += node->fullPath; } return true; }); } void restoreMinimalSelection(const FileInfoSelection &selection) { if (selection.dirs.isEmpty() && selection.files.isEmpty()) return; traverse(index(0, 0, QModelIndex()), [&](const QModelIndex &index){ auto node = static_cast(index.internalPointer()); if (node->isDir && selection.dirs.contains(node->fullPath)) { setData(index, Qt::Checked, Qt::CheckStateRole); return false; // Do not descend further. } if (!node->isDir && selection.files.contains(node->fullPath)) setData(index, Qt::Checked, Qt::CheckStateRole); return true; }); } FileInfos selectedFileInfos() const { FileInfos result; traverse(index(0, 0, QModelIndex()), [&](const QModelIndex &index){ auto node = static_cast(index.internalPointer()); if (node->checked == Qt::Unchecked) return false; if (!node->isDir) result.push_back(static_cast(node)->info); return true; }); return result; } private: void traverse(const QModelIndex &index, const std::function &visit) const { if (!index.isValid()) return; if (!visit(index)) return; if (!hasChildren(index)) return; const int rows = rowCount(index); const int cols = columnCount(index); for (int i = 0; i < rows; ++i) { for (int j = 0; j < cols; ++j) traverse(this->index(i, j, index), visit); } } Tree *buildProjectDirTree(const FilePath &projectDir, const FileInfos &fileInfos, FileInfos &outOfBaseDirFiles) const { Tree *projectDirNode = createDirNode(projectDir.fileName(), projectDir); QHash dirsToNode; dirsToNode.insert(projectDirNode->fullPath, projectDirNode); for (const FileInfo &fileInfo : fileInfos) { if (!fileInfo.file.isChildOf(projectDirNode->fullPath)) { outOfBaseDirFiles.push_back(fileInfo); continue; // Handle these separately. } // Find or create parent nodes FilePath parentDir = fileInfo.file.parentDir(); Tree *parentNode = dirsToNode[parentDir]; if (!parentNode) { // Find nearest existing node QStringList dirsToCreate; while (!parentNode) { dirsToCreate.prepend(parentDir.fileName()); parentDir = parentDir.parentDir(); parentNode = dirsToNode[parentDir]; } // Create needed extra dir nodes FilePath currentDirPath = parentDir; for (const QString &dirName : dirsToCreate) { currentDirPath = currentDirPath.pathAppended(dirName); Tree *newDirNode = createDirNode(dirName, currentDirPath); linkDirNode(parentNode, newDirNode); dirsToNode.insert(currentDirPath, newDirNode); parentNode = newDirNode; } } // Create and link file node to dir node linkFileNode(parentNode, createFileNode(fileInfo)); } return projectDirNode; } }; SelectableFilesDialog::SelectableFilesDialog(Project *project, const FileInfoProviders &fileInfoProviders, int initialProviderIndex) : QDialog(nullptr) , m_filesModel(new SelectableFilesModel) , m_fileInfoProviders(fileInfoProviders) , m_project(project) { setWindowTitle(Tr::tr("Files to Analyze")); resize(700, 600); m_fileFilterComboBox = new QComboBox(this); m_fileFilterComboBox->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); // 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()); // Filter combo box for (const FileInfoProvider &provider : m_fileInfoProviders) { m_fileFilterComboBox->addItem(provider.displayName); // Disable item if it has no file infos auto *model = qobject_cast(m_fileFilterComboBox->model()); QStandardItem *item = model->item(m_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_fileFilterComboBox->setCurrentIndex(providerIndex); onFileFilterChanged(providerIndex); connect(m_fileFilterComboBox, &QComboBox::currentIndexChanged, this, &SelectableFilesDialog::onFileFilterChanged); auto analyzeButton = new QPushButton(Tr::tr("Analyze"), this); analyzeButton->setEnabled(m_filesModel->hasCheckedFiles()); // Buttons auto buttons = new QDialogButtonBox; buttons->setStandardButtons(QDialogButtonBox::Cancel); buttons->addButton(analyzeButton, QDialogButtonBox::AcceptRole); connect(m_filesModel.get(), &QAbstractItemModel::dataChanged, this, [this, analyzeButton] { analyzeButton->setEnabled(m_filesModel->hasCheckedFiles()); }); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); using namespace Layouting; Column { m_fileFilterComboBox, Core::ItemViewFind::createSearchableWrapper(m_fileView, Core::ItemViewFind::LightColored), buttons }.attachTo(this); } SelectableFilesDialog::~SelectableFilesDialog() = default; FileInfos SelectableFilesDialog::fileInfos() const { return m_filesModel->selectedFileInfos(); } int SelectableFilesDialog::currentProviderIndex() const { return m_fileFilterComboBox->currentIndex(); } 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_fileFilterComboBox->currentIndex()]; provider.onSelectionAccepted(selection); QDialog::accept(); } } // namespace Internal } // namespace ClangTools #include "clangselectablefilesdialog.moc"