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