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