File indexing completed on 2024-04-28 07:46:29
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch> 0004 SPDX-FileCopyrightText: 2003-2005 Hamish Rodda <rodda@kde.org> 0005 SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org> 0006 SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org> 0007 SPDX-FileCopyrightText: 1999 Jochen Wilhelmy <digisnap@cs.tu-berlin.de> 0008 SPDX-FileCopyrightText: 2013 Andrey Matveyakin <a.matveyakin@gmail.com> 0009 0010 SPDX-License-Identifier: LGPL-2.0-only 0011 */ 0012 0013 #include "katerenderer.h" 0014 0015 #include "inlinenotedata.h" 0016 #include "katebuffer.h" 0017 #include "katedocument.h" 0018 #include "kateextendedattribute.h" 0019 #include "katehighlight.h" 0020 #include "katerenderrange.h" 0021 #include "katetextlayout.h" 0022 #include "kateview.h" 0023 0024 #include "ktexteditor/attribute.h" 0025 #include "ktexteditor/inlinenote.h" 0026 #include "ktexteditor/inlinenoteprovider.h" 0027 0028 #include "katepartdebug.h" 0029 0030 #include <QBrush> 0031 #include <QPaintEngine> 0032 #include <QPainter> 0033 #include <QPainterPath> 0034 #include <QRegularExpression> 0035 #include <QStack> 0036 #include <QtMath> // qCeil 0037 0038 static const QChar tabChar(QLatin1Char('\t')); 0039 static const QChar spaceChar(QLatin1Char(' ')); 0040 static const QChar nbSpaceChar(0xa0); // non-breaking space 0041 0042 KateRenderer::KateRenderer(KTextEditor::DocumentPrivate *doc, Kate::TextFolding &folding, KTextEditor::ViewPrivate *view) 0043 : m_doc(doc) 0044 , m_folding(folding) 0045 , m_view(view) 0046 , m_tabWidth(m_doc->config()->tabWidth()) 0047 , m_indentWidth(m_doc->config()->indentationWidth()) 0048 , m_caretStyle(KTextEditor::caretStyles::Line) 0049 , m_drawCaret(true) 0050 , m_showSelections(true) 0051 , m_showTabs(true) 0052 , m_showSpaces(KateDocumentConfig::Trailing) 0053 , m_showNonPrintableSpaces(false) 0054 , m_printerFriendly(false) 0055 , m_config(new KateRendererConfig(this)) 0056 , m_font(m_config->baseFont()) 0057 , m_fontMetrics(m_font) 0058 { 0059 updateAttributes(); 0060 0061 // initialize with a sane font height 0062 updateFontHeight(); 0063 0064 // make the proper calculation for markerSize 0065 updateMarkerSize(); 0066 } 0067 0068 void KateRenderer::updateAttributes() 0069 { 0070 m_attributes = m_doc->highlight()->attributes(config()->schema()); 0071 } 0072 0073 const KTextEditor::Attribute::Ptr &KateRenderer::attribute(uint pos) const 0074 { 0075 if (pos < (uint)m_attributes.count()) { 0076 return m_attributes[pos]; 0077 } 0078 0079 return m_attributes[0]; 0080 } 0081 0082 KTextEditor::Attribute::Ptr KateRenderer::specificAttribute(int context) const 0083 { 0084 if (context >= 0 && context < m_attributes.count()) { 0085 return m_attributes[context]; 0086 } 0087 0088 return m_attributes[0]; 0089 } 0090 0091 void KateRenderer::setDrawCaret(bool drawCaret) 0092 { 0093 m_drawCaret = drawCaret; 0094 } 0095 0096 void KateRenderer::setCaretStyle(KTextEditor::caretStyles style) 0097 { 0098 m_caretStyle = style; 0099 } 0100 0101 void KateRenderer::setShowTabs(bool showTabs) 0102 { 0103 m_showTabs = showTabs; 0104 } 0105 0106 void KateRenderer::setShowSpaces(KateDocumentConfig::WhitespaceRendering showSpaces) 0107 { 0108 m_showSpaces = showSpaces; 0109 } 0110 0111 void KateRenderer::setShowNonPrintableSpaces(const bool on) 0112 { 0113 m_showNonPrintableSpaces = on; 0114 } 0115 0116 void KateRenderer::setTabWidth(int tabWidth) 0117 { 0118 m_tabWidth = tabWidth; 0119 } 0120 0121 bool KateRenderer::showIndentLines() const 0122 { 0123 return m_config->showIndentationLines(); 0124 } 0125 0126 void KateRenderer::setShowIndentLines(bool showIndentLines) 0127 { 0128 // invalidate our "active indent line" cached stuff 0129 m_currentBracketRange = KTextEditor::Range::invalid(); 0130 m_currentBracketX = -1; 0131 0132 m_config->setShowIndentationLines(showIndentLines); 0133 } 0134 0135 void KateRenderer::setIndentWidth(int indentWidth) 0136 { 0137 m_indentWidth = indentWidth; 0138 } 0139 0140 void KateRenderer::setShowSelections(bool showSelections) 0141 { 0142 m_showSelections = showSelections; 0143 } 0144 0145 void KateRenderer::increaseFontSizes(qreal step) const 0146 { 0147 QFont f(config()->baseFont()); 0148 f.setPointSizeF(f.pointSizeF() + step); 0149 config()->setFont(f); 0150 } 0151 0152 void KateRenderer::resetFontSizes() const 0153 { 0154 QFont f(KateRendererConfig::global()->baseFont()); 0155 config()->setFont(f); 0156 } 0157 0158 void KateRenderer::decreaseFontSizes(qreal step) const 0159 { 0160 QFont f(config()->baseFont()); 0161 if ((f.pointSizeF() - step) > 0) { 0162 f.setPointSizeF(f.pointSizeF() - step); 0163 } 0164 config()->setFont(f); 0165 } 0166 0167 bool KateRenderer::isPrinterFriendly() const 0168 { 0169 return m_printerFriendly; 0170 } 0171 0172 void KateRenderer::setPrinterFriendly(bool printerFriendly) 0173 { 0174 m_printerFriendly = printerFriendly; 0175 setShowTabs(false); 0176 setShowSpaces(KateDocumentConfig::None); 0177 setShowSelections(false); 0178 setDrawCaret(false); 0179 } 0180 0181 void KateRenderer::paintTextLineBackground(QPainter &paint, KateLineLayout *layout, int currentViewLine, int xStart, int xEnd) 0182 { 0183 if (isPrinterFriendly()) { 0184 return; 0185 } 0186 0187 // Normal background color 0188 QColor backgroundColor(config()->backgroundColor()); 0189 0190 // paint the current line background if we're on the current line 0191 QColor currentLineColor = config()->highlightedLineColor(); 0192 0193 // Check for mark background 0194 int markRed = 0; 0195 int markGreen = 0; 0196 int markBlue = 0; 0197 int markCount = 0; 0198 0199 // Retrieve marks for this line 0200 uint mrk = m_doc->mark(layout->line()); 0201 if (mrk) { 0202 for (uint bit = 0; bit < 32; bit++) { 0203 KTextEditor::Document::MarkTypes markType = (KTextEditor::Document::MarkTypes)(1U << bit); 0204 if (mrk & markType) { 0205 QColor markColor = config()->lineMarkerColor(markType); 0206 0207 if (markColor.isValid()) { 0208 markCount++; 0209 markRed += markColor.red(); 0210 markGreen += markColor.green(); 0211 markBlue += markColor.blue(); 0212 } 0213 } 0214 } // for 0215 } // Marks 0216 0217 if (markCount) { 0218 markRed /= markCount; 0219 markGreen /= markCount; 0220 markBlue /= markCount; 0221 backgroundColor.setRgb(int((backgroundColor.red() * 0.9) + (markRed * 0.1)), 0222 int((backgroundColor.green() * 0.9) + (markGreen * 0.1)), 0223 int((backgroundColor.blue() * 0.9) + (markBlue * 0.1)), 0224 backgroundColor.alpha()); 0225 } 0226 0227 // Draw line background 0228 paint.fillRect(0, 0, xEnd - xStart, lineHeight() * layout->viewLineCount(), backgroundColor); 0229 0230 // paint the current line background if we're on the current line 0231 const bool currentLineHasSelection = m_view && m_view->selection() && m_view->selectionRange().overlapsLine(layout->line()); 0232 if (currentViewLine != -1 && !currentLineHasSelection) { 0233 if (markCount) { 0234 markRed /= markCount; 0235 markGreen /= markCount; 0236 markBlue /= markCount; 0237 currentLineColor.setRgb(int((currentLineColor.red() * 0.9) + (markRed * 0.1)), 0238 int((currentLineColor.green() * 0.9) + (markGreen * 0.1)), 0239 int((currentLineColor.blue() * 0.9) + (markBlue * 0.1)), 0240 currentLineColor.alpha()); 0241 } 0242 0243 paint.fillRect(0, lineHeight() * currentViewLine, xEnd - xStart, lineHeight(), currentLineColor); 0244 } 0245 } 0246 0247 void KateRenderer::paintTabstop(QPainter &paint, qreal x, qreal y) const 0248 { 0249 QPen penBackup(paint.pen()); 0250 QPen pen(config()->tabMarkerColor()); 0251 pen.setWidthF(qMax(1.0, spaceWidth() / 10.0)); 0252 paint.setPen(pen); 0253 0254 int dist = spaceWidth() * 0.3; 0255 QPoint points[8]; 0256 points[0] = QPoint(x - dist, y - dist); 0257 points[1] = QPoint(x, y); 0258 points[2] = QPoint(x, y); 0259 points[3] = QPoint(x - dist, y + dist); 0260 x += spaceWidth() / 3.0; 0261 points[4] = QPoint(x - dist, y - dist); 0262 points[5] = QPoint(x, y); 0263 points[6] = QPoint(x, y); 0264 points[7] = QPoint(x - dist, y + dist); 0265 paint.drawLines(points, 4); 0266 paint.setPen(penBackup); 0267 } 0268 0269 void KateRenderer::paintSpaces(QPainter &paint, const QPointF *points, const int count) const 0270 { 0271 if (count == 0) { 0272 return; 0273 } 0274 QPen penBackup(paint.pen()); 0275 QPen pen(config()->tabMarkerColor()); 0276 0277 pen.setWidthF(m_markerSize); 0278 pen.setCapStyle(Qt::RoundCap); 0279 paint.setPen(pen); 0280 paint.setRenderHint(QPainter::Antialiasing, true); 0281 paint.drawPoints(points, count); 0282 paint.setPen(penBackup); 0283 paint.setRenderHint(QPainter::Antialiasing, false); 0284 } 0285 0286 void KateRenderer::paintNonBreakSpace(QPainter &paint, qreal x, qreal y) const 0287 { 0288 QPen penBackup(paint.pen()); 0289 QPen pen(config()->tabMarkerColor()); 0290 pen.setWidthF(qMax(1.0, spaceWidth() / 10.0)); 0291 paint.setPen(pen); 0292 0293 const int height = fontHeight(); 0294 const int width = spaceWidth(); 0295 0296 QPoint points[6]; 0297 points[0] = QPoint(x + width / 10, y + height / 4); 0298 points[1] = QPoint(x + width / 10, y + height / 3); 0299 points[2] = QPoint(x + width / 10, y + height / 3); 0300 points[3] = QPoint(x + width - width / 10, y + height / 3); 0301 points[4] = QPoint(x + width - width / 10, y + height / 3); 0302 points[5] = QPoint(x + width - width / 10, y + height / 4); 0303 paint.drawLines(points, 3); 0304 paint.setPen(penBackup); 0305 } 0306 0307 void KateRenderer::paintNonPrintableSpaces(QPainter &paint, qreal x, qreal y, const QChar &chr) 0308 { 0309 paint.save(); 0310 QPen pen(config()->spellingMistakeLineColor()); 0311 pen.setWidthF(qMax(1.0, spaceWidth() * 0.1)); 0312 paint.setPen(pen); 0313 0314 const int height = fontHeight(); 0315 const int width = m_fontMetrics.boundingRect(chr).width(); 0316 const int offset = spaceWidth() * 0.1; 0317 0318 QPoint points[8]; 0319 points[0] = QPoint(x - offset, y + offset); 0320 points[1] = QPoint(x + width + offset, y + offset); 0321 points[2] = QPoint(x + width + offset, y + offset); 0322 points[3] = QPoint(x + width + offset, y - height - offset); 0323 points[4] = QPoint(x + width + offset, y - height - offset); 0324 points[5] = QPoint(x - offset, y - height - offset); 0325 points[6] = QPoint(x - offset, y - height - offset); 0326 points[7] = QPoint(x - offset, y + offset); 0327 paint.drawLines(points, 4); 0328 paint.restore(); 0329 } 0330 0331 /** 0332 * Helper function that checks if our cursor is at a bracket 0333 * and calculates X position for opening/closing brackets. We 0334 * then use this data to color the indentation line differently. 0335 * @p view is current view 0336 * @p range is the current range from @ref paintTextLine 0337 * @p spaceWidth width of space char 0338 * @p c is the position of cursor 0339 * @p bracketXPos will be X position of close bracket or -1 if not found 0340 */ 0341 static KTextEditor::Range cursorAtBracket(KTextEditor::ViewPrivate *view, const KateLineLayout *range, int spaceWidth, KTextEditor::Cursor c, int &bracketXPos) 0342 { 0343 bracketXPos = -1; 0344 if (range->line() != c.line()) { 0345 return KTextEditor::Range::invalid(); 0346 } 0347 0348 auto *doc = view->doc(); 0349 // Avoid work if we are below tabwidth 0350 if (c.column() < doc->config()->tabWidth()) { 0351 return KTextEditor::Range::invalid(); 0352 } 0353 0354 // We match these brackets only 0355 static constexpr QChar brackets[] = {QLatin1Char('{'), QLatin1Char('}'), QLatin1Char('('), QLatin1Char(')')}; 0356 // look for character in front 0357 QChar right = doc->characterAt(c); 0358 auto it = std::find(std::begin(brackets), std::end(brackets), right); 0359 0360 KTextEditor::Range ret = KTextEditor::Range::invalid(); 0361 bool found = false; 0362 if (it != std::end(brackets)) { 0363 found = true; 0364 } else { 0365 // look at previous character 0366 QChar left = doc->characterAt({c.line(), c.column() - 1}); 0367 it = std::find(std::begin(brackets), std::end(brackets), left); 0368 if (it != std::end(brackets)) { 0369 found = true; 0370 } 0371 } 0372 0373 // We have a bracket 0374 if (found) { 0375 ret = doc->findMatchingBracket(c, 500); 0376 if (!ret.isValid()) { 0377 return ret; 0378 } 0379 bracketXPos = (ret.end().column() * spaceWidth) + 1; 0380 } 0381 0382 return ret; 0383 } 0384 0385 void KateRenderer::paintIndentMarker(QPainter &paint, uint x, int line) 0386 { 0387 const QPen penBackup(paint.pen()); 0388 static const QList<qreal> dashPattern = QList<qreal>() << 1 << 1; 0389 QPen myPen; 0390 0391 const bool onBracket = m_currentBracketX == (int)x; 0392 if (onBracket && m_currentBracketRange.containsLine(line)) { 0393 QColor c = view()->theme().textColor(KSyntaxHighlighting::Theme::Normal); 0394 c.setAlphaF(0.7); 0395 myPen.setColor(c); 0396 } else { 0397 myPen.setColor(config()->indentationLineColor()); 0398 myPen.setDashPattern(dashPattern); 0399 } 0400 0401 paint.setPen(myPen); 0402 0403 QPainter::RenderHints renderHints = paint.renderHints(); 0404 paint.setRenderHints(renderHints, false); 0405 0406 paint.drawLine(x + 2, 0, x + 2, lineHeight()); 0407 0408 paint.setRenderHints(renderHints, true); 0409 0410 paint.setPen(penBackup); 0411 } 0412 0413 static bool rangeLessThanForRenderer(const Kate::TextRange *a, const Kate::TextRange *b) 0414 { 0415 // compare Z-Depth first 0416 // smaller Z-Depths should win! 0417 if (a->zDepth() > b->zDepth()) { 0418 return true; 0419 } else if (a->zDepth() < b->zDepth()) { 0420 return false; 0421 } 0422 0423 // end of a > end of b? 0424 if (a->end().toCursor() > b->end().toCursor()) { 0425 return true; 0426 } 0427 0428 // if ends are equal, start of a < start of b? 0429 if (a->end().toCursor() == b->end().toCursor()) { 0430 return a->start().toCursor() < b->start().toCursor(); 0431 } 0432 0433 return false; 0434 } 0435 0436 QList<QTextLayout::FormatRange> KateRenderer::decorationsForLine(const Kate::TextLine &textLine, int line, bool selectionsOnly) const 0437 { 0438 // limit number of attributes we can highlight in reasonable time 0439 const int limitOfRanges = 1024; 0440 auto rangesWithAttributes = m_doc->buffer().rangesForLine(line, m_printerFriendly ? nullptr : m_view, true); 0441 if (rangesWithAttributes.size() > limitOfRanges) { 0442 rangesWithAttributes.clear(); 0443 } 0444 0445 // Don't compute the highlighting if there isn't going to be any highlighting 0446 const auto &al = textLine.attributesList(); 0447 if (!(selectionsOnly || !al.empty() || !rangesWithAttributes.empty())) { 0448 return QList<QTextLayout::FormatRange>(); 0449 } 0450 0451 // Add the inbuilt highlighting to the list, limit with limitOfRanges 0452 RenderRangeVector renderRanges; 0453 if (!al.empty()) { 0454 auto ¤tRange = renderRanges.pushNewRange(); 0455 for (int i = 0; i < std::min<int>(al.size(), limitOfRanges); ++i) { 0456 if (al[i].length > 0 && al[i].attributeValue > 0) { 0457 currentRange.addRange(KTextEditor::Range(KTextEditor::Cursor(line, al[i].offset), al[i].length), specificAttribute(al[i].attributeValue)); 0458 } 0459 } 0460 } 0461 0462 // check for dynamic hl stuff 0463 const QSet<Kate::TextRange *> *rangesMouseIn = m_view ? m_view->rangesMouseIn() : nullptr; 0464 const QSet<Kate::TextRange *> *rangesCaretIn = m_view ? m_view->rangesCaretIn() : nullptr; 0465 bool anyDynamicHlsActive = m_view && (!rangesMouseIn->empty() || !rangesCaretIn->empty()); 0466 0467 // sort all ranges, we want that the most specific ranges win during rendering, multiple equal ranges are kind of random, still better than old smart 0468 // rangs behavior ;) 0469 std::sort(rangesWithAttributes.begin(), rangesWithAttributes.end(), rangeLessThanForRenderer); 0470 0471 renderRanges.reserve(rangesWithAttributes.size()); 0472 // loop over all ranges 0473 for (int i = 0; i < rangesWithAttributes.size(); ++i) { 0474 // real range 0475 Kate::TextRange *kateRange = rangesWithAttributes[i]; 0476 0477 // calculate attribute, default: normal attribute 0478 KTextEditor::Attribute::Ptr attribute = kateRange->attribute(); 0479 if (anyDynamicHlsActive) { 0480 // check mouse in 0481 if (KTextEditor::Attribute::Ptr attributeMouseIn = attribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)) { 0482 if (rangesMouseIn->contains(kateRange)) { 0483 attribute = attributeMouseIn; 0484 } 0485 } 0486 0487 // check caret in 0488 if (KTextEditor::Attribute::Ptr attributeCaretIn = attribute->dynamicAttribute(KTextEditor::Attribute::ActivateCaretIn)) { 0489 if (rangesCaretIn->contains(kateRange)) { 0490 attribute = attributeCaretIn; 0491 } 0492 } 0493 } 0494 0495 // span range 0496 renderRanges.pushNewRange().addRange(*kateRange, std::move(attribute)); 0497 } 0498 0499 // Add selection highlighting if we're creating the selection decorations 0500 if ((m_view && selectionsOnly && showSelections() && m_view->selection()) || (m_view && m_view->blockSelection())) { 0501 auto ¤tRange = renderRanges.pushNewRange(); 0502 0503 // Set up the selection background attribute TODO: move this elsewhere, eg. into the config? 0504 static KTextEditor::Attribute::Ptr backgroundAttribute; 0505 if (!backgroundAttribute) { 0506 backgroundAttribute = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute()); 0507 } 0508 0509 if (!hasCustomLineHeight()) { 0510 backgroundAttribute->setBackground(config()->selectionColor()); 0511 } 0512 backgroundAttribute->setForeground(attribute(KSyntaxHighlighting::Theme::TextStyle::Normal)->selectedForeground().color()); 0513 0514 // Create a range for the current selection 0515 if (m_view->blockSelection() && m_view->selectionRange().overlapsLine(line)) { 0516 currentRange.addRange(m_doc->rangeOnLine(m_view->selectionRange(), line), backgroundAttribute); 0517 } else { 0518 currentRange.addRange(m_view->selectionRange(), backgroundAttribute); 0519 } 0520 } 0521 0522 // no render ranges, nothing to do, else we loop below endless! 0523 if (renderRanges.isEmpty()) { 0524 return QList<QTextLayout::FormatRange>(); 0525 } 0526 0527 // Calculate the range which we need to iterate in order to get the highlighting for just this line 0528 KTextEditor::Cursor currentPosition; 0529 KTextEditor::Cursor endPosition; 0530 if (m_view && selectionsOnly) { 0531 if (m_view->blockSelection()) { 0532 KTextEditor::Range subRange = m_doc->rangeOnLine(m_view->selectionRange(), line); 0533 currentPosition = subRange.start(); 0534 endPosition = subRange.end(); 0535 } else { 0536 KTextEditor::Range rangeNeeded = m_view->selectionRange() & KTextEditor::Range(line, 0, line + 1, 0); 0537 0538 currentPosition = qMax(KTextEditor::Cursor(line, 0), rangeNeeded.start()); 0539 endPosition = qMin(KTextEditor::Cursor(line + 1, 0), rangeNeeded.end()); 0540 } 0541 } else { 0542 currentPosition = KTextEditor::Cursor(line, 0); 0543 endPosition = KTextEditor::Cursor(line + 1, 0); 0544 } 0545 0546 // Background formats have lower priority so they get overriden by selection 0547 const bool shoudlClearBackground = m_view && hasCustomLineHeight() && m_view->selection(); 0548 const KTextEditor::Range selectionRange = shoudlClearBackground ? m_view->selectionRange() : KTextEditor::Range::invalid(); 0549 0550 // Main iterative loop. This walks through each set of highlighting ranges, and stops each 0551 // time the highlighting changes. It then creates the corresponding QTextLayout::FormatRanges. 0552 QList<QTextLayout::FormatRange> newHighlight; 0553 while (currentPosition < endPosition) { 0554 renderRanges.advanceTo(currentPosition); 0555 0556 if (!renderRanges.hasAttribute()) { 0557 // No attribute, don't need to create a FormatRange for this text range 0558 currentPosition = renderRanges.nextBoundary(); 0559 continue; 0560 } 0561 0562 KTextEditor::Cursor nextPosition = renderRanges.nextBoundary(); 0563 0564 // Create the format range and populate with the correct start, length and format info 0565 QTextLayout::FormatRange fr; 0566 fr.start = currentPosition.column(); 0567 0568 if (nextPosition < endPosition || endPosition.line() <= line) { 0569 fr.length = nextPosition.column() - currentPosition.column(); 0570 0571 } else { 0572 // before we did here +1 to force background drawing at the end of the line when it's warranted 0573 // we now skip this, we don't draw e.g. full line backgrounds 0574 fr.length = textLine.length() - currentPosition.column(); 0575 } 0576 0577 KTextEditor::Attribute::Ptr a = renderRanges.generateAttribute(); 0578 if (a) { 0579 fr.format = *a; 0580 0581 if (selectionsOnly) { 0582 assignSelectionBrushesFromAttribute(fr, *a); 0583 } 0584 } 0585 0586 // Clear background if this position overlaps selection 0587 if (shoudlClearBackground && selectionRange.contains(currentPosition) && fr.format.hasProperty(QTextFormat::BackgroundBrush)) { 0588 fr.format.clearBackground(); 0589 } 0590 0591 newHighlight.append(fr); 0592 0593 currentPosition = nextPosition; 0594 } 0595 0596 // ensure bold & italic fonts work, even if the main font is e.g. light or something like that 0597 for (auto &formatRange : newHighlight) { 0598 if (formatRange.format.fontWeight() == QFont::Bold || formatRange.format.fontItalic()) { 0599 formatRange.format.setFontStyleName(QString()); 0600 } 0601 } 0602 0603 return newHighlight; 0604 } 0605 0606 void KateRenderer::assignSelectionBrushesFromAttribute(QTextLayout::FormatRange &target, const KTextEditor::Attribute &attribute) 0607 { 0608 if (attribute.hasProperty(SelectedForeground)) { 0609 target.format.setForeground(attribute.selectedForeground()); 0610 } 0611 if (attribute.hasProperty(SelectedBackground)) { 0612 target.format.setBackground(attribute.selectedBackground()); 0613 } 0614 } 0615 0616 void KateRenderer::paintTextBackground(QPainter &paint, KateLineLayout *layout, const QList<QTextLayout::FormatRange> &selRanges, const QBrush &brush) const 0617 { 0618 const bool rtl = layout->isRightToLeft(); 0619 0620 for (const auto &sel : selRanges) { 0621 const int s = sel.start; 0622 const int e = sel.start + sel.length; 0623 QBrush br; 0624 0625 // Prefer using the brush supplied by user 0626 if (brush != Qt::NoBrush) { 0627 br = brush; 0628 } else if (sel.format.background() != Qt::NoBrush) { 0629 // Otherwise use the brush in format 0630 br = sel.format.background(); 0631 } else { 0632 // skip if no brush to fill with 0633 continue; 0634 } 0635 0636 const int startViewLine = layout->viewLineForColumn(s); 0637 const int endViewLine = layout->viewLineForColumn(e); 0638 if (startViewLine == endViewLine) { 0639 KateTextLayout l = layout->viewLine(startViewLine); 0640 const int startX = cursorToX(l, s); 0641 const int endX = cursorToX(l, e); 0642 const int y = startViewLine * lineHeight(); 0643 QRect r(startX, y, (endX - startX), lineHeight()); 0644 paint.fillRect(r, br); 0645 } else { 0646 QPainterPath p; 0647 for (int l = startViewLine; l <= endViewLine; ++l) { 0648 auto kateLayout = layout->viewLine(l); 0649 int sx = 0; 0650 int width = rtl ? kateLayout.lineLayout().width() : kateLayout.lineLayout().naturalTextWidth(); 0651 0652 if (l == startViewLine) { 0653 if (rtl) { 0654 // For rtl, Rect starts at 0 and ends at selection start 0655 sx = 0; 0656 width = kateLayout.lineLayout().cursorToX(s); 0657 } else { 0658 sx = kateLayout.lineLayout().cursorToX(s); 0659 } 0660 } else if (l == endViewLine) { 0661 if (rtl) { 0662 // Drawing will start at selection end, and end at the view border 0663 sx = kateLayout.lineLayout().cursorToX(e); 0664 } else { 0665 width = kateLayout.lineLayout().cursorToX(e); 0666 } 0667 } 0668 0669 const int y = l * lineHeight(); 0670 QRect r(sx, y, width - sx, lineHeight()); 0671 p.addRect(r); 0672 } 0673 paint.fillPath(p, br); 0674 } 0675 } 0676 } 0677 0678 void KateRenderer::paintTextLine(QPainter &paint, 0679 KateLineLayout *range, 0680 int xStart, 0681 int xEnd, 0682 const QRectF &textClipRect, 0683 const KTextEditor::Cursor *cursor, 0684 PaintTextLineFlags flags) 0685 { 0686 Q_ASSERT(range->isValid()); 0687 0688 // qCDebug(LOG_KTE)<<"KateRenderer::paintTextLine"; 0689 0690 // font data 0691 const QFontMetricsF &fm = m_fontMetrics; 0692 0693 int currentViewLine = -1; 0694 if (cursor && cursor->line() == range->line()) { 0695 currentViewLine = range->viewLineForColumn(cursor->column()); 0696 } 0697 0698 paintTextLineBackground(paint, range, currentViewLine, xStart, xEnd); 0699 0700 // Draws the dashed underline at the start of a folded block of text. 0701 if (!(flags & SkipDrawFirstInvisibleLineUnderlined) && range->startsInvisibleBlock()) { 0702 QPen pen(config()->foldingColor()); 0703 pen.setCosmetic(true); 0704 pen.setStyle(Qt::DashLine); 0705 pen.setDashOffset(xStart); 0706 pen.setWidth(2); 0707 paint.setPen(pen); 0708 paint.drawLine(0, (lineHeight() * range->viewLineCount()) - 2, xEnd - xStart, (lineHeight() * range->viewLineCount()) - 2); 0709 } 0710 0711 if (range->layout()) { 0712 bool drawSelection = 0713 m_view && m_view->selection() && showSelections() && m_view->selectionRange().overlapsLine(range->line()) && !flags.testFlag(SkipDrawLineSelection); 0714 // Draw selection in block selection mode. We need 2 kinds of selections that QTextLayout::draw can't render: 0715 // - past-end-of-line selection and 0716 // - 0-column-wide selection (used to indicate where text will be typed) 0717 if (drawSelection && m_view->blockSelection()) { 0718 int selectionStartColumn = m_doc->fromVirtualColumn(range->line(), m_doc->toVirtualColumn(m_view->selectionRange().start())); 0719 int selectionEndColumn = m_doc->fromVirtualColumn(range->line(), m_doc->toVirtualColumn(m_view->selectionRange().end())); 0720 QBrush selectionBrush = config()->selectionColor(); 0721 if (selectionStartColumn != selectionEndColumn) { 0722 KateTextLayout lastLine = range->viewLine(range->viewLineCount() - 1); 0723 if (selectionEndColumn > lastLine.startCol()) { 0724 int selectionStartX = (selectionStartColumn > lastLine.startCol()) ? cursorToX(lastLine, selectionStartColumn, true) : 0; 0725 int selectionEndX = cursorToX(lastLine, selectionEndColumn, true); 0726 paint.fillRect(QRect(selectionStartX - xStart, (int)lastLine.lineLayout().y(), selectionEndX - selectionStartX, lineHeight()), 0727 selectionBrush); 0728 } 0729 } else { 0730 const int selectStickWidth = 2; 0731 KateTextLayout selectionLine = range->viewLine(range->viewLineForColumn(selectionStartColumn)); 0732 int selectionX = cursorToX(selectionLine, selectionStartColumn, true); 0733 paint.fillRect(QRect(selectionX - xStart, (int)selectionLine.lineLayout().y(), selectStickWidth, lineHeight()), selectionBrush); 0734 } 0735 } 0736 0737 QList<QTextLayout::FormatRange> additionalFormats; 0738 if (range->length() > 0) { 0739 // We may have changed the pen, be absolutely sure it gets set back to 0740 // normal foreground color before drawing text for text that does not 0741 // set the pen color 0742 paint.setPen(attribute(KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color()); 0743 // Draw the text :) 0744 0745 if (range->layout()->textOption().textDirection() == Qt::RightToLeft) { 0746 // If the text is RTL, we draw text background ourselves 0747 auto decos = decorationsForLine(range->textLine(), range->line(), false); 0748 auto sr = view()->selectionRange(); 0749 auto c = config()->selectionColor(); 0750 int line = range->line(); 0751 // Remove "selection" format from decorations 0752 // "selection" will get painted below 0753 decos.erase(std::remove_if(decos.begin(), 0754 decos.end(), 0755 [sr, c, line](const QTextLayout::FormatRange &fr) { 0756 return sr.overlapsLine(line) && sr.overlapsColumn(fr.start) && fr.format.background().color() == c; 0757 }), 0758 decos.end()); 0759 paintTextBackground(paint, range, decos, Qt::NoBrush); 0760 } 0761 0762 if (drawSelection) { 0763 additionalFormats = decorationsForLine(range->textLine(), range->line(), true); 0764 if (hasCustomLineHeight()) { 0765 paintTextBackground(paint, range, additionalFormats, config()->selectionColor()); 0766 } 0767 range->layout()->draw(&paint, QPoint(-xStart, 0), additionalFormats, textClipRect); 0768 0769 } else { 0770 range->layout()->draw(&paint, QPoint(-xStart, 0), QList<QTextLayout::FormatRange>{}, textClipRect); 0771 } 0772 } 0773 0774 // Check if we are at a bracket and color the indentation 0775 // line differently 0776 const bool indentLinesEnabled = showIndentLines(); 0777 if (cursor && indentLinesEnabled) { 0778 auto cur = *cursor; 0779 cur.setColumn(cur.column() - 1); 0780 if (!m_currentBracketRange.boundaryAtCursor(*cursor) && m_currentBracketRange.end() != cur && m_currentBracketRange.start() != cur) { 0781 m_currentBracketRange = cursorAtBracket(view(), range, spaceWidth(), *cursor, m_currentBracketX); 0782 } 0783 } 0784 0785 // Loop each individual line for additional text decoration etc. 0786 for (int i = 0; i < range->viewLineCount(); ++i) { 0787 KateTextLayout line = range->viewLine(i); 0788 0789 // Draw indent lines 0790 if (!m_printerFriendly && (indentLinesEnabled && i == 0)) { 0791 const qreal w = spaceWidth(); 0792 const int lastIndentColumn = range->textLine().indentDepth(m_tabWidth); 0793 for (int x = m_indentWidth; x < lastIndentColumn; x += m_indentWidth) { 0794 auto xPos = x * w + 1 - xStart; 0795 if (xPos >= 0) { 0796 paintIndentMarker(paint, xPos, range->line()); 0797 } 0798 } 0799 } 0800 0801 // draw an open box to mark non-breaking spaces 0802 const QString &text = range->textLine().text(); 0803 int y = lineHeight() * i + m_fontAscent - fm.strikeOutPos(); 0804 int nbSpaceIndex = text.indexOf(nbSpaceChar, line.lineLayout().xToCursor(xStart)); 0805 0806 while (nbSpaceIndex != -1 && nbSpaceIndex < line.endCol()) { 0807 int x = line.lineLayout().cursorToX(nbSpaceIndex); 0808 if (x > xEnd) { 0809 break; 0810 } 0811 paintNonBreakSpace(paint, x - xStart, y); 0812 nbSpaceIndex = text.indexOf(nbSpaceChar, nbSpaceIndex + 1); 0813 } 0814 0815 // draw tab stop indicators 0816 if (showTabs()) { 0817 int tabIndex = text.indexOf(tabChar, line.lineLayout().xToCursor(xStart)); 0818 while (tabIndex != -1 && tabIndex < line.endCol()) { 0819 int x = line.lineLayout().cursorToX(tabIndex); 0820 if (x > xEnd) { 0821 break; 0822 } 0823 paintTabstop(paint, x - xStart + spaceWidth() / 2.0, y); 0824 tabIndex = text.indexOf(tabChar, tabIndex + 1); 0825 } 0826 } 0827 0828 // draw trailing spaces 0829 if (showSpaces() != KateDocumentConfig::None) { 0830 int spaceIndex = line.endCol() - 1; 0831 const int trailingPos = showSpaces() == KateDocumentConfig::All ? 0 : qMax(range->textLine().lastChar(), 0); 0832 0833 if (spaceIndex >= trailingPos) { 0834 QVarLengthArray<int, 32> spacePositions; 0835 // Adjust to visible contents 0836 const auto dir = range->layout()->textOption().textDirection(); 0837 const bool isRTL = dir == Qt::RightToLeft && m_view->dynWordWrap(); 0838 int start = isRTL ? xEnd : xStart; 0839 int end = isRTL ? xStart : xEnd; 0840 0841 spaceIndex = std::min(line.lineLayout().xToCursor(end), spaceIndex); 0842 int visibleStart = line.lineLayout().xToCursor(start); 0843 0844 for (; spaceIndex >= line.startCol(); --spaceIndex) { 0845 if (!text.at(spaceIndex).isSpace()) { 0846 if (showSpaces() == KateDocumentConfig::Trailing) { 0847 break; 0848 } else { 0849 continue; 0850 } 0851 } 0852 if (text.at(spaceIndex) != QLatin1Char('\t') || !showTabs()) { 0853 spacePositions << spaceIndex; 0854 } 0855 0856 if (spaceIndex < visibleStart) { 0857 break; 0858 } 0859 } 0860 0861 QPointF prev; 0862 QVarLengthArray<QPointF, 32> spacePoints; 0863 const auto spaceWidth = this->spaceWidth(); 0864 // reverse because we want to look at the spaces at the beginning of line first 0865 for (auto rit = spacePositions.rbegin(); rit != spacePositions.rend(); ++rit) { 0866 const int spaceIdx = *rit; 0867 qreal x = line.lineLayout().cursorToX(spaceIdx) - xStart; 0868 int dir = 1; // 1 == ltr, -1 == rtl 0869 if (range->layout()->textOption().alignment() == Qt::AlignRight) { 0870 dir = -1; 0871 if (spaceIdx > 0) { 0872 QChar c = text.at(spaceIdx - 1); 0873 // line is LTR aligned, but is the char ltr or rtl? 0874 if (!isLineRightToLeft(QStringView(&c, 1))) { 0875 dir = 1; 0876 } 0877 } 0878 } else { 0879 if (spaceIdx > 0) { 0880 // line is LTR aligned, but is the char ltr or rtl? 0881 QChar c = text.at(spaceIdx - 1); 0882 if (isLineRightToLeft(QStringView(&c, 1))) { 0883 dir = -1; 0884 } 0885 } 0886 } 0887 0888 x += dir * (spaceWidth / 2.0); 0889 0890 const QPointF currentPoint(x, y); 0891 if (!prev.isNull() && currentPoint == prev) { 0892 break; 0893 } 0894 spacePoints << currentPoint; 0895 prev = QPointF(x, y); 0896 } 0897 if (!spacePoints.isEmpty()) { 0898 paintSpaces(paint, spacePoints.constData(), spacePoints.size()); 0899 } 0900 } 0901 } 0902 0903 if (showNonPrintableSpaces()) { 0904 const int y = lineHeight() * i + m_fontAscent; 0905 0906 static const QRegularExpression nonPrintableSpacesRegExp( 0907 QStringLiteral("[\\x{2000}-\\x{200F}\\x{2028}-\\x{202F}\\x{205F}-\\x{2064}\\x{206A}-\\x{206F}]")); 0908 QRegularExpressionMatchIterator i = nonPrintableSpacesRegExp.globalMatch(text, line.lineLayout().xToCursor(xStart)); 0909 0910 while (i.hasNext()) { 0911 const int charIndex = i.next().capturedStart(); 0912 0913 const int x = line.lineLayout().cursorToX(charIndex); 0914 if (x > xEnd) { 0915 break; 0916 } 0917 0918 paintNonPrintableSpaces(paint, x - xStart, y, text[charIndex]); 0919 } 0920 } 0921 0922 // draw word-wrap-honor-indent filling 0923 if ((i > 0) && range->shiftX && (range->shiftX > xStart)) { 0924 // fill background first with selection if we had selection from the previous line 0925 if (drawSelection && !m_view->blockSelection() && m_view->selectionRange().start() < line.start() 0926 && m_view->selectionRange().end() >= line.start()) { 0927 paint.fillRect(0, lineHeight() * i, range->shiftX - xStart, lineHeight(), QBrush(config()->selectionColor())); 0928 } 0929 0930 // paint the normal filling for the word wrap markers 0931 paint.fillRect(0, lineHeight() * i, range->shiftX - xStart, lineHeight(), QBrush(config()->wordWrapMarkerColor(), Qt::Dense4Pattern)); 0932 } 0933 } 0934 0935 // Draw carets 0936 if (m_view && cursor && drawCaret()) { 0937 const auto &secCursors = view()->secondaryCursors(); 0938 // Find carets on this line 0939 auto mIt = std::lower_bound(secCursors.begin(), secCursors.end(), range->line(), [](const KTextEditor::ViewPrivate::SecondaryCursor &l, int line) { 0940 return l.pos->line() < line; 0941 }); 0942 if (mIt != secCursors.end() && mIt->cursor().line() == range->line()) { 0943 for (; mIt != secCursors.end(); ++mIt) { 0944 auto cursor = mIt->cursor(); 0945 if (cursor.line() == range->line()) { 0946 paintCaret(cursor, range, paint, xStart, xEnd); 0947 } else { 0948 break; 0949 } 0950 } 0951 } 0952 paintCaret(*cursor, range, paint, xStart, xEnd); 0953 } 0954 } 0955 0956 // show word wrap marker if desirable 0957 if ((!isPrinterFriendly()) && config()->wordWrapMarker()) { 0958 const QPainter::RenderHints backupRenderHints = paint.renderHints(); 0959 paint.setPen(config()->wordWrapMarkerColor()); 0960 int _x = qreal(m_doc->config()->wordWrapAt()) * fm.horizontalAdvance(QLatin1Char('x')) - xStart; 0961 paint.drawLine(_x, 0, _x, lineHeight()); 0962 paint.setRenderHints(backupRenderHints); 0963 } 0964 0965 // Draw inline notes 0966 if (!isPrinterFriendly()) { 0967 const auto inlineNotes = m_view->inlineNotes(range->line()); 0968 for (const auto &inlineNoteData : inlineNotes) { 0969 KTextEditor::InlineNote inlineNote(inlineNoteData); 0970 const int column = inlineNote.position().column(); 0971 const int viewLine = range->viewLineForColumn(column); 0972 // We only consider a line "rtl" if dynamic wrap is enabled. If it is disabled, our 0973 // text is always on the left side of the view 0974 const auto dir = range->layout()->textOption().textDirection(); 0975 const bool isRTL = dir == Qt::RightToLeft && m_view->dynWordWrap(); 0976 0977 // Determine the position where to paint the note. 0978 // We start by getting the x coordinate of cursor placed to the column. 0979 // If the text is ltr or rtl + dyn wrap, get the X from column 0980 qreal x; 0981 if (dir == Qt::LeftToRight || (dir == Qt::RightToLeft && m_view->dynWordWrap())) { 0982 x = range->viewLine(viewLine).lineLayout().cursorToX(column) - xStart; 0983 } else /* rtl + dynWordWrap == false */ { 0984 // if text is rtl and dynamic wrap is false, the x offsets are in the opposite 0985 // direction i.e., [0] == biggest offset, [1] = next 0986 x = range->viewLine(viewLine).lineLayout().cursorToX(range->length() - column) - xStart; 0987 } 0988 int textLength = range->length(); 0989 if (column == 0 || column < textLength) { 0990 // If the note is inside text or at the beginning, then there is a hole in the text where the 0991 // note should be painted and the cursor gets placed at the right side of it. So we have to 0992 // subtract the width of the note to get to left side of the hole. 0993 x -= inlineNote.width(); 0994 } else { 0995 // If the note is outside the text, then the X coordinate is located at the end of the line. 0996 // Add appropriate amount of spaces to reach the required column. 0997 const auto spaceToAdd = spaceWidth() * (column - textLength); 0998 x += isRTL ? -spaceToAdd : spaceToAdd; 0999 } 1000 1001 qreal y = lineHeight() * viewLine; 1002 1003 // Paint the note 1004 paint.save(); 1005 paint.translate(x, y); 1006 inlineNote.provider()->paintInlineNote(inlineNote, paint, isRTL ? Qt::RightToLeft : Qt::LeftToRight); 1007 paint.restore(); 1008 } 1009 } 1010 } 1011 1012 static void drawCursor(const QTextLayout &layout, QPainter *p, const QPointF &pos, int cursorPosition, int width, const int height) 1013 { 1014 cursorPosition = qBound(0, cursorPosition, layout.text().length()); 1015 const QTextLine l = layout.lineForTextPosition(cursorPosition); 1016 Q_ASSERT(l.isValid()); 1017 if (!l.isValid()) { 1018 return; 1019 } 1020 const QPainter::CompositionMode origCompositionMode = p->compositionMode(); 1021 if (p->paintEngine()->hasFeature(QPaintEngine::RasterOpModes)) { 1022 p->setCompositionMode(QPainter::RasterOp_NotDestination); 1023 } 1024 1025 const QPointF position = pos + layout.position(); 1026 const qreal x = position.x() + l.cursorToX(cursorPosition); 1027 const qreal y = l.lineNumber() * height; 1028 p->fillRect(QRectF(x, y, (qreal)width, (qreal)height), p->pen().brush()); 1029 p->setCompositionMode(origCompositionMode); 1030 } 1031 1032 void KateRenderer::paintCaret(KTextEditor::Cursor cursor, KateLineLayout *range, QPainter &paint, int xStart, int xEnd) 1033 { 1034 if (range->includesCursor(cursor)) { 1035 int caretWidth; 1036 int lineWidth = 2; 1037 QColor color; 1038 QTextLine line = range->layout()->lineForTextPosition(qMin(cursor.column(), range->length())); 1039 1040 // Determine the caret's style 1041 KTextEditor::caretStyles style = caretStyle(); 1042 1043 // Make the caret the desired width 1044 if (style == KTextEditor::caretStyles::Line) { 1045 caretWidth = lineWidth; 1046 } else if (line.isValid() && cursor.column() < range->length()) { 1047 caretWidth = int(line.cursorToX(cursor.column() + 1) - line.cursorToX(cursor.column())); 1048 if (caretWidth < 0) { 1049 caretWidth = -caretWidth; 1050 } 1051 } else { 1052 caretWidth = spaceWidth(); 1053 } 1054 1055 // Determine the color 1056 if (m_caretOverrideColor.isValid()) { 1057 // Could actually use the real highlighting system for this... 1058 // would be slower, but more accurate for corner cases 1059 color = m_caretOverrideColor; 1060 } else { 1061 // search for the FormatRange that includes the cursor 1062 const auto formatRanges = range->layout()->formats(); 1063 for (const QTextLayout::FormatRange &r : formatRanges) { 1064 if ((r.start <= cursor.column()) && ((r.start + r.length) > cursor.column())) { 1065 // check for Qt::NoBrush, as the returned color is black() and no invalid QColor 1066 QBrush foregroundBrush = r.format.foreground(); 1067 if (foregroundBrush != Qt::NoBrush) { 1068 color = r.format.foreground().color(); 1069 } 1070 break; 1071 } 1072 } 1073 // still no color found, fall back to default style 1074 if (!color.isValid()) { 1075 color = attribute(KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color(); 1076 } 1077 } 1078 1079 paint.save(); 1080 switch (style) { 1081 case KTextEditor::caretStyles::Line: 1082 paint.setPen(QPen(color, caretWidth)); 1083 break; 1084 case KTextEditor::caretStyles::Block: 1085 // use a gray caret so it's possible to see the character 1086 color.setAlpha(128); 1087 paint.setPen(QPen(color, caretWidth)); 1088 break; 1089 case KTextEditor::caretStyles::Underline: 1090 break; 1091 case KTextEditor::caretStyles::Half: 1092 color.setAlpha(128); 1093 paint.setPen(QPen(color, caretWidth)); 1094 break; 1095 } 1096 1097 if (cursor.column() <= range->length()) { 1098 // Ensure correct cursor placement for RTL text 1099 if (range->layout()->textOption().textDirection() == Qt::RightToLeft) { 1100 xStart += caretWidth; 1101 } 1102 qreal width = 0; 1103 const auto inlineNotes = m_view->inlineNotes(range->line()); 1104 for (const auto &inlineNoteData : inlineNotes) { 1105 KTextEditor::InlineNote inlineNote(inlineNoteData); 1106 if (inlineNote.position().column() == cursor.column()) { 1107 width = inlineNote.width() + (caretStyle() == KTextEditor::caretStyles::Line ? 2.0 : 0.0); 1108 } 1109 } 1110 drawCursor(*range->layout(), &paint, QPoint(-xStart - width, 0), cursor.column(), caretWidth, lineHeight()); 1111 } else { 1112 // Off the end of the line... must be block mode. Draw the caret ourselves. 1113 const KateTextLayout &lastLine = range->viewLine(range->viewLineCount() - 1); 1114 int x = cursorToX(lastLine, KTextEditor::Cursor(range->line(), cursor.column()), true); 1115 if ((x >= xStart) && (x <= xEnd)) { 1116 paint.fillRect(x - xStart, (int)lastLine.lineLayout().y(), caretWidth, lineHeight(), color); 1117 } 1118 } 1119 1120 paint.restore(); 1121 } 1122 } 1123 1124 uint KateRenderer::fontHeight() const 1125 { 1126 return m_fontHeight; 1127 } 1128 1129 uint KateRenderer::documentHeight() const 1130 { 1131 return m_doc->lines() * lineHeight(); 1132 } 1133 1134 int KateRenderer::lineHeight() const 1135 { 1136 return fontHeight(); 1137 } 1138 1139 bool KateRenderer::getSelectionBounds(int line, int lineLength, int &start, int &end) const 1140 { 1141 bool hasSel = false; 1142 1143 if (m_view->selection() && !m_view->blockSelection()) { 1144 if (m_view->lineIsSelection(line)) { 1145 start = m_view->selectionRange().start().column(); 1146 end = m_view->selectionRange().end().column(); 1147 hasSel = true; 1148 } else if (line == m_view->selectionRange().start().line()) { 1149 start = m_view->selectionRange().start().column(); 1150 end = lineLength; 1151 hasSel = true; 1152 } else if (m_view->selectionRange().containsLine(line)) { 1153 start = 0; 1154 end = lineLength; 1155 hasSel = true; 1156 } else if (line == m_view->selectionRange().end().line()) { 1157 start = 0; 1158 end = m_view->selectionRange().end().column(); 1159 hasSel = true; 1160 } 1161 } else if (m_view->lineHasSelected(line)) { 1162 start = m_view->selectionRange().start().column(); 1163 end = m_view->selectionRange().end().column(); 1164 hasSel = true; 1165 } 1166 1167 if (start > end) { 1168 int temp = end; 1169 end = start; 1170 start = temp; 1171 } 1172 1173 return hasSel; 1174 } 1175 1176 void KateRenderer::updateConfig() 1177 { 1178 // update the attribute list pointer 1179 updateAttributes(); 1180 1181 // update font height, do this before we update the view! 1182 updateFontHeight(); 1183 1184 // trigger view update, if any! 1185 if (m_view) { 1186 m_view->updateRendererConfig(); 1187 } 1188 } 1189 1190 bool KateRenderer::hasCustomLineHeight() const 1191 { 1192 return !qFuzzyCompare(config()->lineHeightMultiplier(), 1.0); 1193 } 1194 1195 void KateRenderer::updateFontHeight() 1196 { 1197 // cache font + metrics 1198 m_font = config()->baseFont(); 1199 m_fontMetrics = QFontMetricsF(m_font); 1200 1201 // ensure minimal height of one pixel to not fall in the div by 0 trap somewhere 1202 // 1203 // use a line spacing that matches the code in qt to layout/paint text 1204 // 1205 // see bug 403868 1206 // https://github.com/qt/qtbase/blob/5.12/src/gui/text/qtextlayout.cpp (line 2270 at the moment) where the text height is set as: 1207 // 1208 // qreal height = maxY + fontHeight - minY; 1209 // 1210 // with fontHeight: 1211 // 1212 // qreal fontHeight = font.ascent() + font.descent(); 1213 m_fontHeight = qMax(1, qCeil(m_fontMetrics.ascent() + m_fontMetrics.descent())); 1214 m_fontAscent = m_fontMetrics.ascent(); 1215 1216 if (hasCustomLineHeight()) { 1217 const auto oldFontHeight = m_fontHeight; 1218 const qreal newFontHeight = qreal(m_fontHeight) * config()->lineHeightMultiplier(); 1219 m_fontHeight = newFontHeight; 1220 1221 qreal diff = std::abs(oldFontHeight - newFontHeight); 1222 m_fontAscent += (diff / 2); 1223 } 1224 } 1225 1226 void KateRenderer::updateMarkerSize() 1227 { 1228 // marker size will be calculated over the value defined 1229 // on dialog 1230 1231 m_markerSize = spaceWidth() / (3.5 - (m_doc->config()->markerSize() * 0.5)); 1232 } 1233 1234 qreal KateRenderer::spaceWidth() const 1235 { 1236 return m_fontMetrics.horizontalAdvance(spaceChar); 1237 } 1238 1239 void KateRenderer::layoutLine(KateLineLayout *lineLayout, int maxwidth, bool cacheLayout) const 1240 { 1241 // if maxwidth == -1 we have no wrap 1242 1243 Kate::TextLine textLine = lineLayout->textLine(); 1244 1245 QTextLayout *l = lineLayout->layout(); 1246 if (!l) { 1247 l = new QTextLayout(textLine.text(), m_font); 1248 } else { 1249 l->setText(textLine.text()); 1250 l->setFont(m_font); 1251 } 1252 1253 l->setCacheEnabled(cacheLayout); 1254 1255 // Initial setup of the QTextLayout. 1256 1257 // Tab width 1258 QTextOption opt; 1259 opt.setFlags(QTextOption::IncludeTrailingSpaces); 1260 opt.setTabStopDistance(m_tabWidth * m_fontMetrics.horizontalAdvance(spaceChar)); 1261 if (m_view && m_view->config()->dynWrapAnywhere()) { 1262 opt.setWrapMode(QTextOption::WrapAnywhere); 1263 } else { 1264 opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); 1265 } 1266 1267 // Find the first strong character in the string. 1268 // If it is an RTL character, set the base layout direction of the string to RTL. 1269 // 1270 // See https://www.unicode.org/reports/tr9/#The_Paragraph_Level (Sections P2 & P3). 1271 // Qt's text renderer ("scribe") version 4.2 assumes a "higher-level protocol" 1272 // (such as KatePart) will specify the paragraph level, so it does not apply P2 & P3 1273 // by itself. If this ever change in Qt, the next code block could be removed. 1274 // ----- 1275 // Only force RTL direction if dynWordWrap is on. Otherwise the view has infinite width 1276 // and the lines will never be forced RTL no matter what direction we set. The layout 1277 // can't force a line to the right if it doesn't know where the "right" is 1278 if (isLineRightToLeft(textLine.text()) || (view()->dynWordWrap() && view()->forceRTLDirection())) { 1279 opt.setAlignment(Qt::AlignRight); 1280 opt.setTextDirection(Qt::RightToLeft); 1281 // Must turn off this flag otherwise cursor placement 1282 // is totally broken. 1283 if (view()->config()->dynWordWrap()) { 1284 auto flags = opt.flags(); 1285 flags.setFlag(QTextOption::IncludeTrailingSpaces, false); 1286 opt.setFlags(flags); 1287 } 1288 } else { 1289 opt.setAlignment(Qt::AlignLeft); 1290 opt.setTextDirection(Qt::LeftToRight); 1291 } 1292 1293 l->setTextOption(opt); 1294 1295 // Syntax highlighting, inbuilt and arbitrary 1296 QList<QTextLayout::FormatRange> decorations = decorationsForLine(textLine, lineLayout->line()); 1297 1298 // Qt works badly if you have RTL text and formats set on that text. 1299 // It will shape the text according to the given format ranges which 1300 // produces incorrect results as a letter in RTL can have a different 1301 // shape depending upon where in the word it resides. The resulting output 1302 // looks like: وقار vs وق ار, i.e, the ligature قا is broken into ق ا which 1303 // is really bad for readability 1304 if (opt.textDirection() == Qt::RightToLeft) { 1305 // We can fix this to a large extent here by using QGlyphRun etc, but for now 1306 // we only fix this for formats which have a background color and a foreground 1307 // color that is same as "dsNormal". Reasoning is that, it is unlikely that RTL 1308 // text will have a lot of cases where you have partially colored ligatures. BG 1309 // formats are different, you can easily have a format that covers a ligature partially 1310 // as a result of "Search" or "multiple cursor selection" 1311 QColor c = view()->theme().textColor(KSyntaxHighlighting::Theme::Normal); 1312 decorations.erase(std::remove_if(decorations.begin(), 1313 decorations.end(), 1314 [c](const QTextLayout::FormatRange &fr) { 1315 return fr.format.hasProperty(QTextFormat::BackgroundBrush) 1316 && (fr.format.property(QTextFormat::ForegroundBrush).value<QBrush>().color() == c 1317 || fr.format.foreground() == Qt::NoBrush); 1318 }), 1319 decorations.end()); 1320 } 1321 1322 int firstLineOffset = 0; 1323 1324 if (!isPrinterFriendly() && m_view) { 1325 const auto inlineNotes = m_view->inlineNotes(lineLayout->line()); 1326 for (const KateInlineNoteData ¬eData : inlineNotes) { 1327 const KTextEditor::InlineNote inlineNote(noteData); 1328 const int column = inlineNote.position().column(); 1329 int width = inlineNote.width(); 1330 1331 // Make space for every inline note. 1332 // If it is on column 0 (at the beginning of the line), we must offset the first line. 1333 // If it is inside the text, we use absolute letter spacing to create space for it between the two letters. 1334 // If it is outside of the text, we don't have to make space for it. 1335 if (column == 0) { 1336 firstLineOffset = width; 1337 } else if (column < l->text().length()) { 1338 QTextCharFormat text_char_format; 1339 const qreal caretWidth = caretStyle() == KTextEditor::caretStyles::Line ? 2.0 : 0.0; 1340 text_char_format.setFontLetterSpacing(width + caretWidth); 1341 text_char_format.setFontLetterSpacingType(QFont::AbsoluteSpacing); 1342 decorations.append(QTextLayout::FormatRange{column - 1, 1, text_char_format}); 1343 } 1344 } 1345 } 1346 l->setFormats(decorations); 1347 1348 // Begin layouting 1349 l->beginLayout(); 1350 1351 int height = 0; 1352 int shiftX = 0; 1353 1354 bool needShiftX = (maxwidth != -1) && m_view && (m_view->config()->dynWordWrapAlignIndent() > 0); 1355 1356 while (true) { 1357 QTextLine line = l->createLine(); 1358 if (!line.isValid()) { 1359 break; 1360 } 1361 1362 if (maxwidth > 0) { 1363 line.setLineWidth(maxwidth); 1364 } else { 1365 line.setLineWidth(INT_MAX); 1366 } 1367 1368 // we include the leading, this must match the ::updateFontHeight code! 1369 line.setLeadingIncluded(true); 1370 1371 line.setPosition(QPoint(line.lineNumber() ? shiftX : firstLineOffset, height - line.ascent() + m_fontAscent)); 1372 1373 if (needShiftX && line.width() > 0) { 1374 needShiftX = false; 1375 // Determine x offset for subsequent-lines-of-paragraph indenting 1376 int pos = textLine.nextNonSpaceChar(0); 1377 1378 if (pos > 0) { 1379 shiftX = (int)line.cursorToX(pos); 1380 } 1381 1382 // check for too deep shift value and limit if necessary 1383 if (m_view && shiftX > ((double)maxwidth / 100 * m_view->config()->dynWordWrapAlignIndent())) { 1384 shiftX = 0; 1385 } 1386 1387 // if shiftX > 0, the maxwidth has to adapted 1388 maxwidth -= shiftX; 1389 1390 lineLayout->shiftX = shiftX; 1391 } 1392 1393 height += lineHeight(); 1394 } 1395 1396 l->endLayout(); 1397 1398 lineLayout->setLayout(l); 1399 } 1400 1401 // 1) QString::isRightToLeft() sux 1402 // 2) QString::isRightToLeft() is marked as internal (WTF?) 1403 // 3) QString::isRightToLeft() does not seem to work on my setup 1404 // 4) isStringRightToLeft() should behave much better than QString::isRightToLeft() therefore: 1405 // 5) isStringRightToLeft() kicks ass 1406 bool KateRenderer::isLineRightToLeft(QStringView str) 1407 { 1408 // borrowed from QString::updateProperties() 1409 for (auto c : str) { 1410 switch (c.direction()) { 1411 case QChar::DirL: 1412 case QChar::DirLRO: 1413 case QChar::DirLRE: 1414 return false; 1415 1416 case QChar::DirR: 1417 case QChar::DirAL: 1418 case QChar::DirRLO: 1419 case QChar::DirRLE: 1420 return true; 1421 1422 default: 1423 break; 1424 } 1425 } 1426 1427 return false; 1428 #if 0 1429 // or should we use the direction of the widget? 1430 QWidget *display = qobject_cast<QWidget *>(view()->parent()); 1431 if (!display) { 1432 return false; 1433 } 1434 return display->layoutDirection() == Qt::RightToLeft; 1435 #endif 1436 } 1437 1438 int KateRenderer::cursorToX(const KateTextLayout &range, int col, bool returnPastLine) const 1439 { 1440 return cursorToX(range, KTextEditor::Cursor(range.line(), col), returnPastLine); 1441 } 1442 1443 int KateRenderer::cursorToX(const KateTextLayout &range, const KTextEditor::Cursor pos, bool returnPastLine) const 1444 { 1445 Q_ASSERT(range.isValid()); 1446 1447 int x; 1448 if (range.lineLayout().width() > 0) { 1449 x = (int)range.lineLayout().cursorToX(pos.column()); 1450 } else { 1451 x = 0; 1452 } 1453 1454 int over = pos.column() - range.endCol(); 1455 if (returnPastLine && over > 0) { 1456 x += over * spaceWidth(); 1457 } 1458 1459 return x; 1460 } 1461 1462 KTextEditor::Cursor KateRenderer::xToCursor(const KateTextLayout &range, int x, bool returnPastLine) const 1463 { 1464 Q_ASSERT(range.isValid()); 1465 KTextEditor::Cursor ret(range.line(), range.lineLayout().xToCursor(x)); 1466 1467 // Do not wrap to the next line. (bug #423253) 1468 if (range.wrap() && ret.column() >= range.endCol() && range.length() > 0) { 1469 ret.setColumn(range.endCol() - 1); 1470 } 1471 // TODO wrong for RTL lines? 1472 if (returnPastLine && range.endCol(true) == -1 && x > range.width() + range.xOffset()) { 1473 ret.setColumn(ret.column() + round((x - (range.width() + range.xOffset())) / spaceWidth())); 1474 } 1475 1476 return ret; 1477 } 1478 1479 void KateRenderer::setCaretOverrideColor(const QColor &color) 1480 { 1481 m_caretOverrideColor = color; 1482 } 1483 1484 void KateRenderer::paintSelection(QPaintDevice *d, int startLine, int xStart, int endLine, int xEnd, qreal scale) 1485 { 1486 if (!d || scale < 0.0) { 1487 return; 1488 } 1489 1490 const int lineHeight = std::max(1, this->lineHeight()); 1491 QPainter paint(d); 1492 paint.scale(scale, scale); 1493 1494 // clip out non selected parts of start / end line 1495 { 1496 QRect mainRect(0, 0, d->width(), d->height()); 1497 QRegion main(mainRect); 1498 // start line 1499 QRect startRect(0, 0, xStart, lineHeight); 1500 QRegion startRegion(startRect); 1501 // end line 1502 QRect endRect(mainRect.bottomLeft().x() + xEnd, mainRect.bottomRight().y() - lineHeight, mainRect.width() - xEnd, lineHeight); 1503 QRegion drawRegion = main.subtracted(startRegion).subtracted(QRegion(endRect)); 1504 paint.setClipRegion(drawRegion); 1505 } 1506 1507 for (int line = startLine; line <= endLine; ++line) { 1508 // get real line, skip if invalid! 1509 if (line < 0 || line >= doc()->lines()) { 1510 continue; 1511 } 1512 1513 // compute layout WITHOUT cache to not poison it + render it 1514 KateLineLayout lineLayout(*this); 1515 lineLayout.setLine(line, -1); 1516 layoutLine(&lineLayout, -1 /* no wrap */, false /* no layout cache */); 1517 KateRenderer::PaintTextLineFlags flags; 1518 flags.setFlag(KateRenderer::SkipDrawFirstInvisibleLineUnderlined); 1519 flags.setFlag(KateRenderer::SkipDrawLineSelection); 1520 paintTextLine(paint, &lineLayout, 0, 0, QRectF{}, nullptr, flags); 1521 1522 // translate for next line 1523 paint.translate(0, lineHeight); 1524 } 1525 }