TerminalPainter.cpp 24.1 KB
Newer Older
1
/*
2
3
4
5
6
    SPDX-FileCopyrightText: 2020-2020 Gustavo Carneiro <gcarneiroa@hotmail.com>
    SPDX-FileCopyrightText: 2007-2008 Robert Knight <robertknight@gmail.com>
    SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de>

    SPDX-License-Identifier: GPL-2.0-or-later
7
8
9
10
11
12
13
*/

// Own
#include "TerminalPainter.hpp"

// Konsole
#include "TerminalDisplay.h"
14
#include "TerminalScrollBar.hpp"
15
#include "ExtendedCharTable.h"
16
#include "../characters/LineBlockCharacters.h"
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

// Qt
#include <QRect>
#include <QColor>
#include <QRegion>
#include <QPainter>
#include <QString>
#include <QVector>
#include <QChar>
#include <QMatrix>
#include <QTransform>
#include <QTimer>
#include <QPen>
#include <QDebug>

Gustavo Carneiro's avatar
Gustavo Carneiro committed
32
33
// we use this to force QPainter to display text in LTR mode
// more information can be found in: https://unicode.org/reports/tr9/
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const QChar LTR_OVERRIDE_CHAR(0x202D);

namespace Konsole
{

    TerminalPainter::TerminalPainter(TerminalDisplay *display)
        : _display(display)
    {}

    static inline bool isLineCharString(const QString &string)
    {
        if (string.length() == 0) {
            return false;
        }
        return LineBlockCharacters::canDraw(string.at(0).unicode());
    }

    static int baseCodePoint(const Character &ch)
    {
        if (ch.rendition & RE_EXTENDED_CHAR) {
            ushort extendedCharLength = 0;
            const uint *chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength);
Kurt Hindenburg's avatar
Kurt Hindenburg committed
56
            // FIXME: Coverity-Dereferencing chars, which is known to be nullptr
57
58
59
60
61
62
            return chars[0];
        } else {
            return ch.character;
        }
    }

63
    void TerminalPainter::drawContents(QPainter &paint, const QRect &rect, bool printerFriendly)
64
65
66
67
68
69
    {
        const int numberOfColumns = _display->_usedColumns;
        QVector<uint> univec;
        univec.reserve(numberOfColumns);
        for (int y = rect.y(); y <= rect.bottom(); y++) {
            int x = rect.x();
70
            if ((_display->_image[_display->loc(rect.x(), y)].character == 0u) && (x != 0)) {
71
72
73
74
75
76
77
78
79
80
81
82
                x--; // Search for start of multi-column character
            }
            for (; x <= rect.right(); x++) {
                int len = 1;
                int p = 0;

                // reset our buffer to the number of columns
                int bufferSize = numberOfColumns;
                univec.resize(bufferSize);
                uint *disstrU = univec.data();

                // is this a single character or a sequence of characters ?
83
                if ((_display->_image[_display->loc(x, y)].rendition & RE_EXTENDED_CHAR) != 0) {
84
85
                    // sequence of characters
                    ushort extendedCharLength = 0;
86
                    const uint *chars = ExtendedCharTable::instance.lookupExtendedChar(_display->_image[_display->loc(x, y)].character, extendedCharLength);
87
88
89
90
91
92
93
94
95
96
97
                    if (chars != nullptr) {
                        Q_ASSERT(extendedCharLength > 1);
                        bufferSize += extendedCharLength - 1;
                        univec.resize(bufferSize);
                        disstrU = univec.data();
                        for (int index = 0; index < extendedCharLength; index++) {
                            Q_ASSERT(p < bufferSize);
                            disstrU[p++] = chars[index];
                        }
                    }
                } else {
98
                    const uint c = _display->_image[_display->loc(x, y)].character;
99
100
101
102
103
104
                    if (c != 0u) {
                        Q_ASSERT(p < bufferSize);
                        disstrU[p++] = c;
                    }
                }

105
106
107
108
109
110
                const bool lineDraw = LineBlockCharacters::canDraw(_display->_image[_display->loc(x, y)].character);
                const bool doubleWidth = (_display->_image[qMin(_display->loc(x, y) + 1, _display->_imageSize - 1)].character == 0);
                const CharacterColor currentForeground = _display->_image[_display->loc(x, y)].foregroundColor;
                const CharacterColor currentBackground = _display->_image[_display->loc(x, y)].backgroundColor;
                const RenditionFlags currentRendition = _display->_image[_display->loc(x, y)].rendition;
                const QChar::Script currentScript = QChar::script(baseCodePoint(_display->_image[_display->loc(x, y)]));
111
112
113

                const auto isInsideDrawArea = [&](int column) { return column <= rect.right(); };
                const auto hasSameColors = [&](int column) {
114
115
                    return _display->_image[_display->loc(column, y)].foregroundColor == currentForeground
                        && _display->_image[_display->loc(column, y)].backgroundColor == currentBackground;
116
117
                };
                const auto hasSameRendition = [&](int column) {
118
                    return (_display->_image[_display->loc(column, y)].rendition & ~RE_EXTENDED_CHAR)
119
120
121
                        == (currentRendition & ~RE_EXTENDED_CHAR);
                };
                const auto hasSameWidth = [&](int column) {
122
                    const int characterLoc = qMin(_display->loc(column, y) + 1, _display->_imageSize - 1);
123
124
125
                    return (_display->_image[characterLoc].character == 0) == doubleWidth;
                };
                const auto hasSameLineDrawStatus = [&](int column) {
126
                    return LineBlockCharacters::canDraw(_display->_image[_display->loc(column, y)].character)
127
128
129
                        == lineDraw;
                };
                const auto isSameScript = [&](int column) {
130
                    const QChar::Script script = QChar::script(baseCodePoint(_display->_image[_display->loc(column, y)]));
131
132
133
134
135
136
137
                    if (currentScript == QChar::Script_Common || script == QChar::Script_Common
                        || currentScript == QChar::Script_Inherited || script == QChar::Script_Inherited) {
                        return true;
                    }
                    return currentScript == script;
                };
                const auto canBeGrouped = [&](int column) {
138
139
                    return _display->_image[_display->loc(column, y)].character <= 0x7e
                            || (_display->_image[_display->loc(column, y)].rendition & RE_EXTENDED_CHAR)
140
141
142
143
144
145
146
147
                            || (_display->_bidiEnabled && !doubleWidth);
                };

                if (canBeGrouped(x)) {
                    while (isInsideDrawArea(x + len) && hasSameColors(x + len)
                            && hasSameRendition(x + len) && hasSameWidth(x + len)
                            && hasSameLineDrawStatus(x + len) && isSameScript(x + len)
                            && canBeGrouped(x + len)) {
148
149
                        const uint c = _display->_image[_display->loc(x + len, y)].character;
                        if ((_display->_image[_display->loc(x + len, y)].rendition & RE_EXTENDED_CHAR) != 0) {
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
                            // sequence of characters
                            ushort extendedCharLength = 0;
                            const uint *chars = ExtendedCharTable::instance.lookupExtendedChar(c, extendedCharLength);
                            if (chars != nullptr) {
                                Q_ASSERT(extendedCharLength > 1);
                                bufferSize += extendedCharLength - 1;
                                univec.resize(bufferSize);
                                disstrU = univec.data();
                                for (int index = 0; index < extendedCharLength; index++) {
                                    Q_ASSERT(p < bufferSize);
                                    disstrU[p++] = chars[index];
                                }
                            }
                        } else {
                            // single character
                            if (c != 0u) {
                                Q_ASSERT(p < bufferSize);
                                disstrU[p++] = c;
                            }
                        }

                        if (doubleWidth) {
                            len++;
                        }
                        len++;
                    }
                } else {
                    // Group spaces following any non-wide character with the character. This allows for
                    // rendering ambiguous characters with wide glyphs without clipping them.
                    while (!doubleWidth && isInsideDrawArea(x + len)
180
                            && _display->_image[_display->loc(x + len, y)].character == ' ' && hasSameColors(x + len)
181
182
183
184
185
                            && hasSameRendition(x + len)) {
                        // disstrU intentionally not modified - trailing spaces are meaningless
                        len++;
                    }
                }
186
                if ((x + len < _display->_usedColumns) && (_display->_image[_display->loc(x + len, y)].character == 0u)) {
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
                    len++; // Adjust for trailing part of multi-column character
                }

                const bool save__fixedFont = _display->_fixedFont;
                if (lineDraw) {
                    _display->_fixedFont = false;
                }
                if (doubleWidth) {
                    _display->_fixedFont = false;
                }
                univec.resize(p);

                QMatrix textScale;

                if (y < _display->_lineProperties.size()) {
                    if ((_display->_lineProperties[y] & LINE_DOUBLEWIDTH) != 0) {
                        textScale.scale(2, 1);
                    }

                    if ((_display->_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) {
                        textScale.scale(1, 2);
                    }
                }

Kurt Hindenburg's avatar
Kurt Hindenburg committed
211
                // Apply text scaling matrix
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
                paint.setWorldTransform(QTransform(textScale), true);

                // Calculate the area in which the text will be drawn
                QRect textArea = QRect(_display->contentRect().left() + _display->contentsRect().left() + _display->fontWidth() * x,
                                        _display->contentRect().top() + _display->contentsRect().top() + _display->fontHeight() * y,
                                        _display->fontWidth() * len,
                                        _display->fontHeight());

                //move the calculated area to take account of scaling applied to the painter.
                //the position of the area from the origin (0,0) is scaled
                //by the opposite of whatever
                //transformation has been applied to the painter.  this ensures that
                //painting does actually start from textArea.topLeft()
                //(instead of textArea.topLeft() * painter-scale)
                textArea.moveTopLeft(textScale.inverted().map(textArea.topLeft()));

                QString unistr = QString::fromUcs4(univec.data(), univec.length());

                // paint text fragment
231
                if (printerFriendly) {
232
233
234
                    drawPrinterFriendlyTextFragment(paint,
                                                    textArea,
                                                    unistr,
235
                                                    &_display->_image[_display->loc(x, y)]);    
236
237
238
239
                } else {
                    drawTextFragment(paint,
                                    textArea,
                                    unistr,
240
                                    &_display->_image[_display->loc(x, y)]);
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
                }

                _display->_fixedFont = save__fixedFont;

                paint.setWorldTransform(QTransform(textScale.inverted()), true);

                if (y < _display->_lineProperties.size() - 1) {

                    if ((_display->_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) {
                        y++;
                    }
                }

                x += len - 1;
            }
        }
    }
    
    void TerminalPainter::drawCurrentResultRect(QPainter &painter)
    {
        if (_display->_screenWindow->currentResultLine() == -1) {
            return;
        }
        
        _display->_searchResultRect.setRect(0, _display->contentRect().top() + (_display->_screenWindow->currentResultLine() - _display->_screenWindow->currentLine()) * _display->fontHeight(),
            _display->columns() * _display->fontWidth(), _display->fontHeight());
        painter.fillRect(_display->_searchResultRect, QColor(0, 0, 255, 80));
    }
    
    void TerminalPainter::highlightScrolledLines(QPainter& painter) 
    {
        if (!_display->_highlightScrolledLinesControl.enabled) {
            return;
        }

        QColor color = QColor(_display->_colorTable[Color4Index]);
        color.setAlpha(_display->_highlightScrolledLinesControl.timer->isActive() ? 255 : 150);
        painter.fillRect(_display->_highlightScrolledLinesControl.rect, color);
    }
    
    QRegion TerminalPainter::highlightScrolledLinesRegion(bool nothingChanged) 
    {
        QRegion dirtyRegion;
284
        const int highlightLeftPosition = _display->scrollBar()->scrollBarPosition() == Enum::ScrollBarLeft ? _display->scrollBar()->width() : 0;
285
286
287

        int start = 0;
        int nb_lines = abs(_display->_screenWindow->scrollCount());
288
        if (nb_lines > 0 && _display->scrollBar()->maximum() > 0) {
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
            QRect new_highlight;
            bool addToCurrentHighlight = _display->_highlightScrolledLinesControl.timer->isActive() &&
                                     (_display->_screenWindow->scrollCount() * _display->_highlightScrolledLinesControl.previousScrollCount > 0);
            if (addToCurrentHighlight) {
                if (_display->_screenWindow->scrollCount() > 0) {
                    start = -1 * (_display->_highlightScrolledLinesControl.previousScrollCount + _display->screenWindow()->scrollCount()) + _display->screenWindow()->windowLines();
                } else {
                    start = -1 * _display->_highlightScrolledLinesControl.previousScrollCount;
                }
                _display->_highlightScrolledLinesControl.previousScrollCount += _display->_screenWindow->scrollCount();
            } else {
                start = _display->_screenWindow->scrollCount() > 0 ? _display->_screenWindow->windowLines() - nb_lines : 0;
                _display->_highlightScrolledLinesControl.previousScrollCount = _display->_screenWindow->scrollCount();
            }

            new_highlight.setRect(highlightLeftPosition, _display->contentRect().top() + start * _display->fontHeight(), _display->HIGHLIGHT_SCROLLED_LINES_WIDTH, nb_lines * _display->fontHeight());
            new_highlight.setTop(std::max(new_highlight.top(), _display->contentRect().top()));
            new_highlight.setBottom(std::min(new_highlight.bottom(), _display->contentRect().bottom()));
            if (!new_highlight.isValid()) {
                new_highlight = QRect(0, 0, 0, 0);
            }

            if (addToCurrentHighlight) {
                _display->_highlightScrolledLinesControl.rect |= new_highlight;
            } else {
                dirtyRegion |= _display->_highlightScrolledLinesControl.rect;
                _display->_highlightScrolledLinesControl.rect = new_highlight;
            }

            _display->_highlightScrolledLinesControl.timer->start();
        } else if (!nothingChanged || _display->_highlightScrolledLinesControl.needToClear) {
            dirtyRegion = _display->_highlightScrolledLinesControl.rect;
            _display->_highlightScrolledLinesControl.rect.setRect(0, 0, 0, 0);
            _display->_highlightScrolledLinesControl.needToClear = false;
        }

        return dirtyRegion;
    }
    
    void TerminalPainter::drawTextFragment(QPainter &painter, const QRect &rect, const QString &text,
                                const Character *style) 
    {
        // setup painter
        const QColor foregroundColor = style->foregroundColor.color(_display->_colorTable);
        const QColor backgroundColor = style->backgroundColor.color(_display->_colorTable);

        if (backgroundColor != _display->_colorTable[DEFAULT_BACK_COLOR]) {
            drawBackground(painter, rect, backgroundColor, false);
        }

        QColor characterColor;
        if ((style->rendition & RE_CURSOR) != 0) {
            drawCursor(painter, rect, foregroundColor, backgroundColor, characterColor);
        }

        // draw text
        drawCharacters(painter, rect, text, style, characterColor);
    }
    
    void TerminalPainter::drawPrinterFriendlyTextFragment(QPainter &painter, const QRect &rect, const QString &text,
                                                const Character *style) 
    {
        Character print_style = *style;
        print_style.foregroundColor = CharacterColor(COLOR_SPACE_RGB, 0x00000000);
        print_style.backgroundColor = CharacterColor(COLOR_SPACE_RGB, 0xFFFFFFFF);

        drawCharacters(painter, rect, text, &print_style, QColor());
    }
    
    void TerminalPainter::drawBackground(QPainter &painter, const QRect &rect, const QColor &backgroundColor,
                                bool useOpacitySetting) 
    {
        if (useOpacitySetting && !_display->_wallpaper->isNull() &&
                _display->_wallpaper->draw(painter, rect, _display->_opacity)) {
        } else if (qAlpha(_display->_blendColor) < 0xff && useOpacitySetting) {
#if defined(Q_OS_MACOS)
            // TODO: On MacOS, using CompositionMode doesn't work. Altering the
            //       transparency in the color scheme alters the brightness.
            painter.fillRect(rect, backgroundColor);
#else
            QColor color(backgroundColor);
            color.setAlpha(qAlpha(_display->_blendColor));

            const QPainter::CompositionMode originalMode = painter.compositionMode();
            painter.setCompositionMode(QPainter::CompositionMode_Source);
            painter.fillRect(rect, color);
            painter.setCompositionMode(originalMode);
#endif
        } else {
            painter.fillRect(rect, backgroundColor);
        }
    }
    
    void TerminalPainter::drawCursor(QPainter &painter, const QRect &rect, const QColor &foregroundColor,
                            const QColor &backgroundColor, QColor &characterColor) 
    {
        if (_display->_cursorBlinking) {
            return;
        }

        QRectF cursorRect = rect.adjusted(0, 1, 0, 0);

        QColor cursorColor = _display->_cursorColor.isValid() ? _display->_cursorColor : foregroundColor;
        QPen pen(cursorColor);
        // TODO: the relative pen width to draw the cursor is a bit hacky
        // and set to 1/12 of the font width. Visually it seems to work at
        // all scales but there must be better ways to do it
        const qreal width = qMax(_display->fontWidth() / 12.0, 1.0);
        const qreal halfWidth = width / 2.0;
        pen.setWidthF(width);
        painter.setPen(pen);

        if (_display->_cursorShape == Enum::BlockCursor) {
            painter.drawRect(cursorRect.adjusted(halfWidth, halfWidth, -halfWidth, -halfWidth));

            if (_display->hasFocus()) {
                painter.fillRect(cursorRect, cursorColor);

                characterColor = _display->_cursorTextColor.isValid() ? _display->_cursorTextColor : backgroundColor;
            }
        } else if (_display->_cursorShape == Enum::UnderlineCursor) {
            QLineF line(cursorRect.left() + halfWidth,
                        cursorRect.bottom() - halfWidth,
                        cursorRect.right() - halfWidth,
                        cursorRect.bottom() - halfWidth);
            painter.drawLine(line);

        } else if (_display->_cursorShape == Enum::IBeamCursor) {
            QLineF line(cursorRect.left() + halfWidth,
                        cursorRect.top() + halfWidth,
                        cursorRect.left() + halfWidth,
                        cursorRect.bottom() - halfWidth);
            painter.drawLine(line);
        }
    }
    
    void TerminalPainter::drawCharacters(QPainter &painter, const QRect &rect, const QString &text,
                                const Character *style, const QColor &characterColor) 
    {
        if (_display->_textBlinking && ((style->rendition & RE_BLINK) != 0)) {
            return;
        }

        if ((style->rendition & RE_CONCEAL) != 0) {
            return;
        }

        static constexpr int MaxFontWeight = 99;
        
        const int normalWeight = _display->font().weight();

        const int boldWeight = qMin(normalWeight + 26, MaxFontWeight);

        const auto isBold = [boldWeight](const QFont &font) { return font.weight() >= boldWeight; };

        const bool useBold = (((style->rendition & RE_BOLD) != 0) && _display->_boldIntense);
        const bool useUnderline = ((style->rendition & RE_UNDERLINE) != 0) || _display->font().underline();
446
        const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || _display->font().italic();
447
448
449
450
451
452
453
454
455
456
        const bool useStrikeOut = ((style->rendition & RE_STRIKEOUT) != 0) || _display->font().strikeOut();
        const bool useOverline = ((style->rendition & RE_OVERLINE) != 0) || _display->font().overline();

        QFont currentFont = painter.font();

        if (isBold(currentFont) != useBold
                || currentFont.underline() != useUnderline
                || currentFont.italic() != useItalic
                || currentFont.strikeOut() != useStrikeOut
                || currentFont.overline() != useOverline) {
457

458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
            currentFont.setWeight(useBold ? boldWeight : normalWeight);
            currentFont.setUnderline(useUnderline);
            currentFont.setItalic(useItalic);
            currentFont.setStrikeOut(useStrikeOut);
            currentFont.setOverline(useOverline);
            painter.setFont(currentFont);
        }

        // setup pen
        const QColor foregroundColor = style->foregroundColor.color(_display->_colorTable);
        const QColor color = characterColor.isValid() ? characterColor : foregroundColor;
        QPen pen = painter.pen();
        if (pen.color() != color) {
            pen.setColor(color);
            painter.setPen(color);
        }

        const bool origClipping = painter.hasClipping();
        const auto origClipRegion = painter.clipRegion();
        painter.setClipRect(rect);
        // draw text
        if (isLineCharString(text) && !_display->_useFontLineCharacters) {
            drawLineCharString(painter, rect.x(), rect.y(), text, style);
        } else {
            painter.setLayoutDirection(Qt::LeftToRight);

            if (_display->_bidiEnabled) {
                painter.drawText(rect.x(), rect.y() + _display->_fontAscent + _display->lineSpacing(), text);
            } else {
                painter.drawText(rect.x(), rect.y() + _display->_fontAscent + _display->lineSpacing(), LTR_OVERRIDE_CHAR + text);
            }
        }
        painter.setClipRegion(origClipRegion);
        painter.setClipping(origClipping);
    }
493

494
    void TerminalPainter::drawLineCharString(QPainter &painter, int x, int y, const QString &str,
495
                                    const Character *attributes)
496
497
498
499
500
501
502
503
504
505
506
    {
        painter.setRenderHint(QPainter::Antialiasing, _display->_antialiasText);

        const bool useBoldPen = (attributes->rendition & RE_BOLD) != 0 && _display->_boldIntense;

        QRect cellRect = {x, y, _display->fontWidth(), _display->fontHeight()};
        for (int i = 0; i < str.length(); i++) {
            LineBlockCharacters::draw(painter, cellRect.translated(i * _display->fontWidth(), 0), str[i], useBoldPen);
        }
        painter.setRenderHint(QPainter::Antialiasing, false);
    }
507
508

    void TerminalPainter::drawInputMethodPreeditString(QPainter &painter, const QRect &rect)
509
    {
510
        if (_display->_inputMethodData.preeditString.isEmpty() || !_display->isCursorOnDisplay()) {
511
512
            return;
        }
513

514
        const QPoint cursorPos = _display->cursorPosition();
515
516
517
518

        QColor characterColor;
        const QColor background = _display->_colorTable[DEFAULT_BACK_COLOR];
        const QColor foreground = _display->_colorTable[DEFAULT_FORE_COLOR];
519
        const Character *style = &_display->_image[_display->loc(cursorPos.x(), cursorPos.y())];
520
521
522
523
524
525
526
527
528

        drawBackground(painter, rect, background, true);
        drawCursor(painter, rect, foreground, background, characterColor);
        drawCharacters(painter, rect, _display->_inputMethodData.preeditString, style, characterColor);

        _display->_inputMethodData.previousPreeditRect = rect;
    }

}