File indexing completed on 2024-05-05 04:40:14
0001 /* 0002 SPDX-FileCopyrightText: 2015 Laszlo Kis-Adam <laszlo.kis-adam@kdemail.net> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "problemsview.h" 0008 0009 #include <KActionMenu> 0010 #include <KLocalizedString> 0011 0012 #include <QAction> 0013 #include <QActionGroup> 0014 #include <QLineEdit> 0015 #include <QMenu> 0016 #include <QTabWidget> 0017 #include <QTimer> 0018 #include <QVBoxLayout> 0019 0020 #include <interfaces/icore.h> 0021 #include <interfaces/ilanguagecontroller.h> 0022 #include <shell/problemconstants.h> 0023 #include <shell/problemmodelset.h> 0024 #include <util/expandablelineedit.h> 0025 #include "problemtreeview.h" 0026 #include "problemmodel.h" 0027 0028 namespace KDevelop 0029 { 0030 0031 void ProblemsView::setupActions() 0032 { 0033 { 0034 m_fullUpdateAction = new QAction(this); 0035 m_fullUpdateAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); 0036 m_fullUpdateAction->setText(i18nc("@action", "Force Full Update")); 0037 m_fullUpdateAction->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); 0038 connect(m_fullUpdateAction, &QAction::triggered, this, [this]() { 0039 currentView()->model()->forceFullUpdate(); 0040 }); 0041 addAction(m_fullUpdateAction); 0042 } 0043 0044 { 0045 m_scopeMenu = new KActionMenu(this); 0046 m_scopeMenu->setPopupMode(QToolButton::InstantPopup); 0047 m_scopeMenu->setToolTip(i18nc("@info:tooltip", "Which files to display the problems for")); 0048 m_scopeMenu->setObjectName(QStringLiteral("scopeMenu")); 0049 0050 auto* scopeActions = new QActionGroup(this); 0051 0052 m_currentDocumentAction = new QAction(this); 0053 m_currentDocumentAction->setText(i18nc("@option:check", "Current Document")); 0054 m_currentDocumentAction->setToolTip(i18nc("@info:tooltip", "Display problems in current document")); 0055 0056 auto* openDocumentsAction = new QAction(this); 0057 openDocumentsAction->setText(i18nc("@option:check", "Open Documents")); 0058 openDocumentsAction->setToolTip(i18nc("@info:tooltip", "Display problems in all open documents")); 0059 0060 auto* currentProjectAction = new QAction(this); 0061 currentProjectAction->setText(i18nc("@option:check", "Current Project")); 0062 currentProjectAction->setToolTip(i18nc("@info:tooltip", "Display problems in current project")); 0063 0064 auto* allProjectAction = new QAction(this); 0065 allProjectAction->setText(i18nc("@option:check", "All Projects")); 0066 allProjectAction->setToolTip(i18nc("@info:tooltip", "Display problems in all projects")); 0067 0068 auto* documentsInPathAction = new QAction(this); 0069 documentsInPathAction->setText(i18nc("@option:check", "Documents In Path")); 0070 documentsInPathAction->setToolTip(i18nc("@info:tooltip", "Display problems from all files in a specific path")); 0071 0072 auto* documentsInCurrentPathAction = new QAction(this); 0073 documentsInCurrentPathAction->setText(i18nc("@option:check", "Documents In Current Path")); 0074 documentsInCurrentPathAction->setToolTip( 0075 i18nc("@info:tooltip", "Display problems from all files in the path of the current document")); 0076 0077 m_showAllAction = new QAction(this); 0078 m_showAllAction->setText(i18nc("@option:check", "Show All")); 0079 m_showAllAction->setToolTip(i18nc("@info:tooltip", "Display all problems")); 0080 0081 QAction* const actions[] = { 0082 m_currentDocumentAction, openDocumentsAction, currentProjectAction, allProjectAction, 0083 documentsInPathAction, documentsInCurrentPathAction, m_showAllAction, 0084 }; 0085 0086 for (QAction* action : actions) { 0087 action->setCheckable(true); 0088 scopeActions->addAction(action); 0089 m_scopeMenu->addAction(action); 0090 } 0091 addAction(m_scopeMenu); 0092 0093 { 0094 auto* updatePathTimer = new QTimer(this); 0095 updatePathTimer->setSingleShot(true); 0096 updatePathTimer->setInterval(500); 0097 0098 auto* pathEdit = new KExpandableLineEdit(this); 0099 pathEdit->setClearButtonEnabled(true); 0100 pathEdit->setPlaceholderText(i18nc("@info:placeholder", "Path Filter...")); 0101 0102 connect(updatePathTimer, &QTimer::timeout, this, 0103 [this, pathEdit]() { currentView()->model()->setPathForDocumentsInPathScope(pathEdit->text()); }); 0104 0105 connect(pathEdit, &QLineEdit::textChanged, updatePathTimer, 0106 static_cast<void (QTimer::*)()>(&QTimer::start)); 0107 0108 auto* pathForForDocumentsInPathAction = new QWidgetAction(this); 0109 pathForForDocumentsInPathAction->setDefaultWidget(pathEdit); 0110 0111 addAction(pathForForDocumentsInPathAction); 0112 0113 connect(documentsInPathAction, &QAction::toggled, pathForForDocumentsInPathAction, &QAction::setVisible); 0114 pathForForDocumentsInPathAction->setVisible(false); 0115 } 0116 0117 connect(m_currentDocumentAction, &QAction::triggered, this, [this](){ setScope(CurrentDocument); }); 0118 connect(openDocumentsAction, &QAction::triggered, this, [this](){ setScope(OpenDocuments); }); 0119 connect(currentProjectAction, &QAction::triggered, this, [this](){ setScope(CurrentProject); }); 0120 connect(allProjectAction, &QAction::triggered, this, [this](){ setScope(AllProjects); }); 0121 connect(documentsInPathAction, &QAction::triggered, this, [this]() { setScope(DocumentsInPath); }); 0122 connect(documentsInCurrentPathAction, &QAction::triggered, this, 0123 [this]() { setScope(DocumentsInCurrentPath); }); 0124 connect(m_showAllAction, &QAction::triggered, this, [this](){ setScope(BypassScopeFilter); }); 0125 } 0126 0127 { 0128 m_showImportsAction = new QAction(this); 0129 addAction(m_showImportsAction); 0130 m_showImportsAction->setCheckable(true); 0131 m_showImportsAction->setChecked(false); 0132 m_showImportsAction->setText(i18nc("@option:check", "Show Imports")); 0133 m_showImportsAction->setToolTip(i18nc("@info:tooltip", "Display problems in imported files")); 0134 connect(m_showImportsAction, &QAction::triggered, this, [this](bool checked) { 0135 currentView()->model()->setShowImports(checked); 0136 }); 0137 } 0138 0139 { 0140 m_severityActions = new QActionGroup(this); 0141 0142 m_errorSeverityAction = new QAction(this); 0143 m_errorSeverityAction->setToolTip(i18nc("@info:tooltip", "Display errors")); 0144 m_errorSeverityAction->setIcon(IProblem::iconForSeverity(IProblem::Error)); 0145 m_errorSeverityAction->setIconText(i18nc("@option:check", "Show Errors")); 0146 0147 m_warningSeverityAction = new QAction(this); 0148 m_warningSeverityAction->setToolTip(i18nc("@info:tooltip", "Display warnings")); 0149 m_warningSeverityAction->setIcon(IProblem::iconForSeverity(IProblem::Warning)); 0150 m_warningSeverityAction->setIconText(i18nc("@option:check", "Show Warnings")); 0151 0152 m_hintSeverityAction = new QAction(this); 0153 m_hintSeverityAction->setToolTip(i18nc("@info:tooltip", "Display hints")); 0154 m_hintSeverityAction->setIcon(IProblem::iconForSeverity(IProblem::Hint)); 0155 m_hintSeverityAction->setIconText(i18nc("@option:check", "Show Hints")); 0156 0157 QAction* const severityActionArray[] = { m_errorSeverityAction, m_warningSeverityAction, m_hintSeverityAction }; 0158 for (auto* action : severityActionArray) { 0159 action->setCheckable(true); 0160 m_severityActions->addAction(action); 0161 addAction(action); 0162 } 0163 m_severityActions->setExclusive(false); 0164 0165 m_hintSeverityAction->setChecked(true); 0166 m_warningSeverityAction->setChecked(true); 0167 m_errorSeverityAction->setChecked(true); 0168 0169 connect(m_errorSeverityAction, &QAction::toggled, this, &ProblemsView::handleSeverityActionToggled); 0170 connect(m_warningSeverityAction, &QAction::toggled, this, &ProblemsView::handleSeverityActionToggled); 0171 connect(m_hintSeverityAction, &QAction::toggled, this, &ProblemsView::handleSeverityActionToggled); 0172 } 0173 0174 { 0175 m_groupingMenu = new KActionMenu(i18nc("@title:menu", "Grouping"), this); 0176 m_groupingMenu->setPopupMode(QToolButton::InstantPopup); 0177 0178 auto* groupingActions = new QActionGroup(this); 0179 0180 auto* noGroupingAction = new QAction(i18nc("@option:check", "None"), this); 0181 auto* pathGroupingAction = new QAction(i18nc("@option:check", "Path"), this); 0182 auto* severityGroupingAction = new QAction(i18nc("@option:check", "Severity"), this); 0183 0184 QAction* const groupingActionArray[] = { noGroupingAction, pathGroupingAction, severityGroupingAction }; 0185 for (auto* action : groupingActionArray) { 0186 action->setCheckable(true); 0187 groupingActions->addAction(action); 0188 m_groupingMenu->addAction(action); 0189 } 0190 addAction(m_groupingMenu); 0191 0192 noGroupingAction->setChecked(true); 0193 0194 connect(noGroupingAction, &QAction::triggered, this, [this](){ currentView()->model()->setGrouping(NoGrouping); }); 0195 connect(pathGroupingAction, &QAction::triggered, this, [this](){ currentView()->model()->setGrouping(PathGrouping); }); 0196 connect(severityGroupingAction, &QAction::triggered, this, [this](){ currentView()->model()->setGrouping(SeverityGrouping); }); 0197 } 0198 0199 { 0200 auto* filterTimer = new QTimer(this); 0201 filterTimer->setSingleShot(true); 0202 filterTimer->setInterval(500); 0203 0204 connect(filterTimer, &QTimer::timeout, this, [this]() { 0205 setFilter(m_filterEdit->text()); 0206 }); 0207 0208 m_filterEdit = new KExpandableLineEdit(this); 0209 m_filterEdit->setClearButtonEnabled(true); 0210 m_filterEdit->setPlaceholderText(i18nc("@info:placeholder", "Search...")); 0211 0212 connect(m_filterEdit, &QLineEdit::textChanged, 0213 filterTimer, static_cast<void (QTimer::*)()>(&QTimer::start)); 0214 0215 auto* filterAction = new QWidgetAction(this); 0216 filterAction->setDefaultWidget(m_filterEdit); 0217 addAction(filterAction); 0218 0219 m_prevTabIdx = -1; 0220 setFocusProxy(m_filterEdit); 0221 } 0222 } 0223 0224 void ProblemsView::updateActions() 0225 { 0226 auto problemModel = currentView()->model(); 0227 Q_ASSERT(problemModel); 0228 0229 m_fullUpdateAction->setVisible(problemModel->features().testFlag(ProblemModel::CanDoFullUpdate)); 0230 m_fullUpdateAction->setToolTip(problemModel->fullUpdateTooltip()); 0231 m_showImportsAction->setVisible(problemModel->features().testFlag(ProblemModel::CanShowImports)); 0232 m_scopeMenu->setVisible(problemModel->features().testFlag(ProblemModel::ScopeFilter)); 0233 m_severityActions->setVisible(problemModel->features().testFlag(ProblemModel::SeverityFilter)); 0234 m_groupingMenu->setVisible(problemModel->features().testFlag(ProblemModel::Grouping)); 0235 0236 m_showAllAction->setVisible(problemModel->features().testFlag(ProblemModel::CanByPassScopeFilter)); 0237 0238 m_showImportsAction->setChecked(false); 0239 problemModel->setShowImports(false); 0240 0241 // Show All should be default if it's supported. It helps with error messages that are otherwise invisible 0242 if (problemModel->features().testFlag(ProblemModel::CanByPassScopeFilter)) { 0243 //actions.last()->setChecked(true); 0244 setScope(BypassScopeFilter); 0245 } else { 0246 m_currentDocumentAction->setChecked(true); 0247 setScope(CurrentDocument); 0248 } 0249 0250 problemModel->setSeverities(IProblem::Error | IProblem::Warning | IProblem::Hint); 0251 0252 setFocus(); // set focus to default widget (filterEdit) 0253 } 0254 0255 /// TODO: Move to util? 0256 /// Note: Support for recursing into child indices would be nice 0257 class ItemViewWalker 0258 { 0259 public: 0260 explicit ItemViewWalker(QItemSelectionModel* itemView); 0261 0262 void selectNextIndex(); 0263 void selectPreviousIndex(); 0264 0265 enum Direction { NextIndex, PreviousIndex }; 0266 void selectIndex(Direction direction); 0267 0268 private: 0269 QItemSelectionModel* m_selectionModel; 0270 }; 0271 0272 ItemViewWalker::ItemViewWalker(QItemSelectionModel* itemView) 0273 : m_selectionModel(itemView) 0274 { 0275 } 0276 0277 void ItemViewWalker::selectNextIndex() 0278 { 0279 selectIndex(NextIndex); 0280 } 0281 0282 void ItemViewWalker::selectPreviousIndex() 0283 { 0284 selectIndex(PreviousIndex); 0285 } 0286 0287 void ItemViewWalker::selectIndex(Direction direction) 0288 { 0289 if (!m_selectionModel) { 0290 return; 0291 } 0292 0293 const QModelIndexList list = m_selectionModel->selectedRows(); 0294 0295 const QModelIndex currentIndex = list.value(0); 0296 if (!currentIndex.isValid()) { 0297 /// no selection yet, just select the first 0298 const QModelIndex firstIndex = m_selectionModel->model()->index(0, 0); 0299 m_selectionModel->setCurrentIndex(firstIndex, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); 0300 return; 0301 } 0302 0303 const int nextRow = currentIndex.row() + (direction == NextIndex ? 1 : -1); 0304 const QModelIndex nextIndex = currentIndex.sibling(nextRow, 0); 0305 if (!nextIndex.isValid()) { 0306 return; /// never invalidate the selection 0307 } 0308 0309 m_selectionModel->setCurrentIndex(nextIndex, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); 0310 } 0311 0312 ProblemsView::ProblemsView(QWidget* parent) 0313 : QWidget(parent) 0314 { 0315 setWindowTitle(i18nc("@title:window", "Problems")); 0316 setWindowIcon(QIcon::fromTheme(QStringLiteral("script-error"), windowIcon())); 0317 0318 auto layout = new QVBoxLayout(this); 0319 layout->setContentsMargins(0, 0, 0, 0); 0320 0321 m_tabWidget = new QTabWidget(this); 0322 m_tabWidget->setTabPosition(QTabWidget::South); 0323 m_tabWidget->setDocumentMode(true); 0324 layout->addWidget(m_tabWidget); 0325 0326 setupActions(); 0327 } 0328 0329 ProblemsView::~ProblemsView() 0330 { 0331 } 0332 0333 void ProblemsView::load() 0334 { 0335 m_tabWidget->clear(); 0336 0337 KDevelop::ProblemModelSet* pms = KDevelop::ICore::self()->languageController()->problemModelSet(); 0338 QVector<KDevelop::ModelData> v = pms->models(); 0339 0340 QVectorIterator<KDevelop::ModelData> itr(v); 0341 while (itr.hasNext()) { 0342 const KDevelop::ModelData& data = itr.next(); 0343 addModel(data); 0344 } 0345 0346 connect(pms, &ProblemModelSet::added, this, &ProblemsView::onModelAdded); 0347 connect(pms, &ProblemModelSet::removed, this, &ProblemsView::onModelRemoved); 0348 connect(m_tabWidget, &QTabWidget::currentChanged, this, &ProblemsView::onCurrentChanged); 0349 0350 if (m_tabWidget->currentIndex() == 0) { 0351 updateActions(); 0352 return; 0353 } 0354 0355 m_tabWidget->setCurrentIndex(0); 0356 } 0357 0358 void ProblemsView::onModelAdded(const ModelData& data) 0359 { 0360 addModel(data); 0361 } 0362 0363 void ProblemsView::showModel(const QString& id) 0364 { 0365 for (int i = 0; i < m_models.size(); ++i) { 0366 if (m_models[i].id == id) { 0367 m_tabWidget->setCurrentIndex(i); 0368 return; 0369 } 0370 } 0371 } 0372 0373 void ProblemsView::onModelRemoved(const QString& id) 0374 { 0375 for (int i = 0; i < m_models.size(); ++i) { 0376 if (m_models[i].id == id) { 0377 m_models.remove(i); 0378 QWidget* w = m_tabWidget->widget(i); 0379 m_tabWidget->removeTab(i); 0380 delete w; 0381 return; 0382 } 0383 } 0384 } 0385 0386 void ProblemsView::onCurrentChanged(int idx) 0387 { 0388 if (idx == -1) 0389 return; 0390 0391 setFilter(QString(), m_prevTabIdx); 0392 setFilter(QString()); 0393 m_prevTabIdx = idx; 0394 0395 updateActions(); 0396 } 0397 0398 void ProblemsView::onViewChanged() 0399 { 0400 auto* view = static_cast<ProblemTreeView*>(sender()); 0401 int idx = m_tabWidget->indexOf(view); 0402 int rows = view->model()->rowCount(); 0403 0404 updateTab(idx, rows); 0405 } 0406 0407 void ProblemsView::addModel(const ModelData& newData) 0408 { 0409 // We implement follows tabs order: 0410 // 0411 // 1) First tab always used by "Parser" model due to it's the most important 0412 // problem listing, it should be at the front (K.Funk idea at #kdevelop IRC channel). 0413 // 0414 // 2) Other tabs are alphabetically ordered. 0415 0416 const QString parserId = QStringLiteral("Parser"); 0417 0418 auto model = newData.model; 0419 auto view = new ProblemTreeView(nullptr, model); 0420 connect(view, &ProblemTreeView::changed, this, &ProblemsView::onViewChanged); 0421 connect(model, &ProblemModel::fullUpdateTooltipChanged, 0422 this, [this, model]() { 0423 if (currentView()->model() == model) { 0424 m_fullUpdateAction->setToolTip(model->fullUpdateTooltip()); 0425 } 0426 }); 0427 0428 int insertIdx = 0; 0429 if (newData.id != parserId) { 0430 for (insertIdx = 0; insertIdx < m_models.size(); ++insertIdx) { 0431 const ModelData& currentData = m_models[insertIdx]; 0432 0433 // Skip first element if it's already occupied by "Parser" model 0434 if (insertIdx == 0 && currentData.id == parserId) 0435 continue; 0436 0437 if (currentData.name.localeAwareCompare(newData.name) > 0) 0438 break; 0439 } 0440 } 0441 m_tabWidget->insertTab(insertIdx, view, newData.name); 0442 m_models.insert(insertIdx, newData); 0443 0444 updateTab(insertIdx, model->rowCount()); 0445 } 0446 0447 void ProblemsView::updateTab(int idx, int rows) 0448 { 0449 if (idx < 0 || idx >= m_models.size()) 0450 return; 0451 0452 const QString name = m_models[idx].name; 0453 const QString tabText = i18nc("@title:tab %1: tab name, %2: number of problems", "%1 (%2)", name, rows); 0454 m_tabWidget->setTabText(idx, tabText); 0455 } 0456 0457 ProblemTreeView* ProblemsView::currentView() const 0458 { 0459 return qobject_cast<ProblemTreeView*>(m_tabWidget->currentWidget()); 0460 } 0461 0462 void ProblemsView::selectNextItem() 0463 { 0464 auto view = currentView(); 0465 if (view) { 0466 ItemViewWalker walker(view->selectionModel()); 0467 walker.selectNextIndex(); 0468 view->openDocumentForCurrentProblem(); 0469 } 0470 } 0471 0472 void ProblemsView::selectPreviousItem() 0473 { 0474 auto view = currentView(); 0475 if (view) { 0476 ItemViewWalker walker(view->selectionModel()); 0477 walker.selectPreviousIndex(); 0478 view->openDocumentForCurrentProblem(); 0479 } 0480 } 0481 0482 void ProblemsView::handleSeverityActionToggled() 0483 { 0484 currentView()->model()->setSeverities( (m_errorSeverityAction->isChecked() ? IProblem::Error : IProblem::Severities()) | 0485 (m_warningSeverityAction->isChecked() ? IProblem::Warning : IProblem::Severities()) | 0486 (m_hintSeverityAction->isChecked() ? IProblem::Hint : IProblem::Severities()) ); 0487 } 0488 0489 void ProblemsView::setScope(ProblemScope scope) 0490 { 0491 m_scopeMenu->setText(i18nc("@title:menu", "Scope: %1", m_scopeMenu->menu()->actions().at(scope)->text())); 0492 0493 currentView()->model()->setScope(scope); 0494 } 0495 0496 void ProblemsView::setFilter(const QString& filterText) 0497 { 0498 setFilter(filterText, m_tabWidget->currentIndex()); 0499 } 0500 0501 void ProblemsView::setFilter(const QString& filterText, int tabIdx) 0502 { 0503 if (tabIdx < 0 || tabIdx >= m_tabWidget->count()) 0504 return; 0505 0506 auto* view = static_cast<ProblemTreeView*>(m_tabWidget->widget(tabIdx)); 0507 int rows = view->setFilter(filterText); 0508 0509 updateTab(tabIdx, rows); 0510 0511 if (tabIdx == m_tabWidget->currentIndex()) { 0512 QSignalBlocker blocker(m_filterEdit); 0513 m_filterEdit->setText(filterText); 0514 } 0515 } 0516 0517 } 0518 0519 #include "moc_problemsview.cpp"