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