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{0000}-\\x{0008}\\x{000A}-\\x{001F}\\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 bool skipPrimary = false;
951 if (mIt != secCursors.end() && mIt->cursor().line() == range->line()) {
953 auto primaryCursor = *cursor;
954 for (; mIt != secCursors.end(); ++mIt) {
955 auto cursor = mIt->cursor();
956 skipPrimary = skipPrimary || cursor == primaryCursor;
957 if (cursor == last) {
958 continue;
959 }
960 last = cursor;
961 if (cursor.line() == range->line()) {
962 paintCaret(cursor, range, paint, xStart, xEnd);
963 } else {
964 break;
965 }
966 }
967 }
968 if (!skipPrimary) {
969 paintCaret(*cursor, range, paint, xStart, xEnd);
970 }
971 }
972 }
973
974 // show word wrap marker if desirable
975 if ((!isPrinterFriendly()) && config()->wordWrapMarker()) {
976 const QPainter::RenderHints backupRenderHints = paint.renderHints();
977 paint.setPen(config()->wordWrapMarkerColor());
978 int _x = qreal(m_doc->config()->wordWrapAt()) * fm.horizontalAdvance(QLatin1Char('x')) - xStart;
979 paint.drawLine(_x, 0, _x, lineHeight());
980 paint.setRenderHints(backupRenderHints);
981 }
982
983 // Draw inline notes
984 if (!isPrinterFriendly()) {
985 const auto inlineNotes = m_view->inlineNotes(range->line());
986 for (const auto &inlineNoteData : inlineNotes) {
987 KTextEditor::InlineNote inlineNote(inlineNoteData);
988 const int column = inlineNote.position().column();
989 const int viewLine = range->viewLineForColumn(column);
990 // We only consider a line "rtl" if dynamic wrap is enabled. If it is disabled, our
991 // text is always on the left side of the view
992 const auto dir = range->layout()->textOption().textDirection();
993 const bool isRTL = dir == Qt::RightToLeft && m_view->dynWordWrap();
994
995 // Determine the position where to paint the note.
996 // We start by getting the x coordinate of cursor placed to the column.
997 // If the text is ltr or rtl + dyn wrap, get the X from column
998 qreal x;
999 if (dir == Qt::LeftToRight || (dir == Qt::RightToLeft && m_view->dynWordWrap())) {
1000 x = range->viewLine(viewLine).lineLayout().cursorToX(column) - xStart;
1001 } else /* rtl + dynWordWrap == false */ {
1002 // if text is rtl and dynamic wrap is false, the x offsets are in the opposite
1003 // direction i.e., [0] == biggest offset, [1] = next
1004 x = range->viewLine(viewLine).lineLayout().cursorToX(range->length() - column) - xStart;
1005 }
1006 int textLength = range->length();
1007 if (column == 0 || column < textLength) {
1008 // If the note is inside text or at the beginning, then there is a hole in the text where the
1009 // note should be painted and the cursor gets placed at the right side of it. So we have to
1010 // subtract the width of the note to get to left side of the hole.
1011 x -= inlineNote.width();
1012 } else {
1013 // If the note is outside the text, then the X coordinate is located at the end of the line.
1014 // Add appropriate amount of spaces to reach the required column.
1015 const auto spaceToAdd = spaceWidth() * (column - textLength);
1016 x += isRTL ? -spaceToAdd : spaceToAdd;
1017 }
1018
1019 qreal y = lineHeight() * viewLine;
1020
1021 // Paint the note
1022 paint.save();
1023 paint.translate(x, y);
1024 inlineNote.provider()->paintInlineNote(inlineNote, paint, isRTL ? Qt::RightToLeft : Qt::LeftToRight);
1025 paint.restore();
1026 }
1027 }
1028}
1029
1030static void drawCursor(const QTextLayout &layout, QPainter *p, const QPointF &pos, int cursorPosition, int width, const int height)
1031{
1032 cursorPosition = qBound(0, cursorPosition, layout.text().length());
1033 const QTextLine l = layout.lineForTextPosition(cursorPosition);
1034 Q_ASSERT(l.isValid());
1035 if (!l.isValid()) {
1036 return;
1037 }
1038 const QPainter::CompositionMode origCompositionMode = p->compositionMode();
1041 }
1042
1043 const QPointF position = pos + layout.position();
1044 const qreal x = position.x() + l.cursorToX(cursorPosition);
1045 const qreal y = l.lineNumber() * height;
1046 p->fillRect(QRectF(x, y, (qreal)width, (qreal)height), p->pen().brush());
1047 p->setCompositionMode(origCompositionMode);
1048}
1049
1050void KateRenderer::paintCaret(KTextEditor::Cursor cursor, KateLineLayout *range, QPainter &paint, int xStart, int xEnd)
1051{
1052 if (range->includesCursor(cursor)) {
1053 int caretWidth;
1054 int lineWidth = 2;
1055 QColor color;
1056 QTextLine line = range->layout()->lineForTextPosition(qMin(cursor.column(), range->length()));
1057
1058 // Determine the caret's style
1059 KTextEditor::caretStyles style = caretStyle();
1060
1061 // Make the caret the desired width
1062 if (style == KTextEditor::caretStyles::Line) {
1063 caretWidth = lineWidth;
1064 } else if (line.isValid() && cursor.column() < range->length()) {
1065 caretWidth = int(line.cursorToX(cursor.column() + 1) - line.cursorToX(cursor.column()));
1066 if (caretWidth < 0) {
1067 caretWidth = -caretWidth;
1068 }
1069 } else {
1070 caretWidth = spaceWidth();
1071 }
1072
1073 // Determine the color
1074 if (m_caretOverrideColor.isValid()) {
1075 // Could actually use the real highlighting system for this...
1076 // would be slower, but more accurate for corner cases
1077 color = m_caretOverrideColor;
1078 } else {
1079 // search for the FormatRange that includes the cursor
1080 const auto formatRanges = range->layout()->formats();
1081 for (const QTextLayout::FormatRange &r : formatRanges) {
1082 if ((r.start <= cursor.column()) && ((r.start + r.length) > cursor.column())) {
1083 // check for Qt::NoBrush, as the returned color is black() and no invalid QColor
1084 QBrush foregroundBrush = r.format.foreground();
1085 if (foregroundBrush != Qt::NoBrush) {
1086 color = r.format.foreground().color();
1087 }
1088 break;
1089 }
1090 }
1091 // still no color found, fall back to default style
1092 if (!color.isValid()) {
1093 color = attribute(KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color();
1094 }
1095 }
1096
1097 paint.save();
1098 switch (style) {
1099 case KTextEditor::caretStyles::Line:
1100 paint.setPen(QPen(color, caretWidth));
1101 break;
1102 case KTextEditor::caretStyles::Block:
1103 // use a gray caret so it's possible to see the character
1104 color.setAlpha(128);
1105 paint.setPen(QPen(color, caretWidth));
1106 break;
1107 case KTextEditor::caretStyles::Underline:
1108 break;
1109 case KTextEditor::caretStyles::Half:
1110 color.setAlpha(128);
1111 paint.setPen(QPen(color, caretWidth));
1112 break;
1113 }
1114
1115 if (cursor.column() <= range->length()) {
1116 // Ensure correct cursor placement for RTL text
1117 if (range->layout()->textOption().textDirection() == Qt::RightToLeft) {
1118 xStart += caretWidth;
1119 }
1120 qreal width = 0;
1121 if (cursor.column() < range->length()) {
1122 const auto inlineNotes = m_view->inlineNotes(range->line());
1123 for (const auto &inlineNoteData : inlineNotes) {
1124 KTextEditor::InlineNote inlineNote(inlineNoteData);
1125 if (inlineNote.position().column() == cursor.column()) {
1126 width = inlineNote.width() + (caretStyle() == KTextEditor::caretStyles::Line ? 2.0 : 0.0);
1127 }
1128 }
1129 }
1130 drawCursor(*range->layout(), &paint, QPoint(-xStart - width, 0), cursor.column(), caretWidth, lineHeight());
1131 } else {
1132 // Off the end of the line... must be block mode. Draw the caret ourselves.
1133 const KateTextLayout &lastLine = range->viewLine(range->viewLineCount() - 1);
1134 int x = cursorToX(lastLine, KTextEditor::Cursor(range->line(), cursor.column()), true);
1135 if ((x >= xStart) && (x <= xEnd)) {
1136 paint.fillRect(x - xStart, (int)lastLine.lineLayout().y(), caretWidth, lineHeight(), color);
1137 }
1138 }
1139
1140 paint.restore();
1141 }
1142}
1143
1144uint KateRenderer::fontHeight() const
1145{
1146 return m_fontHeight;
1147}
1148
1149uint KateRenderer::documentHeight() const
1150{
1151 return m_doc->lines() * lineHeight();
1152}
1153
1154int KateRenderer::lineHeight() const
1155{
1156 return fontHeight();
1157}
1158
1159bool KateRenderer::getSelectionBounds(int line, int lineLength, int &start, int &end) const
1160{
1161 bool hasSel = false;
1162
1163 if (m_view->selection() && !m_view->blockSelection()) {
1164 if (m_view->lineIsSelection(line)) {
1165 start = m_view->selectionRange().start().column();
1166 end = m_view->selectionRange().end().column();
1167 hasSel = true;
1168 } else if (line == m_view->selectionRange().start().line()) {
1169 start = m_view->selectionRange().start().column();
1170 end = lineLength;
1171 hasSel = true;
1172 } else if (m_view->selectionRange().containsLine(line)) {
1173 start = 0;
1174 end = lineLength;
1175 hasSel = true;
1176 } else if (line == m_view->selectionRange().end().line()) {
1177 start = 0;
1178 end = m_view->selectionRange().end().column();
1179 hasSel = true;
1180 }
1181 } else if (m_view->lineHasSelected(line)) {
1182 start = m_view->selectionRange().start().column();
1183 end = m_view->selectionRange().end().column();
1184 hasSel = true;
1185 }
1186
1187 if (start > end) {
1188 int temp = end;
1189 end = start;
1190 start = temp;
1191 }
1192
1193 return hasSel;
1194}
1195
1197{
1198 // update the attribute list pointer
1200
1201 // update font height, do this before we update the view!
1202 updateFontHeight();
1203
1204 // trigger view update, if any!
1205 if (m_view) {
1206 m_view->updateRendererConfig();
1207 }
1208}
1209
1210bool KateRenderer::hasCustomLineHeight() const
1211{
1212 return !qFuzzyCompare(config()->lineHeightMultiplier(), 1.0);
1213}
1214
1215void KateRenderer::updateFontHeight()
1216{
1217 // cache font + metrics
1218 m_font = config()->baseFont();
1219 m_fontMetrics = QFontMetricsF(m_font);
1220
1221 // ensure minimal height of one pixel to not fall in the div by 0 trap somewhere
1222 //
1223 // use a line spacing that matches the code in qt to layout/paint text
1224 //
1225 // see bug 403868
1226 // 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:
1227 //
1228 // qreal height = maxY + fontHeight - minY;
1229 //
1230 // with fontHeight:
1231 //
1232 // qreal fontHeight = font.ascent() + font.descent();
1233 m_fontHeight = qMax(1, qCeil(m_fontMetrics.ascent() + m_fontMetrics.descent()));
1234 m_fontAscent = m_fontMetrics.ascent();
1235
1236 if (hasCustomLineHeight()) {
1237 const auto oldFontHeight = m_fontHeight;
1238 const qreal newFontHeight = qreal(m_fontHeight) * config()->lineHeightMultiplier();
1239 m_fontHeight = newFontHeight;
1240
1241 qreal diff = std::abs(oldFontHeight - newFontHeight);
1242 m_fontAscent += (diff / 2);
1243 }
1244}
1245
1247{
1248 // marker size will be calculated over the value defined
1249 // on dialog
1250
1251 m_markerSize = spaceWidth() / (3.5 - (m_doc->config()->markerSize() * 0.5));
1252}
1253
1254qreal KateRenderer::spaceWidth() const
1255{
1256 return m_fontMetrics.horizontalAdvance(spaceChar);
1257}
1258
1259void KateRenderer::layoutLine(KateLineLayout *lineLayout, int maxwidth, bool cacheLayout) const
1260{
1261 // if maxwidth == -1 we have no wrap
1262
1263 Kate::TextLine textLine = lineLayout->textLine();
1264
1265 QTextLayout *l = lineLayout->layout();
1266 if (!l) {
1267 l = new QTextLayout(textLine.text(), m_font);
1268 } else {
1269 l->setText(textLine.text());
1270 l->setFont(m_font);
1271 }
1272
1273 l->setCacheEnabled(cacheLayout);
1274
1275 // Initial setup of the QTextLayout.
1276
1277 // Tab width
1278 QTextOption opt;
1280 opt.setTabStopDistance(m_tabWidth * m_fontMetrics.horizontalAdvance(spaceChar));
1281 if (m_view && m_view->config()->dynWrapAnywhere()) {
1283 } else {
1285 }
1286
1287 // Find the first strong character in the string.
1288 // If it is an RTL character, set the base layout direction of the string to RTL.
1289 //
1290 // See https://www.unicode.org/reports/tr9/#The_Paragraph_Level (Sections P2 & P3).
1291 // Qt's text renderer ("scribe") version 4.2 assumes a "higher-level protocol"
1292 // (such as KatePart) will specify the paragraph level, so it does not apply P2 & P3
1293 // by itself. If this ever change in Qt, the next code block could be removed.
1294 // -----
1295 // Only force RTL direction if dynWordWrap is on. Otherwise the view has infinite width
1296 // and the lines will never be forced RTL no matter what direction we set. The layout
1297 // can't force a line to the right if it doesn't know where the "right" is
1298 if (isLineRightToLeft(textLine.text()) || (view()->dynWordWrap() && view()->forceRTLDirection())) {
1301 // Must turn off this flag otherwise cursor placement
1302 // is totally broken.
1303 if (view()->config()->dynWordWrap()) {
1304 auto flags = opt.flags();
1305 flags.setFlag(QTextOption::IncludeTrailingSpaces, false);
1306 opt.setFlags(flags);
1307 }
1308 } else {
1311 }
1312
1313 l->setTextOption(opt);
1314
1315 // Syntax highlighting, inbuilt and arbitrary
1316 QList<QTextLayout::FormatRange> decorations = decorationsForLine(textLine, lineLayout->line());
1317
1318 // Qt works badly if you have RTL text and formats set on that text.
1319 // It will shape the text according to the given format ranges which
1320 // produces incorrect results as a letter in RTL can have a different
1321 // shape depending upon where in the word it resides. The resulting output
1322 // looks like: وقار vs وق ار, i.e, the ligature قا is broken into ق ا which
1323 // is really bad for readability
1324 if (opt.textDirection() == Qt::RightToLeft) {
1325 // We can fix this to a large extent here by using QGlyphRun etc, but for now
1326 // we only fix this for formats which have a background color and a foreground
1327 // color that is same as "dsNormal". Reasoning is that, it is unlikely that RTL
1328 // text will have a lot of cases where you have partially colored ligatures. BG
1329 // formats are different, you can easily have a format that covers a ligature partially
1330 // as a result of "Search" or "multiple cursor selection"
1332 decorations.erase(std::remove_if(decorations.begin(),
1333 decorations.end(),
1334 [c](const QTextLayout::FormatRange &fr) {
1335 return fr.format.hasProperty(QTextFormat::BackgroundBrush)
1336 && (fr.format.property(QTextFormat::ForegroundBrush).value<QBrush>().color() == c
1337 || fr.format.foreground() == Qt::NoBrush);
1338 }),
1339 decorations.end());
1340 }
1341
1342 int firstLineOffset = 0;
1343
1344 if (!isPrinterFriendly() && m_view) {
1345 const auto inlineNotes = m_view->inlineNotes(lineLayout->line());
1346 for (const KateInlineNoteData &noteData : inlineNotes) {
1347 const KTextEditor::InlineNote inlineNote(noteData);
1348 const int column = inlineNote.position().column();
1349 int width = inlineNote.width();
1350
1351 // Make space for every inline note.
1352 // If it is on column 0 (at the beginning of the line), we must offset the first line.
1353 // If it is inside the text, we use absolute letter spacing to create space for it between the two letters.
1354 // If it is outside of the text, we don't have to make space for it.
1355 if (column == 0) {
1356 firstLineOffset = width;
1357 } else if (column < l->text().length()) {
1358 QTextCharFormat text_char_format;
1359 const qreal caretWidth = caretStyle() == KTextEditor::caretStyles::Line ? 2.0 : 0.0;
1360 text_char_format.setFontLetterSpacing(width + caretWidth);
1362 decorations.append(QTextLayout::FormatRange{.start = column - 1, .length = 1, .format = text_char_format});
1363 }
1364 }
1365 }
1366 l->setFormats(decorations);
1367
1368 // Begin layouting
1369 l->beginLayout();
1370
1371 int height = 0;
1372 int shiftX = 0;
1373
1374 bool needShiftX = (maxwidth != -1) && m_view && (m_view->config()->dynWordWrapAlignIndent() > 0);
1375
1376 while (true) {
1377 QTextLine line = l->createLine();
1378 if (!line.isValid()) {
1379 break;
1380 }
1381
1382 if (maxwidth > 0) {
1383 line.setLineWidth(maxwidth);
1384 } else {
1385 line.setLineWidth(INT_MAX);
1386 }
1387
1388 // we include the leading, this must match the ::updateFontHeight code!
1389 line.setLeadingIncluded(true);
1390
1391 line.setPosition(QPoint(line.lineNumber() ? shiftX : firstLineOffset, height - line.ascent() + m_fontAscent));
1392
1393 if (needShiftX && line.width() > 0) {
1394 needShiftX = false;
1395 // Determine x offset for subsequent-lines-of-paragraph indenting
1396 int pos = textLine.nextNonSpaceChar(0);
1397
1398 if (pos > 0) {
1399 shiftX = (int)line.cursorToX(pos);
1400 }
1401
1402 // check for too deep shift value and limit if necessary
1403 if (m_view && shiftX > ((double)maxwidth / 100 * m_view->config()->dynWordWrapAlignIndent())) {
1404 shiftX = 0;
1405 }
1406
1407 // if shiftX > 0, the maxwidth has to adapted
1408 maxwidth -= shiftX;
1409
1410 lineLayout->shiftX = shiftX;
1411 }
1412
1413 height += lineHeight();
1414 }
1415
1416 l->endLayout();
1417
1418 lineLayout->setLayout(l);
1419}
1420
1421// 1) QString::isRightToLeft() sux
1422// 2) QString::isRightToLeft() is marked as internal (WTF?)
1423// 3) QString::isRightToLeft() does not seem to work on my setup
1424// 4) isStringRightToLeft() should behave much better than QString::isRightToLeft() therefore:
1425// 5) isStringRightToLeft() kicks ass
1427{
1428 // borrowed from QString::updateProperties()
1429 for (auto c : str) {
1430 switch (c.direction()) {
1431 case QChar::DirL:
1432 case QChar::DirLRO:
1433 case QChar::DirLRE:
1434 return false;
1435
1436 case QChar::DirR:
1437 case QChar::DirAL:
1438 case QChar::DirRLO:
1439 case QChar::DirRLE:
1440 return true;
1441
1442 default:
1443 break;
1444 }
1445 }
1446
1447 return false;
1448#if 0
1449 // or should we use the direction of the widget?
1450 QWidget *display = qobject_cast<QWidget *>(view()->parent());
1451 if (!display) {
1452 return false;
1453 }
1454 return display->layoutDirection() == Qt::RightToLeft;
1455#endif
1456}
1457
1458int KateRenderer::cursorToX(const KateTextLayout &range, int col, bool returnPastLine) const
1459{
1460 return cursorToX(range, KTextEditor::Cursor(range.line(), col), returnPastLine);
1461}
1462
1463int KateRenderer::cursorToX(const KateTextLayout &range, const KTextEditor::Cursor pos, bool returnPastLine) const
1464{
1465 Q_ASSERT(range.isValid());
1466
1467 int x;
1468 if (range.lineLayout().width() > 0) {
1469 x = (int)range.lineLayout().cursorToX(pos.column());
1470 } else {
1471 x = 0;
1472 }
1473
1474 int over = pos.column() - range.endCol();
1475 if (returnPastLine && over > 0) {
1476 x += over * spaceWidth();
1477 }
1478
1479 return x;
1480}
1481
1482KTextEditor::Cursor KateRenderer::xToCursor(const KateTextLayout &range, int x, bool returnPastLine) const
1483{
1484 Q_ASSERT(range.isValid());
1485 KTextEditor::Cursor ret(range.line(), range.lineLayout().xToCursor(x));
1486
1487 // Do not wrap to the next line. (bug #423253)
1488 if (range.wrap() && ret.column() >= range.endCol() && range.length() > 0) {
1489 ret.setColumn(range.endCol() - 1);
1490 }
1491 // TODO wrong for RTL lines?
1492 if (returnPastLine && range.endCol(true) == -1 && x > range.width() + range.xOffset()) {
1493 ret.setColumn(ret.column() + round((x - (range.width() + range.xOffset())) / spaceWidth()));
1494 }
1495
1496 return ret;
1497}
1498
1500{
1501 m_caretOverrideColor = color;
1502}
1503
1504void KateRenderer::paintSelection(QPaintDevice *d, int startLine, int xStart, int endLine, int xEnd, int viewWidth, qreal scale)
1505{
1506 if (!d || scale < 0.0) {
1507 return;
1508 }
1509
1510 const int lineHeight = std::max(1, this->lineHeight());
1511 QPainter paint(d);
1512 paint.scale(scale, scale);
1513
1514 // clip out non selected parts of start / end line
1515 {
1516 QRect mainRect(0, 0, d->width(), d->height());
1517 QRegion main(mainRect);
1518 // start line
1519 QRect startRect(0, 0, xStart, lineHeight);
1520 QRegion startRegion(startRect);
1521 // end line
1522 QRect endRect(mainRect.bottomLeft().x() + xEnd, mainRect.bottomRight().y() - lineHeight, mainRect.width() - xEnd, lineHeight);
1523 QRegion drawRegion = main.subtracted(startRegion).subtracted(QRegion(endRect));
1524 paint.setClipRegion(drawRegion);
1525 }
1526
1527 for (int line = startLine; line <= endLine; ++line) {
1528 // get real line, skip if invalid!
1529 if (line < 0 || line >= doc()->lines()) {
1530 continue;
1531 }
1532
1533 // compute layout WITHOUT cache to not poison it + render it
1534 KateLineLayout lineLayout(*this);
1535 lineLayout.setLine(line, -1);
1536 layoutLine(&lineLayout, viewWidth, false /* no layout cache */);
1540 paintTextLine(paint, &lineLayout, 0, 0, QRectF{}, nullptr, flags);
1541
1542 // translate for next line
1543 paint.translate(0, lineHeight * lineLayout.viewLineCount());
1544 }
1545}
QRgb textColor(TextStyle style) const
A class which provides customized text decorations.
Definition attribute.h:51
QExplicitlySharedDataPointer< Attribute > Ptr
Shared data pointer for Attribute.
Definition attribute.h:56
@ ActivateMouseIn
Activate attribute on mouse in.
Definition attribute.h:246
@ ActivateCaretIn
Activate attribute on caret in.
Definition attribute.h:248
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.
KateBuffer & buffer()
Get access to buffer of this document.
int lines() const override
Get the count of lines of the document.
KateDocumentConfig * config()
Configuration.
uint mark(int line) override
Get all marks set on the line.
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 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 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 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.
QList< TextRange * > rangesForLine(int line, KTextEditor::View *view, bool rangesWithAttributeOnly) const
Return the ranges which affect the given 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()
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 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
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 Fri Nov 29 2024 11:53:44 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.