QGIS API Documentation 4.3.0-Master (b46232e4f9a)
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 "qgspainting.h"
21
22#include <QAbstractScrollArea>
23#include <QEvent>
24#include <QPainter>
25#include <QScrollBar>
26#include <QStyleOptionSlider>
27
28#include "moc_qgsdecoratedscrollbar.cpp"
29
31
32//
33// QgsScrollBarHighlightOverlay
34//
35
36QgsScrollBarHighlightOverlay::QgsScrollBarHighlightOverlay( QgsScrollBarHighlightController *scrollBarController )
37 : QWidget( scrollBarController->scrollArea() )
38 , mHighlightController( scrollBarController )
39{
40 setAttribute( Qt::WA_TransparentForMouseEvents );
41 scrollBar()->parentWidget()->installEventFilter( this );
42 doResize();
43 doMove();
44 setVisible( scrollBar()->isVisible() );
45}
46
47void QgsScrollBarHighlightOverlay::doResize()
48{
49 resize( scrollBar()->size() );
50}
51
52void QgsScrollBarHighlightOverlay::doMove()
53{
54 move( parentWidget()->mapFromGlobal( scrollBar()->mapToGlobal( scrollBar()->pos() ) ) );
55}
56
57void QgsScrollBarHighlightOverlay::scheduleUpdate()
58{
59 if ( mIsCacheUpdateScheduled )
60 return;
61
62 mIsCacheUpdateScheduled = true;
63// silence false positive leak warning
64#ifndef __clang_analyzer__
65 QMetaObject::invokeMethod( this, qOverload<>( &QWidget::update ), Qt::QueuedConnection );
66#endif
67}
68
69void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent )
70{
71 QWidget::paintEvent( paintEvent );
72
73 updateCache();
74
75 if ( mHighlightCache.isEmpty() )
76 return;
77
78 QPainter painter( this );
79 painter.setRenderHint( QPainter::Antialiasing, false );
80
81 const QRect &gRect = overlayRect();
82 const QRect &hRect = handleRect();
83
84 constexpr int marginX = 3;
85 constexpr int marginH = -2 * marginX + 1;
86 const QRect aboveHandleRect = QRect( gRect.x() + marginX, gRect.y(), gRect.width() + marginH, hRect.y() - gRect.y() );
87 const QRect handleRect = QRect( gRect.x() + marginX, hRect.y(), gRect.width() + marginH, hRect.height() );
88 const QRect belowHandleRect = QRect( gRect.x() + marginX, hRect.y() + hRect.height(), gRect.width() + marginH, gRect.height() - hRect.height() + gRect.y() - hRect.y() );
89
90 const int aboveValue = scrollBar()->value();
91 const int belowValue = scrollBar()->maximum() - scrollBar()->value();
92 const int sizeDocAbove = int( aboveValue * mHighlightController->lineHeight() );
93 const int sizeDocBelow = int( belowValue * mHighlightController->lineHeight() );
94 const int sizeDocVisible = int( mHighlightController->visibleRange() );
95
96 const int scrollBarBackgroundHeight = aboveHandleRect.height() + belowHandleRect.height();
97 const int sizeDocInvisible = sizeDocAbove + sizeDocBelow;
98 const double backgroundRatio = sizeDocInvisible ? ( ( double ) scrollBarBackgroundHeight / sizeDocInvisible ) : 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 ? ( ( double ) handleRect.height() / sizeDocVisible ) : 0;
118
119 // This is the hypothetical handle position if the background would
120 // be stretched using the handle ratio.
121 const double aboveVirtualHeight = sizeDocAbove * handleRatio;
122
123 // This is the accurate handle position (double)
124 const double accurateHandlePos = sizeDocAbove * backgroundRatio;
125 // The correction between handle position (int) and accurate position (double)
126 const double correction = aboveHandleRect.height() - accurateHandlePos;
127 // Skip the doc above and apply correction
128 const int offset = static_cast<int>( std::round( aboveVirtualHeight + correction ) );
129
130 drawHighlights( &painter, sizeDocAbove, sizeDocVisible, handleRatio, offset, handleRect );
131}
132
133void QgsScrollBarHighlightOverlay::drawHighlights( QPainter *painter, int docStart, int docSize, double docSizeToHandleSizeRatio, int handleOffset, const QRect &viewport )
134{
135 if ( docSize <= 0 )
136 return;
137
138 QgsScopedQPainterState painterState( painter );
139 painter->setClipRect( viewport );
140
141 const double lineHeight = mHighlightController->lineHeight();
142
143 for ( const QMap<QRgb, QMap<int, int>> &colors : std::as_const( mHighlightCache ) )
144 {
145 const auto itColorEnd = colors.constEnd();
146 for ( auto itColor = colors.constBegin(); itColor != itColorEnd; ++itColor )
147 {
148 const QColor color = itColor.key();
149 const QMap<int, int> &positions = itColor.value();
150 const auto itPosEnd = positions.constEnd();
151 const auto firstPos = int( docStart / lineHeight );
152 auto itPos = positions.upperBound( firstPos );
153 if ( itPos != positions.constBegin() )
154 --itPos;
155 while ( itPos != itPosEnd )
156 {
157 const double posStart = itPos.key() * lineHeight;
158 const double posEnd = ( itPos.value() + 1 ) * lineHeight;
159 if ( posEnd < docStart )
160 {
161 ++itPos;
162 continue;
163 }
164 if ( posStart > docStart + docSize )
165 break;
166
167 const int height = std::max( static_cast<int>( std::round( ( posEnd - posStart ) * docSizeToHandleSizeRatio ) ), 1 );
168 const int top = static_cast<int>( std::round( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y() );
169
170 const QRect rect( viewport.left(), top, viewport.width(), height );
171 painter->fillRect( rect, color );
172 ++itPos;
173 }
174 }
175 }
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
292
293
294//
295// QgsScrollBarHighlightController
296//
297
299
301{
302 if ( mOverlay )
303 delete mOverlay;
304}
305
307{
308 if ( mScrollArea )
309 return mScrollArea->verticalScrollBar();
310
311 return nullptr;
312}
313
315{
316 return mScrollArea;
317}
318
320{
321 if ( mScrollArea == scrollArea )
322 return;
323
324 if ( mOverlay )
325 {
326 delete mOverlay;
327 mOverlay = nullptr;
328 }
329
330 mScrollArea = scrollArea;
331
332 if ( mScrollArea )
333 {
334 mOverlay = new QgsScrollBarHighlightOverlay( this );
335 mOverlay->scheduleUpdate();
336 }
337}
338
340{
341 return std::ceil( mLineHeight );
342}
343
345{
346 mLineHeight = lineHeight;
347}
348
350{
351 return mVisibleRange;
352}
353
358
360{
361 return mMargin;
362}
363
365{
366 mMargin = margin;
367}
368
369QHash<int, QVector<QgsScrollBarHighlight>> QgsScrollBarHighlightController::highlights() const
370{
371 return mHighlights;
372}
373
375{
376 if ( !mOverlay )
377 return;
378
379 mHighlights[highlight.category] << highlight;
380 mOverlay->scheduleUpdate();
381}
382
384{
385 if ( !mOverlay )
386 return;
387
388 mHighlights.remove( category );
389 mOverlay->scheduleUpdate();
390}
391
393{
394 if ( !mOverlay )
395 return;
396
397 mHighlights.clear();
398 mOverlay->scheduleUpdate();
399}
Scoped object for saving and restoring a QPainter object's state.
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.