File indexing completed on 2024-05-05 04:40:14

0001 /*
0002     SPDX-FileCopyrightText: 2006-2007 Hamish Rodda <rodda@kde.org>
0003     SPDX-FileCopyrightText: 2006 Adam Treat <treat@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "problemtreeview.h"
0009 
0010 #include <QAction>
0011 #include <QApplication>
0012 #include <QClipboard>
0013 #include <QContextMenuEvent>
0014 #include <QHeaderView>
0015 #include <QItemDelegate>
0016 #include <QMenu>
0017 #include <QSortFilterProxyModel>
0018 
0019 #include <KLocalizedString>
0020 
0021 #include <interfaces/icore.h>
0022 #include <interfaces/idocumentcontroller.h>
0023 #include <interfaces/iassistant.h>
0024 #include <language/duchain/duchain.h>
0025 #include <language/duchain/duchainlock.h>
0026 #include <language/editor/documentrange.h>
0027 #include <util/kdevstringhandler.h>
0028 
0029 #include "problemreporterplugin.h"
0030 #include <shell/problemmodel.h>
0031 #include <shell/problem.h>
0032 #include <shell/problemconstants.h>
0033 
0034 #include <algorithm>
0035 #include <array>
0036 
0037 using namespace KDevelop;
0038 
0039 namespace {
0040 QString descriptionFromProblem(IProblem::Ptr problem)
0041 {
0042     QString text;
0043     const auto location = problem->finalLocation();
0044     if (location.isValid()) {
0045         text += location.document.toUrl()
0046             .adjusted(QUrl::NormalizePathSegments)
0047             .toDisplayString(QUrl::PreferLocalFile);
0048         if (location.start().line() >= 0) {
0049             text += QLatin1Char(':') + QString::number(location.start().line() + 1);
0050             if (location.start().column() >= 0) {
0051                 text += QLatin1Char(':') + QString::number(location.start().column() + 1);
0052             }
0053         }
0054         text += QLatin1String(": ");
0055     }
0056     text += problem->description();
0057     if (!problem->explanation().isEmpty()) {
0058         text += QLatin1Char('\n') + problem->explanation();
0059     }
0060     return text;
0061 }
0062 }
0063 
0064 namespace KDevelop
0065 {
0066 
0067 class ProblemTreeViewItemDelegate : public QItemDelegate
0068 {
0069     Q_OBJECT
0070 
0071 public:
0072     explicit ProblemTreeViewItemDelegate(QObject* parent = nullptr);
0073 
0074     void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
0075 };
0076 }
0077 
0078 ProblemTreeViewItemDelegate::ProblemTreeViewItemDelegate(QObject* parent)
0079     : QItemDelegate(parent)
0080 {
0081 }
0082 
0083 void ProblemTreeViewItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
0084                                         const QModelIndex& index) const
0085 {
0086     QStyleOptionViewItem newOption(option);
0087     newOption.textElideMode = index.column() == ProblemModel::File ? Qt::ElideMiddle : Qt::ElideRight;
0088 
0089     QItemDelegate::paint(painter, newOption, index);
0090 }
0091 
0092 ProblemTreeView::ProblemTreeView(QWidget* parent, QAbstractItemModel* itemModel)
0093     : QTreeView(parent)
0094     , m_proxy(new QSortFilterProxyModel(this))
0095 {
0096     setObjectName(QStringLiteral("Problem Reporter Tree"));
0097     setWhatsThis(i18nc("@info:whatsthis", "Problems"));
0098     setItemDelegate(new ProblemTreeViewItemDelegate(this));
0099     setSelectionBehavior(QAbstractItemView::SelectRows);
0100     setUniformRowHeights(true);
0101 
0102     m_proxy->setSortRole(ProblemModel::SeverityRole);
0103     m_proxy->setDynamicSortFilter(true);
0104     m_proxy->sort(0, Qt::AscendingOrder);
0105 
0106     auto* problemModel = qobject_cast<ProblemModel*>(itemModel);
0107     Q_ASSERT(problemModel);
0108     setModel(problemModel);
0109 
0110     header()->setStretchLastSection(false);
0111     if (!problemModel->features().testFlag(ProblemModel::ShowSource)) {
0112         hideColumn(ProblemModel::Source);
0113     }
0114 
0115     connect(this, &ProblemTreeView::clicked, this, &ProblemTreeView::itemActivated);
0116 
0117     connect(model(), &QAbstractItemModel::rowsInserted, this, &ProblemTreeView::changed);
0118     connect(model(), &QAbstractItemModel::rowsRemoved, this, &ProblemTreeView::changed);
0119     connect(model(), &QAbstractItemModel::modelReset, this, &ProblemTreeView::changed);
0120 
0121     m_proxy->setFilterKeyColumn(-1);
0122     m_proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
0123 
0124     resizeColumns();
0125 }
0126 
0127 ProblemTreeView::~ProblemTreeView()
0128 {
0129 }
0130 
0131 void ProblemTreeView::openDocumentForCurrentProblem()
0132 {
0133     itemActivated(currentIndex());
0134 }
0135 
0136 void ProblemTreeView::itemActivated(const QModelIndex& index)
0137 {
0138     if (!index.isValid())
0139         return;
0140 
0141     KTextEditor::Cursor start;
0142     QUrl url;
0143 
0144     {
0145         // TODO: is this really necessary?
0146         DUChainReadLocker lock(DUChain::lock());
0147         const auto problem = index.data(ProblemModel::ProblemRole).value<IProblem::Ptr>();
0148         if (!problem)
0149             return;
0150 
0151         url = problem->finalLocation().document.toUrl();
0152         start = problem->finalLocation().start();
0153     }
0154 
0155     if (QFile::exists(url.toLocalFile())) {
0156         ICore::self()->documentController()->openDocument(url, start);
0157     }
0158 }
0159 
0160 void ProblemTreeView::resizeColumns()
0161 {
0162     // Don't simply call QTreeView::resizeColumnToContents() for each column here,
0163     // because it is not useful enough to justify significant performance cost.
0164     // Instead, set column widths to heuristic values independent on the contents (the problem list).
0165 
0166     const int averageCharWidth = fontMetrics().averageCharWidth();
0167     const int headerWidth = header()->width();
0168     if (averageCharWidth == m_averageCharWidth && headerWidth == m_headerWidth) {
0169         // No reason to change column widths. This early return is not just an optimization: KDevelop should not
0170         // gratuitously reapply unchanged heuristic column widths, because the user may have fine-tuned them manually.
0171         return;
0172     }
0173     m_averageCharWidth = averageCharWidth;
0174     m_headerWidth = headerWidth;
0175 
0176     struct ColumnSizePolicy
0177     {
0178         int minWidthInCharacters;
0179         int stretchFactor;
0180     };
0181     static constexpr std::array<ColumnSizePolicy, 5> sizePolicy{
0182         ColumnSizePolicy{40, 20}, // Error
0183         ColumnSizePolicy{25, 1}, //  Source
0184         ColumnSizePolicy{30, 10}, // File
0185         ColumnSizePolicy{10, 1}, //  Line
0186         ColumnSizePolicy{10, 1}, //  Column
0187     };
0188     static_assert(sizePolicy.size() == ProblemModel::LastColumn);
0189 
0190     // Cannot use std::accumulate() here, because it is not constexpr in C++17.
0191     static constexpr ColumnSizePolicy totalAllColumns = [] {
0192         ColumnSizePolicy sum{};
0193         for (auto p : sizePolicy) {
0194             sum.minWidthInCharacters += p.minWidthInCharacters;
0195             sum.stretchFactor += p.stretchFactor;
0196         }
0197         return sum;
0198     }();
0199 
0200     ColumnSizePolicy total = totalAllColumns;
0201     if (!model()->features().testFlag(ProblemModel::ShowSource)) {
0202         // Disregard the size policy of the hidden Source column.
0203         static constexpr auto hiddenColumn = sizePolicy[ProblemModel::Source];
0204         total.minWidthInCharacters -= hiddenColumn.minWidthInCharacters;
0205         total.stretchFactor -= hiddenColumn.stretchFactor;
0206     }
0207     Q_ASSERT(total.stretchFactor > 0);
0208 
0209     const int remainingPixels = std::max(0, headerWidth - total.minWidthInCharacters * averageCharWidth);
0210 
0211     // Give each column its minimum needed width. If there is any horizontal space left,
0212     // distribute it among columns in proportion to their stretch factors.
0213     for (std::size_t i = 0; i < sizePolicy.size(); ++i) {
0214         int width = sizePolicy[i].minWidthInCharacters * averageCharWidth;
0215         width += remainingPixels * sizePolicy[i].stretchFactor / total.stretchFactor;
0216         setColumnWidth(i, width);
0217     }
0218 }
0219 
0220 int ProblemTreeView::setFilter(const QString& filterText)
0221 {
0222     m_proxy->setFilterFixedString(filterText);
0223 
0224     return m_proxy->rowCount();
0225 }
0226 
0227 ProblemModel* ProblemTreeView::model() const
0228 {
0229     return static_cast<ProblemModel*>(m_proxy->sourceModel());
0230 }
0231 
0232 void ProblemTreeView::setModel(QAbstractItemModel* model)
0233 {
0234     Q_ASSERT(qobject_cast<ProblemModel*>(model));
0235     m_proxy->setSourceModel(model);
0236     QTreeView::setModel(m_proxy);
0237 }
0238 
0239 void ProblemTreeView::contextMenuEvent(QContextMenuEvent* event)
0240 {
0241     QModelIndex index = indexAt(event->pos());
0242     if (!index.isValid())
0243         return;
0244 
0245     const auto problem = index.data(ProblemModel::ProblemRole).value<IProblem::Ptr>();
0246     if (!problem) {
0247         return;
0248     }
0249 
0250     QPointer<QMenu> m = new QMenu(this);
0251 
0252     m->addSection(i18nc("@title:menu", "Problem"));
0253     auto copyDescriptionAction = m->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")),
0254                                               i18nc("@action:inmenu", "&Copy Description"));
0255     connect(copyDescriptionAction, &QAction::triggered, this, [problem]() {
0256         QApplication::clipboard()->setText(descriptionFromProblem(problem), QClipboard::Clipboard);
0257     });
0258 
0259     QExplicitlySharedDataPointer<KDevelop::IAssistant> solution = problem->solutionAssistant();
0260     if (solution && !solution->actions().isEmpty()) {
0261         QList<QAction*> actions;
0262         const auto solutionActions = solution->actions();
0263         actions.reserve(solutionActions.size());
0264         for (auto assistantAction : solutionActions) {
0265             auto action = assistantAction->toQAction(m.data());
0266             action->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply")));
0267             actions << action;
0268         }
0269 
0270         QString title = solution->title();
0271         title = KDevelop::htmlToPlainText(title);
0272         title.replace(QLatin1String("&apos;"), QLatin1String("\'"));
0273         m->addSection(i18nc("@title:menu", "Solve: %1", title));
0274         m->addActions(actions);
0275     }
0276 
0277     m->exec(event->globalPos());
0278     delete m;
0279 
0280 }
0281 
0282 void ProblemTreeView::resizeEvent(QResizeEvent* event)
0283 {
0284     QTreeView::resizeEvent(event);
0285     // resizeEvent() is invoked whenever this tree view is resized and also whenever the default system font
0286     // changes. So the resizeColumns() call below should cover all scenarios where heuristic column widths change.
0287     resizeColumns();
0288 }
0289 
0290 #include "problemtreeview.moc"
0291 #include "moc_problemtreeview.cpp"