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

KDE's Doxygen guidelines are available online.