QGIS API Documentation 3.40.0-Bratislava (b56115d8743)
Loading...
Searching...
No Matches
qgsdecoratedscrollbar.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsdecoratedscrollbar.cpp
3 --------------------------------------
4 Date : May 2024
5 Copyright : (C) 2024 by Nyall Dawson
6 Email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17#include <QAbstractScrollArea>
18#include <QScrollBar>
19#include <QPainter>
20#include <QEvent>
21#include <QStyleOptionSlider>
22#include <cmath>
23
25
26//
27// QgsScrollBarHighlightOverlay
28//
29
30QgsScrollBarHighlightOverlay::QgsScrollBarHighlightOverlay( QgsScrollBarHighlightController *scrollBarController )
31 : QWidget( scrollBarController->scrollArea() )
32 , mHighlightController( scrollBarController )
33{
34 setAttribute( Qt::WA_TransparentForMouseEvents );
35 scrollBar()->parentWidget()->installEventFilter( this );
36 doResize();
37 doMove();
38 setVisible( scrollBar()->isVisible() );
39}
40
41void QgsScrollBarHighlightOverlay::doResize()
42{
43 resize( scrollBar()->size() );
44}
45
46void QgsScrollBarHighlightOverlay::doMove()
47{
48 move( parentWidget()->mapFromGlobal( scrollBar()->mapToGlobal( scrollBar()->pos() ) ) );
49}
50
51void QgsScrollBarHighlightOverlay::scheduleUpdate()
52{
53 if ( mIsCacheUpdateScheduled )
54 return;
55
56 mIsCacheUpdateScheduled = true;
57// silence false positive leak warning
58#ifndef __clang_analyzer__
59 QMetaObject::invokeMethod( this, qOverload<>( &QWidget::update ), Qt::QueuedConnection );
60#endif
61}
62
63void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent )
64{
65 QWidget::paintEvent( paintEvent );
66
67 updateCache();
68
69 if ( mHighlightCache.isEmpty() )
70 return;
71
72 QPainter painter( this );
73 painter.setRenderHint( QPainter::Antialiasing, false );
74
75 const QRect &gRect = overlayRect();
76 const QRect &hRect = handleRect();
77
78 constexpr int marginX = 3;
79 constexpr int marginH = -2 * marginX + 1;
80 const QRect aboveHandleRect = QRect( gRect.x() + marginX,
81 gRect.y(),
82 gRect.width() + marginH,
83 hRect.y() - gRect.y() );
84 const QRect handleRect = QRect( gRect.x() + marginX,
85 hRect.y(),
86 gRect.width() + marginH,
87 hRect.height() );
88 const QRect belowHandleRect = QRect( gRect.x() + marginX,
89 hRect.y() + hRect.height(),
90 gRect.width() + marginH,
91 gRect.height() - hRect.height() + gRect.y() - hRect.y() );
92
93 const int aboveValue = scrollBar()->value();
94 const int belowValue = scrollBar()->maximum() - scrollBar()->value();
95 const int sizeDocAbove = int( aboveValue * mHighlightController->lineHeight() );
96 const int sizeDocBelow = int( belowValue * mHighlightController->lineHeight() );
97 const int sizeDocVisible = int( mHighlightController->visibleRange() );
98
99 const int scrollBarBackgroundHeight = aboveHandleRect.height() + belowHandleRect.height();
100 const int sizeDocInvisible = sizeDocAbove + sizeDocBelow;
101 const double backgroundRatio = sizeDocInvisible
102 ? ( ( double )scrollBarBackgroundHeight / sizeDocInvisible ) : 0;
103
104
105 if ( aboveValue )
106 {
107 drawHighlights( &painter,
108 0,
109 sizeDocAbove,
110 backgroundRatio,
111 0,
112 aboveHandleRect );
113 }
114
115 if ( belowValue )
116 {
117 // This is the hypothetical handle height if the handle would
118 // be stretched using the background ratio.
119 const double handleVirtualHeight = sizeDocVisible * backgroundRatio;
120 // Skip the doc above and visible part.
121 const int offset = static_cast< int >( std::round( aboveHandleRect.height() + handleVirtualHeight ) );
122
123 drawHighlights( &painter,
124 sizeDocAbove + sizeDocVisible,
125 sizeDocBelow,
126 backgroundRatio,
127 offset,
128 belowHandleRect );
129 }
130
131 const double handleRatio = sizeDocVisible
132 ? ( ( double )handleRect.height() / sizeDocVisible ) : 0;
133
134 // This is the hypothetical handle position if the background would
135 // be stretched using the handle ratio.
136 const double aboveVirtualHeight = sizeDocAbove * handleRatio;
137
138 // This is the accurate handle position (double)
139 const double accurateHandlePos = sizeDocAbove * backgroundRatio;
140 // The correction between handle position (int) and accurate position (double)
141 const double correction = aboveHandleRect.height() - accurateHandlePos;
142 // Skip the doc above and apply correction
143 const int offset = static_cast< int >( std::round( aboveVirtualHeight + correction ) );
144
145 drawHighlights( &painter,
146 sizeDocAbove,
147 sizeDocVisible,
148 handleRatio,
149 offset,
150 handleRect );
151}
152
153void QgsScrollBarHighlightOverlay::drawHighlights( QPainter *painter,
154 int docStart,
155 int docSize,
156 double docSizeToHandleSizeRatio,
157 int handleOffset,
158 const QRect &viewport )
159{
160 if ( docSize <= 0 )
161 return;
162
163 painter->save();
164 painter->setClipRect( viewport );
165
166 const double lineHeight = mHighlightController->lineHeight();
167
168 for ( const QMap<QRgb, QMap<int, int>> &colors : std::as_const( mHighlightCache ) )
169 {
170 const auto itColorEnd = colors.constEnd();
171 for ( auto itColor = colors.constBegin(); itColor != itColorEnd; ++itColor )
172 {
173 const QColor color = itColor.key();
174 const QMap<int, int> &positions = itColor.value();
175 const auto itPosEnd = positions.constEnd();
176 const auto firstPos = int( docStart / lineHeight );
177 auto itPos = positions.upperBound( firstPos );
178 if ( itPos != positions.constBegin() )
179 --itPos;
180 while ( itPos != itPosEnd )
181 {
182 const double posStart = itPos.key() * lineHeight;
183 const double posEnd = ( itPos.value() + 1 ) * lineHeight;
184 if ( posEnd < docStart )
185 {
186 ++itPos;
187 continue;
188 }
189 if ( posStart > docStart + docSize )
190 break;
191
192 const int height = std::max( static_cast< int >( std::round( ( posEnd - posStart ) * docSizeToHandleSizeRatio ) ), 1 );
193 const int top = static_cast< int >( std::round( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y() );
194
195 const QRect rect( viewport.left(), top, viewport.width(), height );
196 painter->fillRect( rect, color );
197 ++itPos;
198 }
199 }
200 }
201 painter->restore();
202}
203
204bool QgsScrollBarHighlightOverlay::eventFilter( QObject *object, QEvent *event )
205{
206 switch ( event->type() )
207 {
208 case QEvent::Move:
209 doMove();
210 break;
211 case QEvent::Resize:
212 doResize();
213 break;
214 case QEvent::ZOrderChange:
215 raise();
216 break;
217 case QEvent::Show:
218 show();
219 break;
220 case QEvent::Hide:
221 hide();
222 break;
223 default:
224 break;
225 }
226 return QWidget::eventFilter( object, event );
227}
228
229static void insertPosition( QMap<int, int> *map, int position )
230{
231 auto itNext = map->upperBound( position );
232
233 bool gluedWithPrev = false;
234 if ( itNext != map->begin() )
235 {
236 auto itPrev = std::prev( itNext );
237 const int keyStart = itPrev.key();
238 const int keyEnd = itPrev.value();
239 if ( position >= keyStart && position <= keyEnd )
240 return; // pos is already included
241
242 if ( keyEnd + 1 == position )
243 {
244 // glue with prev
245 ( *itPrev )++;
246 gluedWithPrev = true;
247 }
248 }
249
250 if ( itNext != map->end() && itNext.key() == position + 1 )
251 {
252 const int keyEnd = itNext.value();
253 itNext = map->erase( itNext );
254 if ( gluedWithPrev )
255 {
256 // glue with prev and next
257 auto itPrev = std::prev( itNext );
258 *itPrev = keyEnd;
259 }
260 else
261 {
262 // glue with next
263 itNext = map->insert( itNext, position, keyEnd );
264 }
265 return; // glued
266 }
267
268 if ( gluedWithPrev )
269 return; // glued
270
271 map->insert( position, position );
272}
273
274void QgsScrollBarHighlightOverlay::updateCache()
275{
276 if ( !mIsCacheUpdateScheduled )
277 return;
278
279 mHighlightCache.clear();
280
281 const QHash<int, QVector<QgsScrollBarHighlight>> highlightsForId = mHighlightController->highlights();
282 for ( const QVector<QgsScrollBarHighlight> &highlights : highlightsForId )
283 {
284 for ( const QgsScrollBarHighlight &highlight : highlights )
285 {
286 QMap<int, int> &highlightMap = mHighlightCache[highlight.priority][highlight.color.rgba()];
287 insertPosition( &highlightMap, highlight.position );
288 }
289 }
290
291 mIsCacheUpdateScheduled = false;
292}
293
294QRect QgsScrollBarHighlightOverlay::overlayRect() const
295{
296 QStyleOptionSlider opt = qt_qscrollbarStyleOption( scrollBar() );
297 return scrollBar()->style()->subControlRect( QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, scrollBar() );
298}
299
300QRect QgsScrollBarHighlightOverlay::handleRect() const
301{
302 QStyleOptionSlider opt = qt_qscrollbarStyleOption( scrollBar() );
303 return scrollBar()->style()->subControlRect( QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, scrollBar() );
304}
305
307
308//
309// QgsScrollBarHighlight
310//
311
313 const QColor &color, QgsScrollBarHighlight::Priority priority )
314 : category( category )
315 , position( position )
316 , color( color )
317 , priority( priority )
318{
319}
320
321
322//
323// QgsScrollBarHighlightController
324//
325
327
329{
330 if ( mOverlay )
331 delete mOverlay;
332}
333
335{
336 if ( mScrollArea )
337 return mScrollArea->verticalScrollBar();
338
339 return nullptr;
340}
341
343{
344 return mScrollArea;
345}
346
347void QgsScrollBarHighlightController::setScrollArea( QAbstractScrollArea *scrollArea )
348{
349 if ( mScrollArea == scrollArea )
350 return;
351
352 if ( mOverlay )
353 {
354 delete mOverlay;
355 mOverlay = nullptr;
356 }
357
358 mScrollArea = scrollArea;
359
360 if ( mScrollArea )
361 {
362 mOverlay = new QgsScrollBarHighlightOverlay( this );
363 mOverlay->scheduleUpdate();
364 }
365}
366
368{
369 return std::ceil( mLineHeight );
370}
371
373{
374 mLineHeight = lineHeight;
375}
376
378{
379 return mVisibleRange;
380}
381
383{
384 mVisibleRange = visibleRange;
385}
386
388{
389 return mMargin;
390}
391
393{
394 mMargin = margin;
395}
396
397QHash<int, QVector<QgsScrollBarHighlight>> QgsScrollBarHighlightController::highlights() const
398{
399 return mHighlights;
400}
401
403{
404 if ( !mOverlay )
405 return;
406
407 mHighlights[highlight.category] << highlight;
408 mOverlay->scheduleUpdate();
409}
410
412{
413 if ( !mOverlay )
414 return;
415
416 mHighlights.remove( category );
417 mOverlay->scheduleUpdate();
418}
419
421{
422 if ( !mOverlay )
423 return;
424
425 mHighlights.clear();
426 mOverlay->scheduleUpdate();
427}
Adds highlights (colored markers) to a scrollbar.
void setVisibleRange(double visibleRange)
Sets the visible range of the scroll area (i.e.
void setMargin(double margin)
Sets the document margin for the associated viewport.
void removeHighlights(int category)
Removes all highlights with matching category from the scrollbar.
double margin() const
Returns the document margins for the associated viewport.
double visibleRange() const
Returns the visible range of the scroll area (i.e.
double lineHeight() const
Returns the line height for text associated with the scroll area.
QScrollBar * scrollBar() const
Returns the associated scroll bar.
void addHighlight(const QgsScrollBarHighlight &highlight)
Adds a highlight to the scrollbar.
void setLineHeight(double height)
Sets the line height for text associated with the scroll area.
QAbstractScrollArea * scrollArea() const
Returns the associated scroll area.
void setScrollArea(QAbstractScrollArea *scrollArea)
Sets the associated scroll bar.
void removeAllHighlights()
Removes all highlights from the scroll bar.
QHash< int, QVector< QgsScrollBarHighlight > > highlights() const
Returns the hash of all highlights in the scrollbar, with highlight categories as hash keys.
Encapsulates the details of a highlight in a scrollbar, used alongside QgsScrollBarHighlightControlle...
Priority
Priority, which dictates how overlapping highlights are rendered.
QgsScrollBarHighlight()=default