File indexing completed on 2024-05-05 16:46:08
0001 /* 0002 SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "probleminlinenoteprovider.h" 0008 0009 #include <interfaces/icore.h> 0010 #include <interfaces/ilanguagecontroller.h> 0011 #include <language/editor/documentrange.h> 0012 0013 #include <KColorScheme> 0014 #include <KTextEditor/Document> 0015 #include <KTextEditor/InlineNoteInterface> 0016 #include <KTextEditor/View> 0017 0018 #include <QIcon> 0019 #include <QPainter> 0020 #include <QFontMetrics> 0021 #include <QStyle> 0022 0023 using namespace KDevelop; 0024 0025 static constexpr int marginColumns = 2; 0026 0027 struct SeverityColors 0028 { 0029 QColor foreground; 0030 QColor background; 0031 }; 0032 0033 static SeverityColors severityColors(IProblem::Severity severity) 0034 { 0035 const KColorScheme::ForegroundRole foregroundRole = 0036 (severity == IProblem::Error) ? KColorScheme::NegativeText : 0037 (severity == IProblem::Warning) ? KColorScheme::NeutralText : 0038 /* IProblem::Hint, default */ KColorScheme::PositiveText; 0039 const KColorScheme::BackgroundRole backgroundRole = 0040 (severity == IProblem::Error) ? KColorScheme::NegativeBackground : 0041 (severity == IProblem::Warning) ? KColorScheme::NeutralBackground : 0042 /* IProblem::Hint, default */ KColorScheme::PositiveBackground; 0043 0044 KColorScheme scheme(QPalette::Active); 0045 return { 0046 scheme.foreground(foregroundRole).color(), 0047 scheme.background(backgroundRole).color() 0048 }; 0049 } 0050 0051 ProblemInlineNoteProvider::ProblemInlineNoteProvider(KTextEditor::Document* document) 0052 : m_document{document} 0053 { 0054 auto registerProvider = [this] (KTextEditor::Document*, KTextEditor::View* view) { 0055 auto* inlineNoteIface = qobject_cast<KTextEditor::InlineNoteInterface*>(view); 0056 if(inlineNoteIface) { 0057 inlineNoteIface->registerInlineNoteProvider(this); 0058 } 0059 }; 0060 for (auto* view : m_document->views()) { 0061 registerProvider(m_document, view); 0062 } 0063 connect(m_document, &KTextEditor::Document::viewCreated, this, registerProvider); 0064 connect(ICore::self()->languageController()->completionSettings(), &ICompletionSettings::settingsChanged, 0065 this, &ProblemInlineNoteProvider::completionSettingsChanged); 0066 } 0067 0068 ProblemInlineNoteProvider::~ProblemInlineNoteProvider() 0069 { 0070 if (!m_document) { 0071 return; 0072 } 0073 for (auto* view : m_document->views()) { 0074 auto* inlineNoteIface = qobject_cast<KTextEditor::InlineNoteInterface*>(view); 0075 if(inlineNoteIface) { 0076 inlineNoteIface->unregisterInlineNoteProvider(this); 0077 } 0078 } 0079 } 0080 0081 void ProblemInlineNoteProvider::completionSettingsChanged() 0082 { 0083 if (m_currentLevel == ICore::self()->languageController()->completionSettings()->problemInlineNotesLevel()) { 0084 return; 0085 } 0086 setProblems(m_problems); 0087 } 0088 0089 void ProblemInlineNoteProvider::setProblems(const QVector<IProblem::Ptr>& problems) 0090 { 0091 if (!m_document) { 0092 return; 0093 } 0094 m_problemForLine.clear(); 0095 m_problems = problems; 0096 if (problems.isEmpty()) { 0097 emit inlineNotesReset(); 0098 return; 0099 } 0100 m_currentLevel = ICore::self()->languageController()->completionSettings()->problemInlineNotesLevel() ; 0101 if (m_currentLevel == ICompletionSettings::NoProblemsInlineNotesLevel) { 0102 return; 0103 } 0104 for (const IProblem::Ptr& problem : problems) { 0105 if (problem->finalLocation().document.toUrl() != m_document->url() || !problem->finalLocation().isValid()) { 0106 continue; 0107 } 0108 switch (problem->severity()) { 0109 case IProblem::NoSeverity: 0110 case IProblem::Hint: 0111 if (m_currentLevel != ICompletionSettings::AllProblemsInlineNotesLevel) { 0112 continue; 0113 } 0114 break; 0115 case IProblem::Warning: 0116 if (m_currentLevel == ICompletionSettings::ErrorsProblemInlineNotesLevel) { 0117 continue; 0118 } 0119 break; 0120 case IProblem::Error: 0121 break; 0122 } 0123 const int line = problem->finalLocation().start().line(); 0124 // Only render the problem with the highest severity in each line. 0125 if (m_problemForLine.contains(line)) { 0126 const IProblem::Ptr currentProblem = m_problemForLine.value(line); 0127 if (problem->severity() == currentProblem->severity()) { 0128 if (problem->finalLocation().start().column() < currentProblem->finalLocation().start().column()) { 0129 m_problemForLine[line] = problem; 0130 } 0131 // No Severity has the lowest value 0132 } else if (problem->severity() < currentProblem->severity() && problem->severity() != IProblem::NoSeverity) { 0133 m_problemForLine[line] = problem; 0134 } else if (currentProblem->severity() == IProblem::NoSeverity) { 0135 m_problemForLine[line] = problem; 0136 } 0137 } else { 0138 m_problemForLine[line] = problem; 0139 } 0140 } 0141 emit inlineNotesReset(); 0142 } 0143 0144 QVector<int> ProblemInlineNoteProvider::inlineNotes(int line) const 0145 { 0146 return m_problemForLine.contains(line) ? QVector<int>(1, m_document->endOfLine(line).column() + marginColumns) 0147 : QVector<int>(); 0148 } 0149 0150 // matching logic of KateIconBorder's icon height calculation for a line 0151 static constexpr int iconTopBottomMargin = 1; 0152 0153 static int iconHeight(const KTextEditor::InlineNote& note) 0154 { 0155 QFontMetrics fontMetrics(note.font()); 0156 0157 return qMin(fontMetrics.height(), note.lineHeight()) - 2 * iconTopBottomMargin; 0158 } 0159 0160 static constexpr int noteBorderWidth = 2; 0161 0162 struct InlineNoteLayout 0163 { 0164 int iconSize; 0165 int iconX; 0166 int descriptionX; 0167 int rightMargin; 0168 }; 0169 0170 0171 // Design of note: 0172 // following basically the message widget design, but reduced to only a colored border on the left side 0173 // 0174 // |_O_description_ 0175 // <vertical borderline><left margin><icon><spacing><description text><right margin> 0176 static void doInlineNoteLayout(const KTextEditor::InlineNote& note, 0177 InlineNoteLayout* layout) 0178 { 0179 const auto* view = note.view(); 0180 const auto* style = view->style(); 0181 const int leftMargin = style->pixelMetric(QStyle::PM_LayoutLeftMargin, nullptr, view); 0182 layout->rightMargin = style->pixelMetric(QStyle::PM_LayoutRightMargin, nullptr, view); 0183 const int noteSpacing = style->pixelMetric(QStyle::PM_LayoutHorizontalSpacing, nullptr, view); 0184 0185 layout->iconSize = iconHeight(note); 0186 0187 layout->iconX = noteBorderWidth + leftMargin; 0188 layout->descriptionX = layout->iconX + layout->iconSize + noteSpacing; 0189 } 0190 0191 QSize ProblemInlineNoteProvider::inlineNoteSize(const KTextEditor::InlineNote& note) const 0192 { 0193 InlineNoteLayout layout; 0194 doInlineNoteLayout(note, &layout); 0195 0196 const auto prob = m_problemForLine[note.position().line()]; 0197 QFont font = note.font(); 0198 font.setItalic(true); 0199 const QFontMetrics metric(font); 0200 const QRect boundingRect = metric.boundingRect(prob->description()); 0201 return {layout.descriptionX + boundingRect.width() + layout.rightMargin, note.lineHeight()}; 0202 } 0203 0204 void ProblemInlineNoteProvider::paintInlineNote(const KTextEditor::InlineNote& note, QPainter& painter) const 0205 { 0206 InlineNoteLayout layout; 0207 doInlineNoteLayout(note, &layout); 0208 0209 const int line = note.position().line(); 0210 const auto prob = m_problemForLine[note.position().line()]; 0211 QFont font = note.font(); 0212 font.setItalic(true); 0213 painter.setFont(font); 0214 const KTextEditor::View* view = note.view(); 0215 // NOTE cursorToCoordinate is relative to (0,0) of the view widget so we have to subtract the x 0216 // value of the start of the line from it. However it returns also -1 for cursors that have no 0217 // actual text so we subtract the width of the margin column(s) from the available width 0218 const int viewWidth = view->textAreaRect().width(); 0219 const int textAreaStart = view->cursorToCoordinate(KTextEditor::Cursor(line, 0)).x(); 0220 const int marginWidth = view->cursorToCoordinate(KTextEditor::Cursor(line, marginColumns)).x() - textAreaStart; 0221 const int nonTextSize = layout.descriptionX + layout.rightMargin; 0222 const int lineEnd = view->cursorToCoordinate(KTextEditor::Cursor(line, note.position().column() - marginColumns)).x() - textAreaStart; 0223 const int availableTextWidth = viewWidth - marginWidth - lineEnd - nonTextSize; 0224 QString text = painter.fontMetrics().elidedText(prob->description(), Qt::ElideRight, availableTextWidth); 0225 QIcon icon = IProblem::iconForSeverity(prob->severity()); 0226 // QFontMetrics doesn't provide results that are good enough for painting so we use QPainter::boundingRect here 0227 QRect boundingRect = painter.boundingRect(QRect(layout.descriptionX, 0, 0, 0), Qt::AlignLeft, text); 0228 const auto colors = severityColors(prob->severity()); 0229 // background 0230 painter.setBrush(colors.background); 0231 painter.setPen(Qt::NoPen); 0232 painter.drawRect(boundingRect.adjusted(-(layout.descriptionX), 0, layout.rightMargin, 0)); 0233 // borderline 0234 painter.setBrush(colors.foreground); 0235 painter.drawRect(QRect(0, 0, noteBorderWidth, note.lineHeight())); 0236 // icpn 0237 icon.paint(&painter, layout.iconX, iconTopBottomMargin, layout.iconSize, layout.iconSize, Qt::AlignCenter); 0238 // text 0239 painter.setPen(colors.foreground); 0240 painter.drawText(boundingRect, Qt::AlignLeft, text); 0241 } 0242 0243 #include "moc_probleminlinenoteprovider.cpp"