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