TerminalPainter.cpp 24.6 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
    Copyright 2020-2020 by Gustavo Carneiro <gcarneiroa@hotmail.com>
    Copyright 2007-2008 by Robert Knight <robertknight@gmail.com>
    Copyright 1997,1998 by Lars Doelle <lars.doelle@on-line.de>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
    02110-1301  USA.
*/

// Own
#include "TerminalPainter.hpp"

// Konsole
#include "TerminalDisplay.h"
27
#include "TerminalScrollBar.hpp"
28
#include "ExtendedCharTable.h"
29
#include "../characters/LineBlockCharacters.h"
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

// 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
45
46
// we use this to force QPainter to display text in LTR mode
// more information can be found in: https://unicode.org/reports/tr9/
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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);
            return chars[0];
        } else {
            return ch.character;
        }
    }

75
    void TerminalPainter::drawContents(QPainter &paint, const QRect &rect, bool printerFriendly)
76
77
78
79
80
81
    {
        const int numberOfColumns = _display->_usedColumns;
        QVector<uint> univec;
        univec.reserve(numberOfColumns);
        for (int y = rect.y(); y <= rect.bottom(); y++) {
            int x = rect.x();
82
            if ((_display->_image[_display->loc(rect.x(), y)].character == 0u) && (x != 0)) {
83
84
85
86
87
88
89
90
91
92
93
94
                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 ?
95
                if ((_display->_image[_display->loc(x, y)].rendition & RE_EXTENDED_CHAR) != 0) {
96
97
                    // sequence of characters
                    ushort extendedCharLength = 0;
98
                    const uint *chars = ExtendedCharTable::instance.lookupExtendedChar(_display->_image[_display->loc(x, y)].character, extendedCharLength);
99
100
101
102
103
104
105
106
107
108
109
                    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 {
110
                    const uint c = _display->_image[_display->loc(x, y)].character;
111
112
113
114
115
116
                    if (c != 0u) {
                        Q_ASSERT(p < bufferSize);
                        disstrU[p++] = c;
                    }
                }

117
118
119
120
121
122
                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)]));
123
124
125

                const auto isInsideDrawArea = [&](int column) { return column <= rect.right(); };
                const auto hasSameColors = [&](int column) {
126
127
                    return _display->_image[_display->loc(column, y)].foregroundColor == currentForeground
                        && _display->_image[_display->loc(column, y)].backgroundColor == currentBackground;
128
129
                };
                const auto hasSameRendition = [&](int column) {
130
                    return (_display->_image[_display->loc(column, y)].rendition & ~RE_EXTENDED_CHAR)
131
132
133
                        == (currentRendition & ~RE_EXTENDED_CHAR);
                };
                const auto hasSameWidth = [&](int column) {
134
                    const int characterLoc = qMin(_display->loc(column, y) + 1, _display->_imageSize - 1);
135
136
137
                    return (_display->_image[characterLoc].character == 0) == doubleWidth;
                };
                const auto hasSameLineDrawStatus = [&](int column) {
138
                    return LineBlockCharacters::canDraw(_display->_image[_display->loc(column, y)].character)
139
140
141
                        == lineDraw;
                };
                const auto isSameScript = [&](int column) {
142
                    const QChar::Script script = QChar::script(baseCodePoint(_display->_image[_display->loc(column, y)]));
143
144
145
146
147
148
149
                    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) {
150
151
                    return _display->_image[_display->loc(column, y)].character <= 0x7e
                            || (_display->_image[_display->loc(column, y)].rendition & RE_EXTENDED_CHAR)
152
153
154
155
156
157
158
159
                            || (_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)) {
160
161
                        const uint c = _display->_image[_display->loc(x + len, y)].character;
                        if ((_display->_image[_display->loc(x + len, y)].rendition & RE_EXTENDED_CHAR) != 0) {
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
                            // 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)
192
                            && _display->_image[_display->loc(x + len, y)].character == ' ' && hasSameColors(x + len)
193
194
195
196
197
                            && hasSameRendition(x + len)) {
                        // disstrU intentionally not modified - trailing spaces are meaningless
                        len++;
                    }
                }
198
                if ((x + len < _display->_usedColumns) && (_display->_image[_display->loc(x + len, y)].character == 0u)) {
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
                    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
223
                // Apply text scaling matrix
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
                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
243
                if (printerFriendly) {
244
245
246
                    drawPrinterFriendlyTextFragment(paint,
                                                    textArea,
                                                    unistr,
247
                                                    &_display->_image[_display->loc(x, y)]);    
248
249
250
251
                } else {
                    drawTextFragment(paint,
                                    textArea,
                                    unistr,
252
                                    &_display->_image[_display->loc(x, y)]);
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
284
285
286
287
288
289
290
291
292
293
294
295
                }

                _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;
296
        const int highlightLeftPosition = _display->scrollBar()->scrollBarPosition() == Enum::ScrollBarLeft ? _display->scrollBar()->width() : 0;
297
298
299

        int start = 0;
        int nb_lines = abs(_display->_screenWindow->scrollCount());
300
        if (nb_lines > 0 && _display->scrollBar()->maximum() > 0) {
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
446
447
448
449
450
451
452
453
454
455
456
457
            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();
458
        const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || _display->font().italic();
459
460
461
462
463
464
465
466
467
468
        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) {
469

470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
            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);
    }
505

506
    void TerminalPainter::drawLineCharString(QPainter &painter, int x, int y, const QString &str,
507
                                    const Character *attributes)
508
509
510
511
512
513
514
515
516
517
518
    {
        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);
    }
519
520

    void TerminalPainter::drawInputMethodPreeditString(QPainter &painter, const QRect &rect)
521
    {
522
        if (_display->_inputMethodData.preeditString.isEmpty() || !_display->isCursorOnDisplay()) {
523
524
            return;
        }
525

526
        const QPoint cursorPos = _display->cursorPosition();
527
528
529
530

        QColor characterColor;
        const QColor background = _display->_colorTable[DEFAULT_BACK_COLOR];
        const QColor foreground = _display->_colorTable[DEFAULT_FORE_COLOR];
531
        const Character *style = &_display->_image[_display->loc(cursorPos.x(), cursorPos.y())];
532
533
534
535
536
537
538
539
540

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

        _display->_inputMethodData.previousPreeditRect = rect;
    }

}