QGIS API Documentation 3.41.0-Master (cea29feecf2)
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, gRect.y(), gRect.width() + marginH, hRect.y() - gRect.y() );
82 const QRect handleRect = QRect( gRect.x() + marginX, hRect.y(), gRect.width() + marginH, hRect.height() );
83 const QRect belowHandleRect = QRect( gRect.x() + marginX, hRect.y() + hRect.height(), gRect.width() + marginH, gRect.height() - hRect.height() + gRect.y() - hRect.y() );
84
85 const int aboveValue = scrollBar()->value();
86 const int belowValue = scrollBar()->maximum() - scrollBar()->value();
87 const int sizeDocAbove = int( aboveValue * mHighlightController->lineHeight() );
88 const int sizeDocBelow = int( belowValue * mHighlightController->lineHeight() );
89 const int sizeDocVisible = int( mHighlightController->visibleRange() );
90
91 const int scrollBarBackgroundHeight = aboveHandleRect.height() + belowHandleRect.height();
92 const int sizeDocInvisible = sizeDocAbove + sizeDocBelow;
93 const double backgroundRatio = sizeDocInvisible
94 ? ( ( double ) scrollBarBackgroundHeight / sizeDocInvisible )
95 : 0;
96
97
98 if ( aboveValue )
99 {
100 drawHighlights( &painter, 0, sizeDocAbove, backgroundRatio, 0, aboveHandleRect );
101 }
102
103 if ( belowValue )
104 {
105 // This is the hypothetical handle height if the handle would
106 // be stretched using the background ratio.
107 const double handleVirtualHeight = sizeDocVisible * backgroundRatio;
108 // Skip the doc above and visible part.
109 const int offset = static_cast<int>( std::round( aboveHandleRect.height() + handleVirtualHeight ) );
110
111 drawHighlights( &painter, sizeDocAbove + sizeDocVisible, sizeDocBelow, backgroundRatio, offset, belowHandleRect );
112 }
113
114 const double handleRatio = sizeDocVisible
115 ? ( ( double ) handleRect.height() / sizeDocVisible )
116 : 0;
117
118 // This is the hypothetical handle position if the background would
119 // be stretched using the handle ratio.
120 const double aboveVirtualHeight = sizeDocAbove * handleRatio;
121
122 // This is the accurate handle position (double)
123 const double accurateHandlePos = sizeDocAbove * backgroundRatio;
124 // The correction between handle position (int) and accurate position (double)
125 const double correction = aboveHandleRect.height() - accurateHandlePos;
126 // Skip the doc above and apply correction
127 const int offset = static_cast<int>( std::round( aboveVirtualHeight + correction ) );
128
129 drawHighlights( &painter, sizeDocAbove, sizeDocVisible, handleRatio, offset, handleRect );
130}
131
132void QgsScrollBarHighlightOverlay::drawHighlights( QPainter *painter, int docStart, int docSize, double docSizeToHandleSizeRatio, int handleOffset, const QRect &viewport )
133{
134 if ( docSize <= 0 )
135 return;
136
137 painter->save();
138 painter->setClipRect( viewport );
139
140 const double lineHeight = mHighlightController->lineHeight();
141
142 for ( const QMap<QRgb, QMap<int, int>> &colors : std::as_const( mHighlightCache ) )
143 {
144 const auto itColorEnd = colors.constEnd();
145 for ( auto itColor = colors.constBegin(); itColor != itColorEnd; ++itColor )
146 {
147 const QColor color = itColor.key();
148 const QMap<int, int> &positions = itColor.value();
149 const auto itPosEnd = positions.constEnd();
150 const auto firstPos = int( docStart / lineHeight );
151 auto itPos = positions.upperBound( firstPos );
152 if ( itPos != positions.constBegin() )
153 --itPos;
154 while ( itPos != itPosEnd )
155 {
156 const double posStart = itPos.key() * lineHeight;
157 const double posEnd = ( itPos.value() + 1 ) * lineHeight;
158 if ( posEnd < docStart )
159 {
160 ++itPos;
161 continue;
162 }
163 if ( posStart > docStart + docSize )
164 break;
165
166 const int height = std::max( static_cast<int>( std::round( ( posEnd - posStart ) * docSizeToHandleSizeRatio ) ), 1 );
167 const int top = static_cast<int>( std::round( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y() );
168
169 const QRect rect( viewport.left(), top, viewport.width(), height );
170 painter->fillRect( rect, color );
171 ++itPos;
172 }
173 }
174 }
175 painter->restore();
176}
177
178bool QgsScrollBarHighlightOverlay::eventFilter( QObject *object, QEvent *event )
179{
180 switch ( event->type() )
181 {
182 case QEvent::Move:
183 doMove();
184 break;
185 case QEvent::Resize:
186 doResize();
187 break;
188 case QEvent::ZOrderChange:
189 raise();
190 break;
191 case QEvent::Show:
192 show();
193 break;
194 case QEvent::Hide:
195 hide();
196 break;
197 default:
198 break;
199 }
200 return QWidget::eventFilter( object, event );
201}
202
203static void insertPosition( QMap<int, int> *map, int position )
204{
205 auto itNext = map->upperBound( position );
206
207 bool gluedWithPrev = false;
208 if ( itNext != map->begin() )
209 {
210 auto itPrev = std::prev( itNext );
211 const int keyStart = itPrev.key();
212 const int keyEnd = itPrev.value();
213 if ( position >= keyStart && position <= keyEnd )
214 return; // pos is already included
215
216 if ( keyEnd + 1 == position )
217 {
218 // glue with prev
219 ( *itPrev )++;
220 gluedWithPrev = true;
221 }
222 }
223
224 if ( itNext != map->end() && itNext.key() == position + 1 )
225 {
226 const int keyEnd = itNext.value();
227 itNext = map->erase( itNext );
228 if ( gluedWithPrev )
229 {
230 // glue with prev and next
231 auto itPrev = std::prev( itNext );
232 *itPrev = keyEnd;
233 }
234 else
235 {
236 // glue with next
237 itNext = map->insert( itNext, position, keyEnd );
238 }
239 return; // glued
240 }
241
242 if ( gluedWithPrev )
243 return; // glued
244
245 map->insert( position, position );
246}
247
248void QgsScrollBarHighlightOverlay::updateCache()
249{
250 if ( !mIsCacheUpdateScheduled )
251 return;
252
253 mHighlightCache.clear();
254
255 const QHash<int, QVector<QgsScrollBarHighlight>> highlightsForId = mHighlightController->highlights();
256 for ( const QVector<QgsScrollBarHighlight> &highlights : highlightsForId )
257 {
258 for ( const QgsScrollBarHighlight &highlight : highlights )
259 {
260 QMap<int, int> &highlightMap = mHighlightCache[highlight.priority][highlight.color.rgba()];
261 insertPosition( &highlightMap, highlight.position );
262 }
263 }
264
265 mIsCacheUpdateScheduled = false;
266}
267
268QRect QgsScrollBarHighlightOverlay::overlayRect() const
269{
270 QStyleOptionSlider opt = qt_qscrollbarStyleOption( scrollBar() );
271 return scrollBar()->style()->subControlRect( QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, scrollBar() );
272}
273
274QRect QgsScrollBarHighlightOverlay::handleRect() const
275{
276 QStyleOptionSlider opt = qt_qscrollbarStyleOption( scrollBar() );
277 return scrollBar()->style()->subControlRect( QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, scrollBar() );
278}
279
281
282//
283// QgsScrollBarHighlight
284//
285
286QgsScrollBarHighlight::QgsScrollBarHighlight( int category, int position, const QColor &color, QgsScrollBarHighlight::Priority priority )
287 : category( category )
288 , position( position )
289 , color( color )
290 , priority( priority )
291{
292}
293
294
295//
296// QgsScrollBarHighlightController
297//
298
300
302{
303 if ( mOverlay )
304 delete mOverlay;
305}
306
308{
309 if ( mScrollArea )
310 return mScrollArea->verticalScrollBar();
311
312 return nullptr;
313}
314
316{
317 return mScrollArea;
318}
319
320void QgsScrollBarHighlightController::setScrollArea( QAbstractScrollArea *scrollArea )
321{
322 if ( mScrollArea == scrollArea )
323 return;
324
325 if ( mOverlay )
326 {
327 delete mOverlay;
328 mOverlay = nullptr;
329 }
330
331 mScrollArea = scrollArea;
332
333 if ( mScrollArea )
334 {
335 mOverlay = new QgsScrollBarHighlightOverlay( this );
336 mOverlay->scheduleUpdate();
337 }
338}
339
341{
342 return std::ceil( mLineHeight );
343}
344
346{
347 mLineHeight = lineHeight;
348}
349
351{
352 return mVisibleRange;
353}
354
356{
357 mVisibleRange = visibleRange;
358}
359
361{
362 return mMargin;
363}
364
366{
367 mMargin = margin;
368}
369
370QHash<int, QVector<QgsScrollBarHighlight>> QgsScrollBarHighlightController::highlights() const
371{
372 return mHighlights;
373}
374
376{
377 if ( !mOverlay )
378 return;
379
380 mHighlights[highlight.category] << highlight;
381 mOverlay->scheduleUpdate();
382}
383
385{
386 if ( !mOverlay )
387 return;
388
389 mHighlights.remove( category );
390 mOverlay->scheduleUpdate();
391}
392
394{
395 if ( !mOverlay )
396 return;
397
398 mHighlights.clear();
399 mOverlay->scheduleUpdate();
400}
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