File indexing completed on 2024-05-05 16:46:09
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("'"), 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"