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

KDE's Doxygen guidelines are available online.