KTextEditor

katerenderer.cpp
1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch>
4 SPDX-FileCopyrightText: 2003-2005 Hamish Rodda <rodda@kde.org>
5 SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org>
6 SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org>
7 SPDX-FileCopyrightText: 1999 Jochen Wilhelmy <digisnap@cs.tu-berlin.de>
8 SPDX-FileCopyrightText: 2013 Andrey Matveyakin <a.matveyakin@gmail.com>
9
10 SPDX-License-Identifier: LGPL-2.0-only
11*/
12
13#include "katerenderer.h"
14
15#include "inlinenotedata.h"
16#include "katebuffer.h"
17#include "katedocument.h"
18#include "kateextendedattribute.h"
19#include "katehighlight.h"
20#include "katerenderrange.h"
21#include "katetextlayout.h"
22#include "kateview.h"
23
24#include "ktexteditor/attribute.h"
25#include "ktexteditor/inlinenote.h"
26#include "ktexteditor/inlinenoteprovider.h"
27
28#include "katepartdebug.h"
29
30#include <QBrush>
31#include <QPaintEngine>
32#include <QPainter>
33#include <QPainterPath>
34#include <QRegularExpression>
35#include <QStack>
36#include <QtMath> // qCeil
37
38static const QChar tabChar(QLatin1Char('\t'));
39static const QChar spaceChar(QLatin1Char(' '));
40static const QChar nbSpaceChar(0xa0); // non-breaking space
41
43 : m_doc(doc)
44 , m_folding(folding)
45 , m_view(view)
46 , m_tabWidth(m_doc->config()->tabWidth())
47 , m_indentWidth(m_doc->config()->indentationWidth())
48 , m_caretStyle(KTextEditor::caretStyles::Line)
49 , m_drawCaret(true)
50 , m_showSelections(true)
51 , m_showTabs(true)
52 , m_showSpaces(KateDocumentConfig::Trailing)
53 , m_showNonPrintableSpaces(false)
54 , m_printerFriendly(false)
55 , m_config(new KateRendererConfig(this))
56 , m_font(m_config->baseFont())
57 , m_fontMetrics(m_font)
58{
60
61 // initialize with a sane font height
62 updateFontHeight();
63
64 // make the proper calculation for markerSize
66}
67
69{
70 m_attributes = m_doc->highlight()->attributes(config()->schema());
71}
72
74{
75 if (pos < (uint)m_attributes.count()) {
76 return m_attributes[pos];
77 }
78
79 return m_attributes[0];
80}
81
82KTextEditor::Attribute::Ptr KateRenderer::specificAttribute(int context) const
83{
84 if (context >= 0 && context < m_attributes.count()) {
85 return m_attributes[context];
86 }
87
88 return m_attributes[0];
89}
90
92{
93 m_drawCaret = drawCaret;
94}
95
96void KateRenderer::setCaretStyle(KTextEditor::caretStyles style)
97{
98 m_caretStyle = style;
99}
100
102{
103 m_showTabs = showTabs;
104}
105
106void KateRenderer::setShowSpaces(KateDocumentConfig::WhitespaceRendering showSpaces)
107{
108 m_showSpaces = showSpaces;
109}
110
112{
113 m_showNonPrintableSpaces = on;
114}
115
117{
118 m_tabWidth = tabWidth;
119}
120
122{
123 return m_config->showIndentationLines();
124}
125
127{
128 // invalidate our "active indent line" cached stuff
129 m_currentBracketRange = KTextEditor::Range::invalid();
130 m_currentBracketX = -1;
131
132 m_config->setShowIndentationLines(showIndentLines);
133}
134
135void KateRenderer::setIndentWidth(int indentWidth)
136{
137 m_indentWidth = indentWidth;
138}
139
141{
142 m_showSelections = showSelections;
143}
144
146{
147 QFont f(config()->baseFont());
148 f.setPointSizeF(f.pointSizeF() + step);
149 config()->setFont(f);
150}
151
152void KateRenderer::resetFontSizes() const
153{
154 QFont f(KateRendererConfig::global()->baseFont());
155 config()->setFont(f);
156}
157
158void KateRenderer::decreaseFontSizes(qreal step) const
159{
160 QFont f(config()->baseFont());
161 if ((f.pointSizeF() - step) > 0) {
162 f.setPointSizeF(f.pointSizeF() - step);
163 }
164 config()->setFont(f);
165}
166
168{
169 return m_printerFriendly;
170}
171
172void KateRenderer::setPrinterFriendly(bool printerFriendly)
173{
174 m_printerFriendly = printerFriendly;
175 setShowTabs(false);
176 setShowSpaces(KateDocumentConfig::None);
177 setShowSelections(false);
178 setDrawCaret(false);
179}
180
181void KateRenderer::paintTextLineBackground(QPainter &paint, KateLineLayout *layout, int currentViewLine, int xStart, int xEnd)
182{
183 if (isPrinterFriendly()) {
184 return;
185 }
186
187 // Normal background color
188 QColor backgroundColor(config()->backgroundColor());
189
190 // paint the current line background if we're on the current line
191 QColor currentLineColor = config()->highlightedLineColor();
192
193 // Check for mark background
194 int markRed = 0;
195 int markGreen = 0;
196 int markBlue = 0;
197 int markCount = 0;
198
199 // Retrieve marks for this line
200 uint mrk = m_doc->mark(layout->line());
201 if (mrk) {
202 for (uint bit = 0; bit < 32; bit++) {
204 if (mrk & markType) {
205 QColor markColor = config()->lineMarkerColor(markType);
206
207 if (markColor.isValid()) {
208 markCount++;
209 markRed += markColor.red();
210 markGreen += markColor.green();
211 markBlue += markColor.blue();
212 }
213 }
214 } // for
215 } // Marks
216
217 if (markCount) {
218 markRed /= markCount;
219 markGreen /= markCount;
220 markBlue /= markCount;
221 backgroundColor.setRgb(int((backgroundColor.red() * 0.9) + (markRed * 0.1)),
222 int((backgroundColor.green() * 0.9) + (markGreen * 0.1)),
223 int((backgroundColor.blue() * 0.9) + (markBlue * 0.1)),
224 backgroundColor.alpha());
225 }
226
227 // Draw line background
228 paint.fillRect(0, 0, xEnd - xStart, lineHeight() * layout->viewLineCount(), backgroundColor);
229
230 // paint the current line background if we're on the current line
231 const bool currentLineHasSelection = m_view && m_view->selection() && m_view->selectionRange().overlapsLine(layout->line());
232 if (currentViewLine != -1 && !currentLineHasSelection) {
233 if (markCount) {
234 markRed /= markCount;
235 markGreen /= markCount;
236 markBlue /= markCount;
237 currentLineColor.setRgb(int((currentLineColor.red() * 0.9) + (markRed * 0.1)),
238 int((currentLineColor.green() * 0.9) + (markGreen * 0.1)),
239 int((currentLineColor.blue() * 0.9) + (markBlue * 0.1)),
240 currentLineColor.alpha());
241 }
242
243 paint.fillRect(0, lineHeight() * currentViewLine, xEnd - xStart, lineHeight(), currentLineColor);
244 }
245}
246
247void KateRenderer::paintTabstop(QPainter &paint, qreal x, qreal y) const
248{
249 QPen penBackup(paint.pen());
250 QPen pen(config()->tabMarkerColor());
251 pen.setWidthF(qMax(1.0, spaceWidth() / 10.0));
252 paint.setPen(pen);
253
254 int dist = spaceWidth() * 0.3;
255 QPoint points[8];
256 points[0] = QPoint(x - dist, y - dist);
257 points[1] = QPoint(x, y);
258 points[2] = QPoint(x, y);
259 points[3] = QPoint(x - dist, y + dist);
260 x += spaceWidth() / 3.0;
261 points[4] = QPoint(x - dist, y - dist);
262 points[5] = QPoint(x, y);
263 points[6] = QPoint(x, y);
264 points[7] = QPoint(x - dist, y + dist);
265 paint.drawLines(points, 4);
266 paint.setPen(penBackup);
267}
268
269void KateRenderer::paintSpaces(QPainter &paint, const QPointF *points, const int count) const
270{
271 if (count == 0) {
272 return;
273 }
274 QPen penBackup(paint.pen());
275 QPen pen(config()->tabMarkerColor());
276
277 pen.setWidthF(m_markerSize);
278 pen.setCapStyle(Qt::RoundCap);
279 paint.setPen(pen);
281 paint.drawPoints(points, count);
282 paint.setPen(penBackup);
284}
285
286void KateRenderer::paintNonBreakSpace(QPainter &paint, qreal x, qreal y) const
287{
288 QPen penBackup(paint.pen());
289 QPen pen(config()->tabMarkerColor());
290 pen.setWidthF(qMax(1.0, spaceWidth() / 10.0));
291 paint.setPen(pen);
292
293 const int height = fontHeight();
294 const int width = spaceWidth();
295
296 QPoint points[6];
297 points[0] = QPoint(x + width / 10, y + height / 4);
298 points[1] = QPoint(x + width / 10, y + height / 3);
299 points[2] = QPoint(x + width / 10, y + height / 3);
300 points[3] = QPoint(x + width - width / 10, y + height / 3);
301 points[4] = QPoint(x + width - width / 10, y + height / 3);
302 points[5] = QPoint(x + width - width / 10, y + height / 4);
303 paint.drawLines(points, 3);
304 paint.setPen(penBackup);
305}
306
307void KateRenderer::paintNonPrintableSpaces(QPainter &paint, qreal x, qreal y, const QChar &chr)
308{
309 paint.save();
310 QPen pen(config()->spellingMistakeLineColor());
311 pen.setWidthF(qMax(1.0, spaceWidth() * 0.1));
312 paint.setPen(pen);
313
314 const int height = fontHeight();
315 const int width = m_fontMetrics.boundingRect(chr).width();
316 const int offset = spaceWidth() * 0.1;
317
318 QPoint points[8];
319 points[0] = QPoint(x - offset, y + offset);
320 points[1] = QPoint(x + width + offset, y + offset);
321 points[2] = QPoint(x + width + offset, y + offset);
322 points[3] = QPoint(x + width + offset, y - height - offset);
323 points[4] = QPoint(x + width + offset, y - height - offset);
324 points[5] = QPoint(x - offset, y - height - offset);
325 points[6] = QPoint(x - offset, y - height - offset);
326 points[7] = QPoint(x - offset, y + offset);
327 paint.drawLines(points, 4);
328 paint.restore();
329}
330
331/**
332 * Helper function that checks if our cursor is at a bracket
333 * and calculates X position for opening/closing brackets. We
334 * then use this data to color the indentation line differently.
335 * @p view is current view
336 * @p range is the current range from @ref paintTextLine
337 * @p spaceWidth width of space char
338 * @p c is the position of cursor
339 * @p bracketXPos will be X position of close bracket or -1 if not found
340 */
341static KTextEditor::Range cursorAtBracket(KTextEditor::ViewPrivate *view, const KateLineLayout *range, int spaceWidth, KTextEditor::Cursor c, int &bracketXPos)
342{
343 bracketXPos = -1;
344 if (range->line() != c.line()) {
346 }
347
348 auto *doc = view->doc();
349 // Avoid work if we are below tabwidth
350 if (c.column() < doc->config()->tabWidth()) {
352 }
353
354 // We match these brackets only
355 static constexpr QChar brackets[] = {QLatin1Char('{'), QLatin1Char('}'), QLatin1Char('('), QLatin1Char(')')};
356 // look for character in front
357 QChar right = doc->characterAt(c);
358 auto it = std::find(std::begin(brackets), std::end(brackets), right);
359
361 bool found = false;
362 if (it != std::end(brackets)) {
363 found = true;
364 } else {
365 // look at previous character
366 QChar left = doc->characterAt({c.line(), c.column() - 1});
367 it = std::find(std::begin(brackets), std::end(brackets), left);
368 if (it != std::end(brackets)) {
369 found = true;
370 }
371 }
372
373 // We have a bracket
374 if (found) {
375 ret = doc->findMatchingBracket(c, 500);
376 if (!ret.isValid()) {
377 return ret;
378 }
379 bracketXPos = (ret.end().column() * spaceWidth) + 1;
380 }
381
382 return ret;
383}
384
385void KateRenderer::paintIndentMarker(QPainter &paint, uint x, int line)
386{
387 const QPen penBackup(paint.pen());
388 static const QList<qreal> dashPattern = QList<qreal>() << 1 << 1;
389 QPen myPen;
390
391 const bool onBracket = m_currentBracketX == (int)x;
392 if (onBracket && m_currentBracketRange.containsLine(line)) {
394 c.setAlphaF(0.7);
395 myPen.setColor(c);
396 } else {
397 myPen.setColor(config()->indentationLineColor());
398 myPen.setDashPattern(dashPattern);
399 }
400
401 paint.setPen(myPen);
402
403 QPainter::RenderHints renderHints = paint.renderHints();
404 paint.setRenderHints(renderHints, false);
405
406 paint.drawLine(x + 2, 0, x + 2, lineHeight());
407
408 paint.setRenderHints(renderHints, true);
409
410 paint.setPen(penBackup);
411}
412
413static bool rangeLessThanForRenderer(const Kate::TextRange *a, const Kate::TextRange *b)
414{
415 // compare Z-Depth first
416 // smaller Z-Depths should win!
417 if (a->zDepth() != b->zDepth()) {
418 return a->zDepth() > b->zDepth();
419 }
420
421 if (a->end().toCursor() != b->end().toCursor()) {
422 return a->end().toCursor() > b->end().toCursor();
423 }
424
425 return a->start().toCursor() < b->start().toCursor();
426}
427
428QList<QTextLayout::FormatRange> KateRenderer::decorationsForLine(const Kate::TextLine &textLine, int line, bool selectionsOnly) const
429{
430 // limit number of attributes we can highlight in reasonable time
431 const int limitOfRanges = 1024;
432 auto rangesWithAttributes = m_doc->buffer().rangesForLine(line, m_printerFriendly ? nullptr : m_view, true);
433 if (rangesWithAttributes.size() > limitOfRanges) {
434 rangesWithAttributes.clear();
435 }
436
437 // Don't compute the highlighting if there isn't going to be any highlighting
438 const auto &al = textLine.attributesList();
439 if (!(selectionsOnly || !al.empty() || !rangesWithAttributes.empty())) {
441 }
442
443 // Add the inbuilt highlighting to the list, limit with limitOfRanges
444 RenderRangeVector renderRanges;
445 if (!al.empty()) {
446 auto &currentRange = renderRanges.pushNewRange();
447 for (int i = 0; i < std::min<int>(al.size(), limitOfRanges); ++i) {
448 if (al[i].length > 0 && al[i].attributeValue > 0) {
449 currentRange.addRange(KTextEditor::Range(KTextEditor::Cursor(line, al[i].offset), al[i].length), specificAttribute(al[i].attributeValue));
450 }
451 }
452 }
453
454 // check for dynamic hl stuff
455 const QSet<Kate::TextRange *> *rangesMouseIn = m_view ? m_view->rangesMouseIn() : nullptr;
456 const QSet<Kate::TextRange *> *rangesCaretIn = m_view ? m_view->rangesCaretIn() : nullptr;
457 bool anyDynamicHlsActive = m_view && (!rangesMouseIn->empty() || !rangesCaretIn->empty());
458
459 // 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
460 // rangs behavior ;)
461 std::sort(rangesWithAttributes.begin(), rangesWithAttributes.end(), rangeLessThanForRenderer);
462
463 renderRanges.reserve(rangesWithAttributes.size());
464 // loop over all ranges
465 for (int i = 0; i < rangesWithAttributes.size(); ++i) {
466 // real range
467 Kate::TextRange *kateRange = rangesWithAttributes[i];
468
469 // calculate attribute, default: normal attribute
471 if (anyDynamicHlsActive) {
472 // check mouse in
473 if (KTextEditor::Attribute::Ptr attributeMouseIn = attribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)) {
474 if (rangesMouseIn->contains(kateRange)) {
475 attribute = attributeMouseIn;
476 }
477 }
478
479 // check caret in
480 if (KTextEditor::Attribute::Ptr attributeCaretIn = attribute->dynamicAttribute(KTextEditor::Attribute::ActivateCaretIn)) {
481 if (rangesCaretIn->contains(kateRange)) {
482 attribute = attributeCaretIn;
483 }
484 }
485 }
486
487 // span range
488 renderRanges.pushNewRange().addRange(*kateRange, std::move(attribute));
489 }
490
491 // Add selection highlighting if we're creating the selection decorations
492 if ((m_view && selectionsOnly && showSelections() && m_view->selection()) || (m_view && m_view->blockSelection())) {
493 auto &currentRange = renderRanges.pushNewRange();
494
495 // Set up the selection background attribute TODO: move this elsewhere, eg. into the config?
496 static KTextEditor::Attribute::Ptr backgroundAttribute;
497 if (!backgroundAttribute) {
498 backgroundAttribute = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute());
499 }
500
501 if (!hasCustomLineHeight()) {
502 backgroundAttribute->setBackground(config()->selectionColor());
503 }
504 backgroundAttribute->setForeground(attribute(KSyntaxHighlighting::Theme::TextStyle::Normal)->selectedForeground().color());
505
506 // Create a range for the current selection
507 if (m_view->blockSelection() && m_view->selectionRange().overlapsLine(line)) {
508 currentRange.addRange(m_doc->rangeOnLine(m_view->selectionRange(), line), backgroundAttribute);
509 } else {
510 currentRange.addRange(m_view->selectionRange(), backgroundAttribute);
511 }
512 }
513
514 // no render ranges, nothing to do, else we loop below endless!
515 if (renderRanges.isEmpty()) {
517 }
518
519 // Calculate the range which we need to iterate in order to get the highlighting for just this line
520 KTextEditor::Cursor currentPosition;
521 KTextEditor::Cursor endPosition;
522 if (m_view && selectionsOnly) {
523 if (m_view->blockSelection()) {
524 KTextEditor::Range subRange = m_doc->rangeOnLine(m_view->selectionRange(), line);
525 currentPosition = subRange.start();
526 endPosition = subRange.end();
527 } else {
528 KTextEditor::Range rangeNeeded = m_view->selectionRange() & KTextEditor::Range(line, 0, line + 1, 0);
529
530 currentPosition = qMax(KTextEditor::Cursor(line, 0), rangeNeeded.start());
531 endPosition = qMin(KTextEditor::Cursor(line + 1, 0), rangeNeeded.end());
532 }
533 } else {
534 currentPosition = KTextEditor::Cursor(line, 0);
535 endPosition = KTextEditor::Cursor(line + 1, 0);
536 }
537
538 // Background formats have lower priority so they get overriden by selection
539 const bool shoudlClearBackground = m_view && hasCustomLineHeight() && m_view->selection();
540 const KTextEditor::Range selectionRange = shoudlClearBackground ? m_view->selectionRange() : KTextEditor::Range::invalid();
541
542 // Main iterative loop. This walks through each set of highlighting ranges, and stops each
543 // time the highlighting changes. It then creates the corresponding QTextLayout::FormatRanges.
545 while (currentPosition < endPosition) {
546 renderRanges.advanceTo(currentPosition);
547
548 if (!renderRanges.hasAttribute()) {
549 // No attribute, don't need to create a FormatRange for this text range
550 currentPosition = renderRanges.nextBoundary();
551 continue;
552 }
553
554 KTextEditor::Cursor nextPosition = renderRanges.nextBoundary();
555
556 // Create the format range and populate with the correct start, length and format info
558 fr.start = currentPosition.column();
559
560 if (nextPosition < endPosition || endPosition.line() <= line) {
561 fr.length = nextPosition.column() - currentPosition.column();
562
563 } else {
564 // before we did here +1 to force background drawing at the end of the line when it's warranted
565 // we now skip this, we don't draw e.g. full line backgrounds
566 fr.length = textLine.length() - currentPosition.column();
567 }
568
569 KTextEditor::Attribute::Ptr a = renderRanges.generateAttribute();
570 if (a) {
571 fr.format = *a;
572
573 if (selectionsOnly) {
574 assignSelectionBrushesFromAttribute(fr, *a);
575 }
576 }
577
578 // Clear background if this position overlaps selection
579 if (shoudlClearBackground && selectionRange.contains(currentPosition) && fr.format.hasProperty(QTextFormat::BackgroundBrush)) {
580 fr.format.clearBackground();
581 }
582
583 newHighlight.append(fr);
584
585 currentPosition = nextPosition;
586 }
587
588 // ensure bold & italic fonts work, even if the main font is e.g. light or something like that
589 for (auto &formatRange : newHighlight) {
590 if (formatRange.format.fontWeight() == QFont::Bold || formatRange.format.fontItalic()) {
591 formatRange.format.setFontStyleName(QString());
592 }
593 }
594
595 return newHighlight;
596}
597
598void KateRenderer::assignSelectionBrushesFromAttribute(QTextLayout::FormatRange &target, const KTextEditor::Attribute &attribute)
599{
600 if (attribute.hasProperty(SelectedForeground)) {
601 target.format.setForeground(attribute.selectedForeground());
602 }
603 if (attribute.hasProperty(SelectedBackground)) {
604 target.format.setBackground(attribute.selectedBackground());
605 }
606}
607
608void KateRenderer::paintTextBackground(QPainter &paint,
609 KateLineLayout *layout,
610 const QList<QTextLayout::FormatRange> &selRanges,
611 const QBrush &brush,
612 int xStart) const
613{
614 const bool rtl = layout->isRightToLeft();
615
616 for (const auto &sel : selRanges) {
617 const int s = sel.start;
618 const int e = sel.start + sel.length;
619 QBrush br;
620
621 // Prefer using the brush supplied by user
622 if (brush != Qt::NoBrush) {
623 br = brush;
624 } else if (sel.format.background() != Qt::NoBrush) {
625 // Otherwise use the brush in format
626 br = sel.format.background();
627 } else {
628 // skip if no brush to fill with
629 continue;
630 }
631
632 const int startViewLine = layout->viewLineForColumn(s);
633 const int endViewLine = layout->viewLineForColumn(e);
634 if (startViewLine == endViewLine) {
635 KateTextLayout l = layout->viewLine(startViewLine);
636 // subtract xStart so that the selection is shown where it belongs
637 // we don't do it in the else case because this only matters when dynWrap==false
638 // and when dynWrap==false, we always have 1 QTextLine per layout
639 const int startX = cursorToX(l, s) - xStart;
640 const int endX = cursorToX(l, e) - xStart;
641 const int y = startViewLine * lineHeight();
642 QRect r(startX, y, (endX - startX), lineHeight());
643 paint.fillRect(r, br);
644 } else {
645 QPainterPath p;
646 for (int l = startViewLine; l <= endViewLine; ++l) {
647 auto kateLayout = layout->viewLine(l);
648 int sx = 0;
649 int width = rtl ? kateLayout.lineLayout().width() : kateLayout.lineLayout().naturalTextWidth();
650
651 if (l == startViewLine) {
652 if (rtl) {
653 // For rtl, Rect starts at 0 and ends at selection start
654 sx = 0;
655 width = kateLayout.lineLayout().cursorToX(s);
656 } else {
657 sx = kateLayout.lineLayout().cursorToX(s);
658 }
659 } else if (l == endViewLine) {
660 if (rtl) {
661 // Drawing will start at selection end, and end at the view border
662 sx = kateLayout.lineLayout().cursorToX(e);
663 } else {
664 width = kateLayout.lineLayout().cursorToX(e);
665 }
666 }
667
668 const int y = l * lineHeight();
669 QRect r(sx, y, width - sx, lineHeight());
670 p.addRect(r);
671 }
672 paint.fillPath(p, br);
673 }
674 }
675}
676
678 KateLineLayout *range,
679 int xStart,
680 int xEnd,
681 const QRectF &textClipRect,
682 const KTextEditor::Cursor *cursor,
683 PaintTextLineFlags flags)
684{
685 Q_ASSERT(range->isValid());
686
687 // qCDebug(LOG_KTE)<<"KateRenderer::paintTextLine";
688
689 // font data
690 const QFontMetricsF &fm = m_fontMetrics;
691
692 int currentViewLine = -1;
693 if (cursor && cursor->line() == range->line()) {
694 currentViewLine = range->viewLineForColumn(cursor->column());
695 }
696
697 paintTextLineBackground(paint, range, currentViewLine, xStart, xEnd);
698
699 // Draws the dashed underline at the start of a folded block of text.
700 if (!(flags & SkipDrawFirstInvisibleLineUnderlined) && range->startsInvisibleBlock()) {
701 QPen pen(config()->foldingColor());
702 pen.setCosmetic(true);
704 pen.setDashOffset(xStart);
705 pen.setWidth(2);
706 paint.setPen(pen);
707 paint.drawLine(0, (lineHeight() * range->viewLineCount()) - 2, xEnd - xStart, (lineHeight() * range->viewLineCount()) - 2);
708 }
709
710 if (range->layout().lineCount() > 0) {
711 bool drawSelection =
712 m_view && m_view->selection() && showSelections() && m_view->selectionRange().overlapsLine(range->line()) && !flags.testFlag(SkipDrawLineSelection);
713 // Draw selection in block selection mode. We need 2 kinds of selections that QTextLayout::draw can't render:
714 // - past-end-of-line selection and
715 // - 0-column-wide selection (used to indicate where text will be typed)
716 if (drawSelection && m_view->blockSelection()) {
717 int selectionStartColumn = m_doc->fromVirtualColumn(range->line(), m_doc->toVirtualColumn(m_view->selectionRange().start()));
718 int selectionEndColumn = m_doc->fromVirtualColumn(range->line(), m_doc->toVirtualColumn(m_view->selectionRange().end()));
719 QBrush selectionBrush = config()->selectionColor();
720 if (selectionStartColumn != selectionEndColumn) {
721 KateTextLayout lastLine = range->viewLine(range->viewLineCount() - 1);
722 if (selectionEndColumn > lastLine.startCol()) {
723 int selectionStartX = (selectionStartColumn > lastLine.startCol()) ? cursorToX(lastLine, selectionStartColumn, true) : 0;
724 int selectionEndX = cursorToX(lastLine, selectionEndColumn, true);
725 paint.fillRect(QRect(selectionStartX - xStart, (int)lastLine.lineLayout().y(), selectionEndX - selectionStartX, lineHeight()),
726 selectionBrush);
727 }
728 } else {
729 const int selectStickWidth = 2;
730 KateTextLayout selectionLine = range->viewLine(range->viewLineForColumn(selectionStartColumn));
731 int selectionX = cursorToX(selectionLine, selectionStartColumn, true);
732 paint.fillRect(QRect(selectionX - xStart, (int)selectionLine.lineLayout().y(), selectStickWidth, lineHeight()), selectionBrush);
733 }
734 }
735
736 QList<QTextLayout::FormatRange> additionalFormats;
737 if (range->length() > 0) {
738 // We may have changed the pen, be absolutely sure it gets set back to
739 // normal foreground color before drawing text for text that does not
740 // set the pen color
741 paint.setPen(attribute(KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color());
742 // Draw the text :)
743
744 if (range->layout().textOption().textDirection() == Qt::RightToLeft) {
745 // If the text is RTL, we draw text background ourselves
746 auto decos = decorationsForLine(range->textLine(), range->line(), false);
747 auto sr = view()->selectionRange();
748 auto c = config()->selectionColor();
749 int line = range->line();
750 // Remove "selection" format from decorations
751 // "selection" will get painted below
752 decos.erase(std::remove_if(decos.begin(),
753 decos.end(),
754 [sr, c, line](const QTextLayout::FormatRange &fr) {
755 return sr.overlapsLine(line) && sr.overlapsColumn(fr.start) && fr.format.background().color() == c;
756 }),
757 decos.end());
758 paintTextBackground(paint, range, decos, Qt::NoBrush, xStart);
759 }
760
761 if (drawSelection) {
762 additionalFormats = decorationsForLine(range->textLine(), range->line(), true);
763 if (hasCustomLineHeight()) {
764 paintTextBackground(paint, range, additionalFormats, config()->selectionColor(), xStart);
765 }
766 // DONT apply clipping, it breaks rendering when there are selections
767 range->layout().draw(&paint, QPoint(-xStart, 0), additionalFormats);
768
769 } else {
770 range->layout().draw(&paint, QPoint(-xStart, 0), QList<QTextLayout::FormatRange>{}, textClipRect);
771 }
772 }
773
774 // Check if we are at a bracket and color the indentation
775 // line differently
776 const bool indentLinesEnabled = showIndentLines();
777 if (cursor && indentLinesEnabled) {
778 auto cur = *cursor;
779 cur.setColumn(cur.column() - 1);
780 if (!m_currentBracketRange.boundaryAtCursor(*cursor) && m_currentBracketRange.end() != cur && m_currentBracketRange.start() != cur) {
781 m_currentBracketRange = cursorAtBracket(view(), range, spaceWidth(), *cursor, m_currentBracketX);
782 }
783 }
784
785 // Loop each individual line for additional text decoration etc.
786 for (int i = 0; i < range->viewLineCount(); ++i) {
787 KateTextLayout line = range->viewLine(i);
788
789 // Draw indent lines
790 if (!m_printerFriendly && (indentLinesEnabled && i == 0)) {
791 const qreal w = spaceWidth();
792 const int lastIndentColumn = range->textLine().indentDepth(m_tabWidth);
793 for (int x = m_indentWidth; x < lastIndentColumn; x += m_indentWidth) {
794 auto xPos = x * w + 1 - xStart;
795 if (xPos >= 0) {
796 paintIndentMarker(paint, xPos, range->line());
797 }
798 }
799 }
800
801 // draw an open box to mark non-breaking spaces
802 const QString &text = range->textLine().text();
803 int y = lineHeight() * i + m_fontAscent - fm.strikeOutPos();
804 int nbSpaceIndex = text.indexOf(nbSpaceChar, line.lineLayout().xToCursor(xStart));
805
806 while (nbSpaceIndex != -1 && nbSpaceIndex < line.endCol()) {
807 int x = line.lineLayout().cursorToX(nbSpaceIndex);
808 if (x > xEnd) {
809 break;
810 }
811 paintNonBreakSpace(paint, x - xStart, y);
812 nbSpaceIndex = text.indexOf(nbSpaceChar, nbSpaceIndex + 1);
813 }
814
815 // draw tab stop indicators
816 if (showTabs()) {
817 int tabIndex = text.indexOf(tabChar, line.lineLayout().xToCursor(xStart));
818 while (tabIndex != -1 && tabIndex < line.endCol()) {
819 int x = line.lineLayout().cursorToX(tabIndex);
820 if (x > xEnd) {
821 break;
822 }
823 paintTabstop(paint, x - xStart + spaceWidth() / 2.0, y);
824 tabIndex = text.indexOf(tabChar, tabIndex + 1);
825 }
826 }
827
828 // draw trailing spaces
829 if (showSpaces() != KateDocumentConfig::None) {
830 int spaceIndex = line.endCol() - 1;
831 const int trailingPos = showSpaces() == KateDocumentConfig::All ? 0 : qMax(range->textLine().lastChar(), 0);
832
833 if (spaceIndex >= trailingPos) {
834 QVarLengthArray<int, 32> spacePositions;
835 // Adjust to visible contents
836 const auto dir = range->layout().textOption().textDirection();
837 const bool isRTL = dir == Qt::RightToLeft && m_view->dynWordWrap();
838 int start = isRTL ? xEnd : xStart;
839 int end = isRTL ? xStart : xEnd;
840
841 spaceIndex = std::min(line.lineLayout().xToCursor(end), spaceIndex);
842 int visibleStart = line.lineLayout().xToCursor(start);
843
844 for (; spaceIndex >= line.startCol(); --spaceIndex) {
845 if (!text.at(spaceIndex).isSpace()) {
846 if (showSpaces() == KateDocumentConfig::Trailing) {
847 break;
848 } else {
849 continue;
850 }
851 }
852 if (text.at(spaceIndex) != QLatin1Char('\t') || !showTabs()) {
853 spacePositions << spaceIndex;
854 }
855
856 if (spaceIndex < visibleStart) {
857 break;
858 }
859 }
860
861 QPointF prev;
863 const auto spaceWidth = this->spaceWidth();
864 // reverse because we want to look at the spaces at the beginning of line first
865 for (auto rit = spacePositions.rbegin(); rit != spacePositions.rend(); ++rit) {
866 const int spaceIdx = *rit;
867 qreal x = line.lineLayout().cursorToX(spaceIdx) - xStart;
868 int dir = 1; // 1 == ltr, -1 == rtl
869 if (range->layout().textOption().alignment() == Qt::AlignRight) {
870 dir = -1;
871 if (spaceIdx > 0) {
872 QChar c = text.at(spaceIdx - 1);
873 // line is LTR aligned, but is the char ltr or rtl?
874 if (!isLineRightToLeft(QStringView(&c, 1))) {
875 dir = 1;
876 }
877 }
878 } else {
879 if (spaceIdx > 0) {
880 // line is LTR aligned, but is the char ltr or rtl?
881 QChar c = text.at(spaceIdx - 1);
882 if (isLineRightToLeft(QStringView(&c, 1))) {
883 dir = -1;
884 }
885 }
886 }
887
888 x += dir * (spaceWidth / 2.0);
889
890 const QPointF currentPoint(x, y);
891 if (!prev.isNull() && currentPoint == prev) {
892 break;
893 }
894 spacePoints << currentPoint;
895 prev = QPointF(x, y);
896 }
897 if (!spacePoints.isEmpty()) {
898 paintSpaces(paint, spacePoints.constData(), spacePoints.size());
899 }
900 }
901 }
902
904 const int y = lineHeight() * i + m_fontAscent;
905
906 static const QRegularExpression nonPrintableSpacesRegExp(
907 QStringLiteral("[\\x{0000}-\\x{0008}\\x{000A}-\\x{001F}\\x{2000}-\\x{200F}\\x{2028}-\\x{202F}\\x{205F}-\\x{2064}\\x{206A}-\\x{206F}]"));
908 QRegularExpressionMatchIterator i = nonPrintableSpacesRegExp.globalMatch(text, line.lineLayout().xToCursor(xStart));
909
910 while (i.hasNext()) {
911 const int charIndex = i.next().capturedStart();
912
913 const int x = line.lineLayout().cursorToX(charIndex);
914 if (x > xEnd) {
915 break;
916 }
917
918 paintNonPrintableSpaces(paint, x - xStart, y, text[charIndex]);
919 }
920 }
921
922 // draw word-wrap-honor-indent filling
923 if ((i > 0) && range->shiftX && (range->shiftX > xStart)) {
924 // fill background first with selection if we had selection from the previous line
925 if (drawSelection && !m_view->blockSelection() && m_view->selectionRange().start() < line.start()
926 && m_view->selectionRange().end() >= line.start()) {
927 paint.fillRect(0, lineHeight() * i, range->shiftX - xStart, lineHeight(), QBrush(config()->selectionColor()));
928 }
929
930 // paint the normal filling for the word wrap markers
931 paint.fillRect(0, lineHeight() * i, range->shiftX - xStart, lineHeight(), QBrush(config()->wordWrapMarkerColor(), Qt::Dense4Pattern));
932 }
933 }
934
935 // Draw carets
936 if (m_view && cursor && drawCaret()) {
937 const auto &secCursors = view()->secondaryCursors();
938 // Find carets on this line
939 auto mIt = std::lower_bound(secCursors.begin(), secCursors.end(), range->line(), [](const KTextEditor::ViewPrivate::SecondaryCursor &l, int line) {
940 return l.pos->line() < line;
941 });
942 bool skipPrimary = false;
943 if (mIt != secCursors.end() && mIt->cursor().line() == range->line()) {
945 auto primaryCursor = *cursor;
946 for (; mIt != secCursors.end(); ++mIt) {
947 auto cursor = mIt->cursor();
948 skipPrimary = skipPrimary || cursor == primaryCursor;
949 if (cursor == last) {
950 continue;
951 }
952 last = cursor;
953 if (cursor.line() == range->line()) {
954 paintCaret(cursor, range, paint, xStart, xEnd);
955 } else {
956 break;
957 }
958 }
959 }
960 if (!skipPrimary) {
961 paintCaret(*cursor, range, paint, xStart, xEnd);
962 }
963 }
964 }
965
966 // show word wrap marker if desirable
967 if ((!isPrinterFriendly()) && config()->wordWrapMarker()) {
968 const QPainter::RenderHints backupRenderHints = paint.renderHints();
969 paint.setPen(config()->wordWrapMarkerColor());
970 int _x = qreal(m_doc->config()->wordWrapAt()) * fm.horizontalAdvance(QLatin1Char('x')) - xStart;
971 paint.drawLine(_x, 0, _x, lineHeight());
972 paint.setRenderHints(backupRenderHints);
973 }
974
975 // Draw inline notes
976 if (!isPrinterFriendly()) {
977 const auto inlineNotes = m_view->inlineNotes(range->line());
978 for (const auto &inlineNoteData : inlineNotes) {
979 KTextEditor::InlineNote inlineNote(inlineNoteData);
980 const int column = inlineNote.position().column();
981 const int viewLine = range->viewLineForColumn(column);
982 // We only consider a line "rtl" if dynamic wrap is enabled. If it is disabled, our
983 // text is always on the left side of the view
984 const auto dir = range->layout().textOption().textDirection();
985 const bool isRTL = dir == Qt::RightToLeft && m_view->dynWordWrap();
986
987 // Determine the position where to paint the note.
988 // We start by getting the x coordinate of cursor placed to the column.
989 // If the text is ltr or rtl + dyn wrap, get the X from column
990 qreal x;
991 if (dir == Qt::LeftToRight || (dir == Qt::RightToLeft && m_view->dynWordWrap())) {
992 x = range->viewLine(viewLine).lineLayout().cursorToX(column) - xStart;
993 } else /* rtl + dynWordWrap == false */ {
994 // if text is rtl and dynamic wrap is false, the x offsets are in the opposite
995 // direction i.e., [0] == biggest offset, [1] = next
996 x = range->viewLine(viewLine).lineLayout().cursorToX(range->length() - column) - xStart;
997 }
998 int textLength = range->length();
999 if (column == 0 || column < textLength) {
1000 // If the note is inside text or at the beginning, then there is a hole in the text where the
1001 // note should be painted and the cursor gets placed at the right side of it. So we have to
1002 // subtract the width of the note to get to left side of the hole.
1003 x -= inlineNote.width();
1004 } else {
1005 // If the note is outside the text, then the X coordinate is located at the end of the line.
1006 // Add appropriate amount of spaces to reach the required column.
1007 const auto spaceToAdd = spaceWidth() * (column - textLength);
1008 x += isRTL ? -spaceToAdd : spaceToAdd;
1009 }
1010
1011 qreal y = lineHeight() * viewLine;
1012
1013 // Paint the note
1014 paint.save();
1015 paint.translate(x, y);
1016 inlineNote.provider()->paintInlineNote(inlineNote, paint, isRTL ? Qt::RightToLeft : Qt::LeftToRight);
1017 paint.restore();
1018 }
1019 }
1020}
1021
1022static void drawCursor(const QTextLayout &layout, QPainter *p, const QPointF &pos, int cursorPosition, int width, const int height)
1023{
1024 cursorPosition = qBound(0, cursorPosition, layout.text().length());
1025 const QTextLine l = layout.lineForTextPosition(cursorPosition);
1026 Q_ASSERT(l.isValid());
1027 if (!l.isValid()) {
1028 return;
1029 }
1030 const QPainter::CompositionMode origCompositionMode = p->compositionMode();
1033 }
1034
1035 const QPointF position = pos + layout.position();
1036 const qreal x = position.x() + l.cursorToX(cursorPosition);
1037 const qreal y = l.lineNumber() * height;
1038 p->fillRect(QRectF(x, y, (qreal)width, (qreal)height), p->pen().brush());
1039 p->setCompositionMode(origCompositionMode);
1040}
1041
1042void KateRenderer::paintCaret(KTextEditor::Cursor cursor, KateLineLayout *range, QPainter &paint, int xStart, int xEnd)
1043{
1044 if (range->includesCursor(cursor)) {
1045 int caretWidth;
1046 int lineWidth = 2;
1047 QColor color;
1048 QTextLine line = range->layout().lineForTextPosition(qMin(cursor.column(), range->length()));
1049
1050 // Determine the caret's style
1051 KTextEditor::caretStyles style = caretStyle();
1052
1053 // Make the caret the desired width
1054 if (style == KTextEditor::caretStyles::Line) {
1055 caretWidth = lineWidth;
1056 } else if (line.isValid() && cursor.column() < range->length()) {
1057 caretWidth = int(line.cursorToX(cursor.column() + 1) - line.cursorToX(cursor.column()));
1058 if (caretWidth < 0) {
1059 caretWidth = -caretWidth;
1060 }
1061 } else {
1062 caretWidth = spaceWidth();
1063 }
1064
1065 // Determine the color
1066 if (m_caretOverrideColor.isValid()) {
1067 // Could actually use the real highlighting system for this...
1068 // would be slower, but more accurate for corner cases
1069 color = m_caretOverrideColor;
1070 } else {
1071 // search for the FormatRange that includes the cursor
1072 const auto formatRanges = range->layout().formats();
1073 for (const QTextLayout::FormatRange &r : formatRanges) {
1074 if ((r.start <= cursor.column()) && ((r.start + r.length) > cursor.column())) {
1075 // check for Qt::NoBrush, as the returned color is black() and no invalid QColor
1076 QBrush foregroundBrush = r.format.foreground();
1077 if (foregroundBrush != Qt::NoBrush) {
1078 color = r.format.foreground().color();
1079 }
1080 break;
1081 }
1082 }
1083 // still no color found, fall back to default style
1084 if (!color.isValid()) {
1085 color = attribute(KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color();
1086 }
1087 }
1088
1089 paint.save();
1090 switch (style) {
1091 case KTextEditor::caretStyles::Line:
1092 paint.setPen(QPen(color, caretWidth));
1093 break;
1094 case KTextEditor::caretStyles::Block:
1095 // use a gray caret so it's possible to see the character
1096 color.setAlpha(128);
1097 paint.setPen(QPen(color, caretWidth));
1098 break;
1099 case KTextEditor::caretStyles::Underline:
1100 break;
1101 case KTextEditor::caretStyles::Half:
1102 color.setAlpha(128);
1103 paint.setPen(QPen(color, caretWidth));
1104 break;
1105 }
1106
1107 if (cursor.column() <= range->length()) {
1108 // Ensure correct cursor placement for RTL text
1109 if (range->layout().textOption().textDirection() == Qt::RightToLeft) {
1110 xStart += caretWidth;
1111 }
1112 qreal width = 0;
1113 if (cursor.column() < range->length()) {
1114 const auto inlineNotes = m_view->inlineNotes(range->line());
1115 for (const auto &inlineNoteData : inlineNotes) {
1116 KTextEditor::InlineNote inlineNote(inlineNoteData);
1117 if (inlineNote.position().column() == cursor.column()) {
1118 width = inlineNote.width() + (caretStyle() == KTextEditor::caretStyles::Line ? 2.0 : 0.0);
1119 }
1120 }
1121 }
1122 drawCursor(range->layout(), &paint, QPoint(-xStart - width, 0), cursor.column(), caretWidth, lineHeight());
1123 } else {
1124 // Off the end of the line... must be block mode. Draw the caret ourselves.
1125 const KateTextLayout &lastLine = range->viewLine(range->viewLineCount() - 1);
1126 int x = cursorToX(lastLine, KTextEditor::Cursor(range->line(), cursor.column()), true);
1127 if ((x >= xStart) && (x <= xEnd)) {
1128 paint.fillRect(x - xStart, (int)lastLine.lineLayout().y(), caretWidth, lineHeight(), color);
1129 }
1130 }
1131
1132 paint.restore();
1133 }
1134}
1135
1136uint KateRenderer::fontHeight() const
1137{
1138 return m_fontHeight;
1139}
1140
1141uint KateRenderer::documentHeight() const
1142{
1143 return m_doc->lines() * lineHeight();
1144}
1145
1146int KateRenderer::lineHeight() const
1147{
1148 return fontHeight();
1149}
1150
1151bool KateRenderer::getSelectionBounds(int line, int lineLength, int &start, int &end) const
1152{
1153 bool hasSel = false;
1154
1155 if (m_view->selection() && !m_view->blockSelection()) {
1156 if (m_view->lineIsSelection(line)) {
1157 start = m_view->selectionRange().start().column();
1158 end = m_view->selectionRange().end().column();
1159 hasSel = true;
1160 } else if (line == m_view->selectionRange().start().line()) {
1161 start = m_view->selectionRange().start().column();
1162 end = lineLength;
1163 hasSel = true;
1164 } else if (m_view->selectionRange().containsLine(line)) {
1165 start = 0;
1166 end = lineLength;
1167 hasSel = true;
1168 } else if (line == m_view->selectionRange().end().line()) {
1169 start = 0;
1170 end = m_view->selectionRange().end().column();
1171 hasSel = true;
1172 }
1173 } else if (m_view->lineHasSelected(line)) {
1174 start = m_view->selectionRange().start().column();
1175 end = m_view->selectionRange().end().column();
1176 hasSel = true;
1177 }
1178
1179 if (start > end) {
1180 int temp = end;
1181 end = start;
1182 start = temp;
1183 }
1184
1185 return hasSel;
1186}
1187
1189{
1190 // update the attribute list pointer
1192
1193 // update font height, do this before we update the view!
1194 updateFontHeight();
1195
1196 // trigger view update, if any!
1197 if (m_view) {
1198 m_view->updateRendererConfig();
1199 }
1200}
1201
1202bool KateRenderer::hasCustomLineHeight() const
1203{
1204 return !qFuzzyCompare(config()->lineHeightMultiplier(), 1.0);
1205}
1206
1207void KateRenderer::updateFontHeight()
1208{
1209 // cache font + metrics
1210 m_font = config()->baseFont();
1211 m_fontMetrics = QFontMetricsF(m_font);
1212
1213 // ensure minimal height of one pixel to not fall in the div by 0 trap somewhere
1214 //
1215 // use a line spacing that matches the code in qt to layout/paint text
1216 //
1217 // see bug 403868
1218 // 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:
1219 //
1220 // qreal height = maxY + fontHeight - minY;
1221 //
1222 // with fontHeight:
1223 //
1224 // qreal fontHeight = font.ascent() + font.descent();
1225 m_fontHeight = qMax(1, qCeil(m_fontMetrics.ascent() + m_fontMetrics.descent()));
1226 m_fontAscent = m_fontMetrics.ascent();
1227
1228 if (hasCustomLineHeight()) {
1229 const auto oldFontHeight = m_fontHeight;
1230 const qreal newFontHeight = qreal(m_fontHeight) * config()->lineHeightMultiplier();
1231 m_fontHeight = newFontHeight;
1232
1233 qreal diff = std::abs(oldFontHeight - newFontHeight);
1234 m_fontAscent += (diff / 2);
1235 }
1236}
1237
1239{
1240 // marker size will be calculated over the value defined
1241 // on dialog
1242
1243 m_markerSize = spaceWidth() / (3.5 - (m_doc->config()->markerSize() * 0.5));
1244}
1245
1246qreal KateRenderer::spaceWidth() const
1247{
1248 return m_fontMetrics.horizontalAdvance(spaceChar);
1249}
1250
1251void KateRenderer::layoutLine(KateLineLayout *lineLayout, int maxwidth, bool cacheLayout) const
1252{
1253 // if maxwidth == -1 we have no wrap
1254
1255 Kate::TextLine textLine = lineLayout->textLine();
1256
1257 QTextLayout &l = lineLayout->modifiableLayout();
1258 l.setText(textLine.text());
1259 l.setFont(m_font);
1260 l.setCacheEnabled(cacheLayout);
1261
1262 // Initial setup of the QTextLayout.
1263
1264 // Tab width
1265 QTextOption opt;
1267 opt.setTabStopDistance(m_tabWidth * m_fontMetrics.horizontalAdvance(spaceChar));
1268 if (m_view && m_view->config()->dynWrapAnywhere()) {
1270 } else {
1272 }
1273
1274 // Find the first strong character in the string.
1275 // If it is an RTL character, set the base layout direction of the string to RTL.
1276 //
1277 // See https://www.unicode.org/reports/tr9/#The_Paragraph_Level (Sections P2 & P3).
1278 // Qt's text renderer ("scribe") version 4.2 assumes a "higher-level protocol"
1279 // (such as KatePart) will specify the paragraph level, so it does not apply P2 & P3
1280 // by itself. If this ever change in Qt, the next code block could be removed.
1281 // -----
1282 // Only force RTL direction if dynWordWrap is on. Otherwise the view has infinite width
1283 // and the lines will never be forced RTL no matter what direction we set. The layout
1284 // can't force a line to the right if it doesn't know where the "right" is
1285 if (isLineRightToLeft(textLine.text()) || (view()->dynWordWrap() && view()->forceRTLDirection())) {
1288 // Must turn off this flag otherwise cursor placement
1289 // is totally broken.
1290 if (view()->config()->dynWordWrap()) {
1291 auto flags = opt.flags();
1292 flags.setFlag(QTextOption::IncludeTrailingSpaces, false);
1293 opt.setFlags(flags);
1294 }
1295 } else {
1298 }
1299
1300 l.setTextOption(opt);
1301
1302 // Syntax highlighting, inbuilt and arbitrary
1303 QList<QTextLayout::FormatRange> decorations = decorationsForLine(textLine, lineLayout->line());
1304
1305 // Qt works badly if you have RTL text and formats set on that text.
1306 // It will shape the text according to the given format ranges which
1307 // produces incorrect results as a letter in RTL can have a different
1308 // shape depending upon where in the word it resides. The resulting output
1309 // looks like: وقار vs وق ار, i.e, the ligature قا is broken into ق ا which
1310 // is really bad for readability
1311 if (opt.textDirection() == Qt::RightToLeft) {
1312 // We can fix this to a large extent here by using QGlyphRun etc, but for now
1313 // we only fix this for formats which have a background color and a foreground
1314 // color that is same as "dsNormal". Reasoning is that, it is unlikely that RTL
1315 // text will have a lot of cases where you have partially colored ligatures. BG
1316 // formats are different, you can easily have a format that covers a ligature partially
1317 // as a result of "Search" or "multiple cursor selection"
1319 decorations.erase(std::remove_if(decorations.begin(),
1320 decorations.end(),
1321 [c](const QTextLayout::FormatRange &fr) {
1322 return fr.format.hasProperty(QTextFormat::BackgroundBrush)
1323 && (fr.format.property(QTextFormat::ForegroundBrush).value<QBrush>().color() == c
1324 || fr.format.foreground() == Qt::NoBrush);
1325 }),
1326 decorations.end());
1327 }
1328
1329 int firstLineOffset = 0;
1330
1331 if (!isPrinterFriendly() && m_view) {
1332 const auto inlineNotes = m_view->inlineNotes(lineLayout->line());
1333 for (const KateInlineNoteData &noteData : inlineNotes) {
1334 const KTextEditor::InlineNote inlineNote(noteData);
1335 const int column = inlineNote.position().column();
1336 int width = inlineNote.width();
1337
1338 // Make space for every inline note.
1339 // If it is on column 0 (at the beginning of the line), we must offset the first line.
1340 // If it is inside the text, we use absolute letter spacing to create space for it between the two letters.
1341 // If it is outside of the text, we don't have to make space for it.
1342 if (column == 0) {
1343 firstLineOffset = width;
1344 } else if (column < l.text().length()) {
1345 QTextCharFormat text_char_format;
1346 const qreal caretWidth = caretStyle() == KTextEditor::caretStyles::Line ? 2.0 : 0.0;
1347 text_char_format.setFontLetterSpacing(width + caretWidth);
1349 decorations.append(QTextLayout::FormatRange{.start = column - 1, .length = 1, .format = text_char_format});
1350 }
1351 }
1352 }
1353 l.setFormats(decorations);
1354
1355 // Begin layouting
1356 l.beginLayout();
1357
1358 int height = 0;
1359 int shiftX = 0;
1360
1361 bool needShiftX = (maxwidth != -1) && m_view && (m_view->config()->dynWordWrapAlignIndent() > 0);
1362
1363 while (true) {
1364 QTextLine line = l.createLine();
1365 if (!line.isValid()) {
1366 break;
1367 }
1368
1369 if (maxwidth > 0) {
1370 line.setLineWidth(maxwidth);
1371 } else {
1372 line.setLineWidth(INT_MAX);
1373 }
1374
1375 // we include the leading, this must match the ::updateFontHeight code!
1376 line.setLeadingIncluded(true);
1377
1378 line.setPosition(QPoint(line.lineNumber() ? shiftX : firstLineOffset, height - line.ascent() + m_fontAscent));
1379
1380 if (needShiftX && line.width() > 0) {
1381 needShiftX = false;
1382 // Determine x offset for subsequent-lines-of-paragraph indenting
1383 int pos = textLine.nextNonSpaceChar(0);
1384
1385 if (pos > 0) {
1386 shiftX = (int)line.cursorToX(pos);
1387 }
1388
1389 // check for too deep shift value and limit if necessary
1390 if (m_view && shiftX > ((double)maxwidth / 100 * m_view->config()->dynWordWrapAlignIndent())) {
1391 shiftX = 0;
1392 }
1393
1394 // if shiftX > 0, the maxwidth has to adapted
1395 maxwidth -= shiftX;
1396
1397 lineLayout->shiftX = shiftX;
1398 }
1399
1400 height += lineHeight();
1401 }
1402
1403 // will end layout and trigger that we mark the layout as changed
1404 lineLayout->endLayout();
1405}
1406
1407// 1) QString::isRightToLeft() sux
1408// 2) QString::isRightToLeft() is marked as internal (WTF?)
1409// 3) QString::isRightToLeft() does not seem to work on my setup
1410// 4) isStringRightToLeft() should behave much better than QString::isRightToLeft() therefore:
1411// 5) isStringRightToLeft() kicks ass
1413{
1414 // borrowed from QString::updateProperties()
1415 for (auto c : str) {
1416 switch (c.direction()) {
1417 case QChar::DirL:
1418 case QChar::DirLRO:
1419 case QChar::DirLRE:
1420 return false;
1421
1422 case QChar::DirR:
1423 case QChar::DirAL:
1424 case QChar::DirRLO:
1425 case QChar::DirRLE:
1426 return true;
1427
1428 default:
1429 break;
1430 }
1431 }
1432
1433 return false;
1434#if 0
1435 // or should we use the direction of the widget?
1436 QWidget *display = qobject_cast<QWidget *>(view()->parent());
1437 if (!display) {
1438 return false;
1439 }
1440 return display->layoutDirection() == Qt::RightToLeft;
1441#endif
1442}
1443
1444qreal KateRenderer::cursorToX(const KateTextLayout &range, int col, bool returnPastLine) const
1445{
1446 return cursorToX(range, KTextEditor::Cursor(range.line(), col), returnPastLine);
1447}
1448
1449qreal KateRenderer::cursorToX(const KateTextLayout &range, const KTextEditor::Cursor pos, bool returnPastLine) const
1450{
1451 Q_ASSERT(range.isValid());
1452
1453 qreal x = 0;
1454 if (range.lineLayout().width() > 0) {
1455 x = range.lineLayout().cursorToX(pos.column());
1456 }
1457
1458 if (const int over = pos.column() - range.endCol(); returnPastLine && over > 0) {
1459 x += over * spaceWidth();
1460 }
1461
1462 return x;
1463}
1464
1465KTextEditor::Cursor KateRenderer::xToCursor(const KateTextLayout &range, int x, bool returnPastLine) const
1466{
1467 Q_ASSERT(range.isValid());
1468 KTextEditor::Cursor ret(range.line(), range.lineLayout().xToCursor(x));
1469
1470 // Do not wrap to the next line. (bug #423253)
1471 if (range.wrap() && ret.column() >= range.endCol() && range.length() > 0) {
1472 ret.setColumn(range.endCol() - 1);
1473 }
1474 // TODO wrong for RTL lines?
1475 if (returnPastLine && range.endCol(true) == -1 && x > range.width() + range.xOffset()) {
1476 ret.setColumn(ret.column() + round((x - (range.width() + range.xOffset())) / spaceWidth()));
1477 }
1478
1479 return ret;
1480}
1481
1483{
1484 m_caretOverrideColor = color;
1485}
1486
1487void KateRenderer::paintSelection(QPaintDevice *d, int startLine, int xStart, int endLine, int xEnd, int viewWidth, qreal scale)
1488{
1489 if (!d || scale < 0.0) {
1490 return;
1491 }
1492
1493 const int lineHeight = std::max(1, this->lineHeight());
1494 QPainter paint(d);
1495 paint.scale(scale, scale);
1496
1497 // clip out non selected parts of start / end line
1498 {
1499 QRect mainRect(0, 0, d->width(), d->height());
1500 QRegion main(mainRect);
1501 // start line
1502 QRect startRect(0, 0, xStart, lineHeight);
1503 QRegion startRegion(startRect);
1504 // end line
1505 QRect endRect(mainRect.bottomLeft().x() + xEnd, mainRect.bottomRight().y() - lineHeight, mainRect.width() - xEnd, lineHeight);
1506 QRegion drawRegion = main.subtracted(startRegion).subtracted(QRegion(endRect));
1507 paint.setClipRegion(drawRegion);
1508 }
1509
1510 for (int line = startLine; line <= endLine; ++line) {
1511 // get real line, skip if invalid!
1512 if (line < 0 || line >= doc()->lines()) {
1513 continue;
1514 }
1515
1516 // compute layout WITHOUT cache to not poison it + render it
1517 KateLineLayout lineLayout(*this);
1518 lineLayout.setLine(line, -1);
1519 layoutLine(&lineLayout, viewWidth, false /* no layout cache */);
1520 KateRenderer::PaintTextLineFlags flags;
1523 paintTextLine(paint, &lineLayout, 0, 0, QRectF{}, nullptr, flags);
1524
1525 // translate for next line
1526 paint.translate(0, lineHeight * lineLayout.viewLineCount());
1527 }
1528}
QRgb textColor(TextStyle style) const
A class which provides customized text decorations.
Definition attribute.h:51
@ ActivateMouseIn
Activate attribute on mouse in.
Definition attribute.h:246
@ ActivateCaretIn
Activate attribute on caret in.
Definition attribute.h:248
QExplicitlySharedDataPointer< Attribute > Ptr
Shared data pointer for Attribute.
Definition attribute.h:56
The Cursor represents a position in a Document.
Definition cursor.h:75
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
Definition cursor.h:192
void setColumn(int column) noexcept
Set the cursor column to column.
Definition cursor.h:201
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
static constexpr Cursor invalid() noexcept
Returns an invalid cursor.
Definition cursor.h:112
Backend of KTextEditor::Document related public KTextEditor interfaces.
MarkTypes
Predefined mark types.
Definition document.h:1557
virtual void paintInlineNote(const InlineNote &note, QPainter &painter, Qt::LayoutDirection direction) const =0
Paint the note into the line.
Describes an inline note.
Definition inlinenote.h:40
qreal width() const
Returns the width of this note in pixels.
InlineNoteProvider * provider() const
The provider which created this note.
KTextEditor::Cursor position() const
The cursor position of this note.
const Cursor toCursor() const
Convert this clever cursor into a dumb one.
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
static constexpr Range invalid() noexcept
Returns an invalid range.
constexpr bool isValid() const noexcept
Validity check.
constexpr bool contains(Range range) const noexcept
Check whether the this range wholly encompasses range.
KSyntaxHighlighting::Theme theme() const
Get the current active theme of this view.
Internal data container for KTextEditor::InlineNote interface.
void increaseFontSizes(qreal step=1.0) const
Change to a different font (soon to be font set?)
Kate::TextFolding & folding() const
Returns the folding info to which this renderer is bound.
void setShowTabs(bool showTabs)
Set whether a mark should be painted to help identifying tabs.
bool showNonPrintableSpaces() const
KateRenderer(KTextEditor::DocumentPrivate *doc, Kate::TextFolding &folding, KTextEditor::ViewPrivate *view=nullptr)
Style of Caret.
void setIndentWidth(int indentWidth)
Sets the width of the tab.
void setShowSelections(bool showSelections)
Set whether the view's selections should be shown.
const AttributePtr & attribute(uint pos) const
This takes an in index, and returns all the attributes for it.
KTextEditor::caretStyles caretStyle() const
The style of the caret (text cursor) to be painted.
void setDrawCaret(bool drawCaret)
Set whether the caret (text cursor) will be drawn.
static bool isLineRightToLeft(QStringView str)
This is a smaller QString::isRightToLeft().
bool isPrinterFriendly() const
KTextEditor::Cursor xToCursor(const KateTextLayout &range, int x, bool returnPastLine=false) const
Returns the real cursor which is occupied by the specified x value, or that closest to it.
void setPrinterFriendly(bool printerFriendly)
Configure this renderer to paint in a printer-friendly fashion.
bool showIndentLines() const
KTextEditor::ViewPrivate * view() const
Returns the view to which this renderer is bound.
void setShowSpaces(KateDocumentConfig::WhitespaceRendering showSpaces)
Set which spaces should be rendered.
bool showTabs() const
QList< QTextLayout::FormatRange > decorationsForLine(const Kate::TextLine &textLine, int line, bool selectionsOnly=false) const
The ultimate decoration creation function.
KateDocumentConfig::WhitespaceRendering showSpaces() const
bool drawCaret() const
Determine whether the caret (text cursor) will be drawn.
@ SkipDrawLineSelection
Skip drawing the line selection This is useful when we are drawing the draggable pixmap for drag even...
@ SkipDrawFirstInvisibleLineUnderlined
Skip drawing the dashed underline at the start of a folded block of text?
void setShowNonPrintableSpaces(const bool showNonPrintableSpaces)
Set whether box should be drawn around non-printable spaces.
bool showSelections() const
Show the view's selection?
void setTabWidth(int tabWidth)
Sets the width of the tab.
void setCaretOverrideColor(const QColor &color)
Set a brush with which to override drawing of the caret.
void updateConfig()
Configuration.
void setCaretStyle(KTextEditor::caretStyles style)
Set the style of caret to be painted.
KTextEditor::DocumentPrivate * doc() const
Returns the document to which this renderer is bound.
void setShowIndentLines(bool showLines)
Set whether a guide should be painted to help identifying indent lines.
qreal cursorToX(const KateTextLayout &range, int col, bool returnPastLine=false) const
Returns the x position of cursor col on the line range.
void updateAttributes()
update the highlighting attributes (for example after an hl change or after hl config changed)
void layoutLine(KateLineLayout *line, int maxwidth=-1, bool cacheLayout=false) const
Text width & height calculation functions...
void paintSelection(QPaintDevice *d, int startLine, int xStart, int endLine, int xEnd, int viewWidth, qreal scale=1.0)
Paints a range of text into d.
void paintTextLineBackground(QPainter &paint, KateLineLayout *layout, int currentViewLine, int xStart, int xEnd)
Paint the background of a line.
void updateMarkerSize()
Update marker size shown.
void paintTextLine(QPainter &paint, KateLineLayout *range, int xStart, int xEnd, const QRectF &textClipRect=QRectF(), const KTextEditor::Cursor *cursor=nullptr, PaintTextLineFlags flags=PaintTextLineFlags())
This is the ultimate function to perform painting of a text line.
This class represents one visible line of text; with dynamic wrapping, many KateTextLayouts can be ne...
int endCol(bool indicateEOL=false) const
Return the end column of this text line.
Class representing the folding information for a TextBuffer.
Class representing a single text line.
const QString & text() const
Accessor to the text contained in this line.
const QList< Attribute > & attributesList() const
Accessor to attributes.
int indentDepth(int tabWidth) const
Returns the indentation depth with each tab expanded into tabWidth characters.
int length() const
Returns the line's length.
int lastChar() const
Returns the position of the last non-whitespace character.
int nextNonSpaceChar(int pos) const
Find the position of the next char that is not a space.
Class representing a 'clever' text range.
qreal zDepth() const override
Gets the current Z-depth of this range.
const KTextEditor::MovingCursor & end() const override
Retrieve end cursor of this range, read-only.
const KTextEditor::Attribute::Ptr & attribute() const override
Gets the active Attribute for this range.
const KTextEditor::MovingCursor & start() const override
Retrieve start cursor of this range, read-only.
Q_SCRIPTABLE QString start(QString train="")
Q_SCRIPTABLE Q_NOREPLY void start()
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
bool isSpace(char32_t ucs4)
int alpha() const const
int blue() const const
int green() const const
bool isValid() const const
int red() const const
void setAlpha(int alpha)
void setAlphaF(float alpha)
void setRgb(QRgb rgb)
QFlags< T > & setFlag(Enum flag, bool on)
bool testFlag(Enum flag) const const
AbsoluteSpacing
qreal pointSizeF() const const
void setPointSizeF(qreal pointSize)
qreal horizontalAdvance(QChar ch) const const
qreal strikeOutPos() const const
void append(QList< T > &&value)
iterator begin()
iterator end()
iterator erase(const_iterator begin, const_iterator end)
int height() const const
int width() const const
bool hasFeature(PaintEngineFeatures feature) const const
CompositionMode compositionMode() const const
void drawLine(const QLine &line)
void drawLines(const QLine *lines, int lineCount)
void drawPoints(const QPoint *points, int pointCount)
void fillPath(const QPainterPath &path, const QBrush &brush)
void fillRect(const QRect &rectangle, QGradient::Preset preset)
QPaintEngine * paintEngine() const const
const QPen & pen() const const
RenderHints renderHints() const const
void restore()
void save()
void scale(qreal sx, qreal sy)
void setClipRegion(const QRegion &region, Qt::ClipOperation operation)
void setCompositionMode(CompositionMode mode)
void setPen(Qt::PenStyle style)
void setRenderHint(RenderHint hint, bool on)
void setRenderHints(RenderHints hints, bool on)
void translate(const QPoint &offset)
void addRect(const QRectF &rectangle)
QBrush brush() const const
void setColor(const QColor &color)
void setCosmetic(bool cosmetic)
void setDashOffset(qreal offset)
void setDashPattern(const QList< qreal > &pattern)
void setStyle(Qt::PenStyle style)
void setWidth(int width)
int x() const const
int y() const const
bool isNull() const const
qreal x() const const
QPoint bottomLeft() const const
QPoint bottomRight() const const
int width() const const
QRegion subtracted(const QRegion &r) const const
QRegularExpressionMatchIterator globalMatch(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
qsizetype capturedStart(QStringView name) const const
QRegularExpressionMatch next()
bool contains(const QSet< T > &other) const const
bool empty() const const
const QChar at(qsizetype position) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
qsizetype length() const const
AlignRight
RightToLeft
RoundCap
DashLine
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
void setFontLetterSpacing(qreal spacing)
void setFontLetterSpacingType(QFont::SpacingType letterSpacingType)
void beginLayout()
QTextLine createLine()
void draw(QPainter *p, const QPointF &pos, const QList< FormatRange > &selections, const QRectF &clip) const const
QList< FormatRange > formats() const const
int lineCount() const const
QTextLine lineForTextPosition(int pos) const const
QPointF position() const const
void setCacheEnabled(bool enable)
void setFont(const QFont &font)
void setFormats(const QList< FormatRange > &formats)
void setText(const QString &string)
void setTextOption(const QTextOption &option)
QString text() const const
const QTextOption & textOption() const const
qreal ascent() const const
qreal cursorToX(int *cursorPos, Edge edge) const const
bool isValid() const const
int lineNumber() const const
void setLeadingIncluded(bool included)
void setLineWidth(qreal width)
void setPosition(const QPointF &pos)
qreal width() const const
int xToCursor(qreal x, CursorPosition cpos) const const
qreal y() const const
Qt::Alignment alignment() const const
Flags flags() const const
void setAlignment(Qt::Alignment alignment)
void setFlags(Flags flags)
void setTabStopDistance(qreal tabStopDistance)
void setTextDirection(Qt::LayoutDirection direction)
void setWrapMode(WrapMode mode)
Qt::LayoutDirection textDirection() const const
const T * constData() const const
bool isEmpty() const const
reverse_iterator rbegin()
reverse_iterator rend()
qsizetype size() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:52:52 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.