1 /*
2     SPDX-FileCopyrightText: 2020-2020 Gustavo Carneiro <gcarneiroa@hotmail.com>
3     SPDX-FileCopyrightText: 2007-2008 Robert Knight <robertknight@gmail.com>
4     SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de>
5 
6     SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 
9 // Own
10 #include "TerminalScrollBar.h"
11 
12 // Konsole
13 #include "../characters/Character.h"
14 #include "TerminalDisplay.h"
15 #include "TerminalFonts.h"
16 #include "session/SessionController.h"
17 
18 // KDE
19 #include <KMessageWidget>
20 
21 // Qt
22 #include <QGuiApplication>
23 #include <QLabel>
24 #include <QProxyStyle>
25 #include <QRect>
26 #include <QTimer>
27 
28 namespace Konsole
29 {
TerminalScrollBar(QWidget * parent)30 TerminalScrollBar::TerminalScrollBar(QWidget *parent)
31     : QScrollBar(parent)
32     , _scrollFullPage(false)
33     , _alternateScrolling(false)
34     , _scrollbarLocation(Enum::ScrollBarRight)
35 {
36     connect(this, &QScrollBar::valueChanged, this, &TerminalScrollBar::scrollBarPositionChanged);
37 }
38 
setScrollBarPosition(Enum::ScrollBarPositionEnum position)39 void TerminalScrollBar::setScrollBarPosition(Enum::ScrollBarPositionEnum position)
40 {
41     if (_scrollbarLocation == position) {
42         return;
43     }
44 
45     _scrollbarLocation = position;
46     applyScrollBarPosition(true);
47 }
48 
setScroll(int cursor,int slines)49 void TerminalScrollBar::setScroll(int cursor, int slines)
50 {
51     const auto display = qobject_cast<TerminalDisplay *>(this->parent());
52     // update _scrollBar if the range or value has changed,
53     // otherwise return
54     //
55     // setting the range or value of a _scrollBar will always trigger
56     // a repaint, so it should be avoided if it is not necessary
57     if (this->minimum() == 0 && this->maximum() == (slines - display->lines()) && this->value() == cursor) {
58         return;
59     }
60 
61     disconnect(this, &QScrollBar::valueChanged, this, &TerminalScrollBar::scrollBarPositionChanged);
62     setRange(0, slines - display->lines());
63     setSingleStep(1);
64     setPageStep(display->lines());
65     setValue(cursor);
66     connect(this, &QScrollBar::valueChanged, this, &TerminalScrollBar::scrollBarPositionChanged);
67 }
68 
setScrollFullPage(bool fullPage)69 void TerminalScrollBar::setScrollFullPage(bool fullPage)
70 {
71     _scrollFullPage = fullPage;
72 }
73 
scrollFullPage() const74 bool TerminalScrollBar::scrollFullPage() const
75 {
76     return _scrollFullPage;
77 }
78 
setHighlightScrolledLines(bool highlight)79 void TerminalScrollBar::setHighlightScrolledLines(bool highlight)
80 {
81     _highlightScrolledLines.setEnabled(highlight);
82     _highlightScrolledLines.setTimer(this);
83     _highlightScrolledLines.setNeedToClear(true);
84 }
85 
alternateScrolling() const86 bool TerminalScrollBar::alternateScrolling() const
87 {
88     return _alternateScrolling;
89 }
90 
setAlternateScrolling(bool enable)91 void TerminalScrollBar::setAlternateScrolling(bool enable)
92 {
93     _alternateScrolling = enable;
94 }
95 
scrollBarPositionChanged(int)96 void TerminalScrollBar::scrollBarPositionChanged(int)
97 {
98     const auto display = qobject_cast<TerminalDisplay *>(this->parent());
99 
100     if (display->screenWindow().isNull()) {
101         return;
102     }
103 
104     display->screenWindow()->scrollTo(this->value());
105 
106     // if the thumb has been moved to the bottom of the _scrollBar then set
107     // the display to automatically track new output,
108     // that is, scroll down automatically
109     // to how new _lines as they are added
110     const bool atEndOfOutput = (this->value() == this->maximum());
111     display->screenWindow()->setTrackOutput(atEndOfOutput);
112 
113     display->updateImage();
114 }
115 
highlightScrolledLinesEvent()116 void TerminalScrollBar::highlightScrolledLinesEvent()
117 {
118     const auto display = qobject_cast<TerminalDisplay *>(this->parent());
119     display->update(_highlightScrolledLines.rect());
120 }
121 
applyScrollBarPosition(bool propagate)122 void TerminalScrollBar::applyScrollBarPosition(bool propagate)
123 {
124     setHidden(_scrollbarLocation == Enum::ScrollBarHidden);
125 
126     if (propagate) {
127         const auto display = qobject_cast<TerminalDisplay *>(this->parent());
128         display->propagateSize();
129         display->update();
130     }
131 }
132 
133 // scrolls the image by 'lines', down if lines > 0 or up otherwise.
134 //
135 // the terminal emulation keeps track of the scrolling of the character
136 // image as it receives input, and when the view is updated, it calls scrollImage()
137 // with the final scroll amount.  this improves performance because scrolling the
138 // display is much cheaper than re-rendering all the text for the
139 // part of the image which has moved up or down.
140 // Instead only new lines have to be drawn
scrollImage(int lines,const QRect & screenWindowRegion,Character * image,int imageSize)141 void TerminalScrollBar::scrollImage(int lines, const QRect &screenWindowRegion, Character *image, int imageSize)
142 {
143     // return if there is nothing to do
144     if ((lines == 0) || (image == nullptr)) {
145         return;
146     }
147 
148     const auto display = qobject_cast<TerminalDisplay *>(this->parent());
149     // constrain the region to the display
150     // the bottom of the region is capped to the number of lines in the display's
151     // internal image - 2, so that the height of 'region' is strictly less
152     // than the height of the internal image.
153     QRect region = screenWindowRegion;
154     region.setBottom(qMin(region.bottom(), display->lines() - 2));
155 
156     // return if there is nothing to do
157     if (!region.isValid() || (region.top() + abs(lines)) >= region.bottom() || display->lines() <= region.bottom()) {
158         return;
159     }
160 
161     // Note:  With Qt 4.4 the left edge of the scrolled area must be at 0
162     // to get the correct (newly exposed) part of the widget repainted.
163     //
164     // The right edge must be before the left edge of the scroll bar to
165     // avoid triggering a repaint of the entire widget, the distance is
166     // given by SCROLLBAR_CONTENT_GAP
167     //
168     // Set the QT_FLUSH_PAINT environment variable to '1' before starting the
169     // application to monitor repainting.
170     //
171     const int scrollBarWidth = this->isHidden() ? 0 : this->width();
172     const int SCROLLBAR_CONTENT_GAP = 1;
173     QRect scrollRect;
174     if (_scrollbarLocation == Enum::ScrollBarLeft) {
175         scrollRect.setLeft(scrollBarWidth + SCROLLBAR_CONTENT_GAP
176                            + (_highlightScrolledLines.isEnabled() ? _highlightScrolledLines.HIGHLIGHT_SCROLLED_LINES_WIDTH : 0));
177         scrollRect.setRight(display->width());
178     } else {
179         scrollRect.setLeft(_highlightScrolledLines.isEnabled() ? _highlightScrolledLines.HIGHLIGHT_SCROLLED_LINES_WIDTH : 0);
180 
181         scrollRect.setRight(display->width() - scrollBarWidth - SCROLLBAR_CONTENT_GAP);
182     }
183     void *firstCharPos = &image[region.top() * display->columns()];
184     void *lastCharPos = &image[(region.top() + abs(lines)) * display->columns()];
185 
186     const int top = display->contentRect().top() + (region.top() * display->terminalFont()->fontHeight());
187     const int linesToMove = region.height() - abs(lines);
188     const int bytesToMove = linesToMove * display->columns() * sizeof(Character);
189 
190     Q_ASSERT(linesToMove > 0);
191     Q_ASSERT(bytesToMove > 0);
192 
193     scrollRect.setTop(lines > 0 ? top : top + abs(lines) * display->terminalFont()->fontHeight());
194     scrollRect.setHeight(linesToMove * display->terminalFont()->fontHeight());
195 
196     if (!scrollRect.isValid() || scrollRect.isEmpty()) {
197         return;
198     }
199 
200     // scroll internal image
201     if (lines > 0) {
202         // check that the memory areas that we are going to move are valid
203         Q_ASSERT((char *)lastCharPos + bytesToMove < (char *)(image + (display->lines() * display->columns())));
204         Q_ASSERT((lines * display->columns()) < imageSize);
205 
206         // scroll internal image down
207         memmove(firstCharPos, lastCharPos, bytesToMove);
208     } else {
209         // check that the memory areas that we are going to move are valid
210         Q_ASSERT((char *)firstCharPos + bytesToMove < (char *)(image + (display->lines() * display->columns())));
211 
212         // scroll internal image up
213         memmove(lastCharPos, firstCharPos, bytesToMove);
214     }
215 
216     // scroll the display vertically to match internal _image
217     display->scroll(0, display->terminalFont()->fontHeight() * (-lines), scrollRect);
218 }
219 
changeEvent(QEvent * e)220 void TerminalScrollBar::changeEvent(QEvent *e)
221 {
222     if (e->type() == QEvent::StyleChange) {
223         updatePalette(_backgroundMatchingPalette);
224     }
225     QScrollBar::changeEvent(e);
226 }
227 
updatePalette(const QPalette & pal)228 void TerminalScrollBar::updatePalette(const QPalette &pal)
229 {
230     _backgroundMatchingPalette = pal;
231 
232     auto proxyStyle = qobject_cast<const QProxyStyle *>(style());
233     const QStyle *appStyle = proxyStyle ? proxyStyle->baseStyle() : style();
234 
235     // Scrollbars in widget styles like Fusion or Plastique do not work well with custom
236     // scrollbar coloring, in particular in conjunction with light terminal background colors.
237     // Use custom colors only for widget styles matched by the allowlist below, otherwise
238     // fall back to generic widget colors.
239     if (appStyle->objectName() == QLatin1String("breeze")) {
240         setPalette(_backgroundMatchingPalette);
241     } else {
242         setPalette(QGuiApplication::palette());
243     }
244 }
245 
246 } // namespace Konsole
247