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"