From f8dd821248f30ab65a1df8e044a5ece0440a0994 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 11:26:43 +1000 Subject: [PATCH 1/4] Port decorated scrollbar widget class from QtCreator Allows decorating scrollbars with colored highlight bars --- .../auto_additions/qgsdecoratedscrollbar.py | 9 + .../qgsdecoratedscrollbar.sip.in | 153 +++++++ python/PyQt6/gui/gui_auto.sip | 1 + .../auto_additions/qgsdecoratedscrollbar.py | 9 + .../qgsdecoratedscrollbar.sip.in | 153 +++++++ python/gui/gui_auto.sip | 1 + src/gui/CMakeLists.txt | 2 + src/gui/qgsdecoratedscrollbar.cpp | 424 ++++++++++++++++++ src/gui/qgsdecoratedscrollbar.h | 215 +++++++++ 9 files changed, 967 insertions(+) create mode 100644 python/PyQt6/gui/auto_additions/qgsdecoratedscrollbar.py create mode 100644 python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in create mode 100644 python/gui/auto_additions/qgsdecoratedscrollbar.py create mode 100644 python/gui/auto_generated/qgsdecoratedscrollbar.sip.in create mode 100644 src/gui/qgsdecoratedscrollbar.cpp create mode 100644 src/gui/qgsdecoratedscrollbar.h diff --git a/python/PyQt6/gui/auto_additions/qgsdecoratedscrollbar.py b/python/PyQt6/gui/auto_additions/qgsdecoratedscrollbar.py new file mode 100644 index 000000000000..a9d54d7d6f82 --- /dev/null +++ b/python/PyQt6/gui/auto_additions/qgsdecoratedscrollbar.py @@ -0,0 +1,9 @@ +# The following has been generated automatically from src/gui/qgsdecoratedscrollbar.h +# monkey patching scoped based enum +QgsScrollBarHighlight.Priority.Invalid.__doc__ = "Invalid" +QgsScrollBarHighlight.Priority.LowPriority.__doc__ = "Low priority, rendered below all other highlights" +QgsScrollBarHighlight.Priority.NormalPriority.__doc__ = "Normal priority" +QgsScrollBarHighlight.Priority.HighPriority.__doc__ = "High priority" +QgsScrollBarHighlight.Priority.HighestPriority.__doc__ = "Highest priority, rendered above all other highlights" +QgsScrollBarHighlight.Priority.__doc__ = "Priority, which dictates how overlapping highlights are rendered\n\n" + '* ``Invalid``: ' + QgsScrollBarHighlight.Priority.Invalid.__doc__ + '\n' + '* ``LowPriority``: ' + QgsScrollBarHighlight.Priority.LowPriority.__doc__ + '\n' + '* ``NormalPriority``: ' + QgsScrollBarHighlight.Priority.NormalPriority.__doc__ + '\n' + '* ``HighPriority``: ' + QgsScrollBarHighlight.Priority.HighPriority.__doc__ + '\n' + '* ``HighestPriority``: ' + QgsScrollBarHighlight.Priority.HighestPriority.__doc__ +# -- diff --git a/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in b/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in new file mode 100644 index 000000000000..b5de42ab39bd --- /dev/null +++ b/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in @@ -0,0 +1,153 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsdecoratedscrollbar.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsScrollBarHighlight +{ +%Docstring(signature="appended") +Encapsulates the details of a highlight in a scrollbar, used alongside :py:class:`QgsScrollBarHighlightController`. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsdecoratedscrollbar.h" +%End + public: + + enum class Priority /BaseType=IntEnum/ + { + Invalid, + LowPriority, + NormalPriority, + HighPriority, + HighestPriority + }; + + QgsScrollBarHighlight( int category, int position, const QColor &color, QgsScrollBarHighlight::Priority priority = QgsScrollBarHighlight::Priority::NormalPriority ); +%Docstring +Constructor for QgsScrollBarHighlight. +%End + QgsScrollBarHighlight(); + + int category; + + int position; + + QColor color; + + QgsScrollBarHighlight::Priority priority; +}; + +class QgsScrollBarHighlightController +{ +%Docstring(signature="appended") +Adds highlights (colored markers) to a scrollbar. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsdecoratedscrollbar.h" +%End + public: + + QgsScrollBarHighlightController(); + ~QgsScrollBarHighlightController(); + + QScrollBar *scrollBar() const; +%Docstring +Returns the associated scroll bar. +%End + + QAbstractScrollArea *scrollArea() const; +%Docstring +Returns the associated scroll area. + +.. seealso:: :py:func:`setScrollArea` +%End + + void setScrollArea( QAbstractScrollArea *scrollArea ); +%Docstring +Sets the associated scroll bar. + +.. seealso:: :py:func:`scrollArea` +%End + + double lineHeight() const; +%Docstring +Returns the line height for text associated with the scroll area. + +.. seealso:: :py:func:`setLineHeight` +%End + + void setLineHeight( double height ); +%Docstring +Sets the line ``height`` for text associated with the scroll area. + +.. seealso:: :py:func:`lineHeight` +%End + + double visibleRange() const; +%Docstring +Returns the visible range of the scroll area (i.e. the viewport's height). + +.. seealso:: :py:func:`setVisibleRange` +%End + + void setVisibleRange( double visibleRange ); +%Docstring +Sets the visible range of the scroll area (i.e. the viewport's height). + +.. seealso:: :py:func:`visibleRange` +%End + + double margin() const; +%Docstring +Returns the document margins for the associated viewport. + +.. seealso:: :py:func:`setMargin` +%End + + void setMargin( double margin ); +%Docstring +Sets the document ``margin`` for the associated viewport. + +.. seealso:: :py:func:`margin` +%End + + + void addHighlight( const QgsScrollBarHighlight &highlight ); +%Docstring +Adds a ``highlight`` to the scrollbar. +%End + + void removeHighlights( int category ); +%Docstring +Removes all highlights with matching ``category`` from the scrollbar. +%End + + void removeAllHighlights(); +%Docstring +Removes all highlights from the scroll bar. +%End + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsdecoratedscrollbar.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/gui/gui_auto.sip b/python/PyQt6/gui/gui_auto.sip index c668cc6c9a90..19789808f04b 100644 --- a/python/PyQt6/gui/gui_auto.sip +++ b/python/PyQt6/gui/gui_auto.sip @@ -46,6 +46,7 @@ %Include auto_generated/qgsdataitemguiproviderregistry.sip %Include auto_generated/qgsdatasourceselectdialog.sip %Include auto_generated/qgsdbrelationshipwidget.sip +%Include auto_generated/qgsdecoratedscrollbar.sip %Include auto_generated/qgsnewdatabasetablenamewidget.sip %Include auto_generated/qgsdetaileditemdata.sip %Include auto_generated/qgsdetaileditemdelegate.sip diff --git a/python/gui/auto_additions/qgsdecoratedscrollbar.py b/python/gui/auto_additions/qgsdecoratedscrollbar.py new file mode 100644 index 000000000000..a9d54d7d6f82 --- /dev/null +++ b/python/gui/auto_additions/qgsdecoratedscrollbar.py @@ -0,0 +1,9 @@ +# The following has been generated automatically from src/gui/qgsdecoratedscrollbar.h +# monkey patching scoped based enum +QgsScrollBarHighlight.Priority.Invalid.__doc__ = "Invalid" +QgsScrollBarHighlight.Priority.LowPriority.__doc__ = "Low priority, rendered below all other highlights" +QgsScrollBarHighlight.Priority.NormalPriority.__doc__ = "Normal priority" +QgsScrollBarHighlight.Priority.HighPriority.__doc__ = "High priority" +QgsScrollBarHighlight.Priority.HighestPriority.__doc__ = "Highest priority, rendered above all other highlights" +QgsScrollBarHighlight.Priority.__doc__ = "Priority, which dictates how overlapping highlights are rendered\n\n" + '* ``Invalid``: ' + QgsScrollBarHighlight.Priority.Invalid.__doc__ + '\n' + '* ``LowPriority``: ' + QgsScrollBarHighlight.Priority.LowPriority.__doc__ + '\n' + '* ``NormalPriority``: ' + QgsScrollBarHighlight.Priority.NormalPriority.__doc__ + '\n' + '* ``HighPriority``: ' + QgsScrollBarHighlight.Priority.HighPriority.__doc__ + '\n' + '* ``HighestPriority``: ' + QgsScrollBarHighlight.Priority.HighestPriority.__doc__ +# -- diff --git a/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in b/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in new file mode 100644 index 000000000000..c9a8a0fb389c --- /dev/null +++ b/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in @@ -0,0 +1,153 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsdecoratedscrollbar.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsScrollBarHighlight +{ +%Docstring(signature="appended") +Encapsulates the details of a highlight in a scrollbar, used alongside :py:class:`QgsScrollBarHighlightController`. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsdecoratedscrollbar.h" +%End + public: + + enum class Priority + { + Invalid, + LowPriority, + NormalPriority, + HighPriority, + HighestPriority + }; + + QgsScrollBarHighlight( int category, int position, const QColor &color, QgsScrollBarHighlight::Priority priority = QgsScrollBarHighlight::Priority::NormalPriority ); +%Docstring +Constructor for QgsScrollBarHighlight. +%End + QgsScrollBarHighlight(); + + int category; + + int position; + + QColor color; + + QgsScrollBarHighlight::Priority priority; +}; + +class QgsScrollBarHighlightController +{ +%Docstring(signature="appended") +Adds highlights (colored markers) to a scrollbar. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsdecoratedscrollbar.h" +%End + public: + + QgsScrollBarHighlightController(); + ~QgsScrollBarHighlightController(); + + QScrollBar *scrollBar() const; +%Docstring +Returns the associated scroll bar. +%End + + QAbstractScrollArea *scrollArea() const; +%Docstring +Returns the associated scroll area. + +.. seealso:: :py:func:`setScrollArea` +%End + + void setScrollArea( QAbstractScrollArea *scrollArea ); +%Docstring +Sets the associated scroll bar. + +.. seealso:: :py:func:`scrollArea` +%End + + double lineHeight() const; +%Docstring +Returns the line height for text associated with the scroll area. + +.. seealso:: :py:func:`setLineHeight` +%End + + void setLineHeight( double height ); +%Docstring +Sets the line ``height`` for text associated with the scroll area. + +.. seealso:: :py:func:`lineHeight` +%End + + double visibleRange() const; +%Docstring +Returns the visible range of the scroll area (i.e. the viewport's height). + +.. seealso:: :py:func:`setVisibleRange` +%End + + void setVisibleRange( double visibleRange ); +%Docstring +Sets the visible range of the scroll area (i.e. the viewport's height). + +.. seealso:: :py:func:`visibleRange` +%End + + double margin() const; +%Docstring +Returns the document margins for the associated viewport. + +.. seealso:: :py:func:`setMargin` +%End + + void setMargin( double margin ); +%Docstring +Sets the document ``margin`` for the associated viewport. + +.. seealso:: :py:func:`margin` +%End + + + void addHighlight( const QgsScrollBarHighlight &highlight ); +%Docstring +Adds a ``highlight`` to the scrollbar. +%End + + void removeHighlights( int category ); +%Docstring +Removes all highlights with matching ``category`` from the scrollbar. +%End + + void removeAllHighlights(); +%Docstring +Removes all highlights from the scroll bar. +%End + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsdecoratedscrollbar.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index c668cc6c9a90..19789808f04b 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -46,6 +46,7 @@ %Include auto_generated/qgsdataitemguiproviderregistry.sip %Include auto_generated/qgsdatasourceselectdialog.sip %Include auto_generated/qgsdbrelationshipwidget.sip +%Include auto_generated/qgsdecoratedscrollbar.sip %Include auto_generated/qgsnewdatabasetablenamewidget.sip %Include auto_generated/qgsdetaileditemdata.sip %Include auto_generated/qgsdetaileditemdelegate.sip diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 89df2a440e22..a9e57f03d6c6 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -553,6 +553,7 @@ set(QGIS_GUI_SRCS qgsdatasourceselectdialog.cpp qgsdbqueryhistoryprovider.cpp qgsdbrelationshipwidget.cpp + qgsdecoratedscrollbar.cpp qgsdetaileditemdata.cpp qgsdetaileditemdelegate.cpp qgsdetaileditemwidget.cpp @@ -824,6 +825,7 @@ set(QGIS_GUI_HDRS qgsdatasourceselectdialog.h qgsdbqueryhistoryprovider.h qgsdbrelationshipwidget.h + qgsdecoratedscrollbar.h qgsnewdatabasetablenamewidget.h qgsdetaileditemdata.h qgsdetaileditemdelegate.h diff --git a/src/gui/qgsdecoratedscrollbar.cpp b/src/gui/qgsdecoratedscrollbar.cpp new file mode 100644 index 000000000000..6c27a47d0132 --- /dev/null +++ b/src/gui/qgsdecoratedscrollbar.cpp @@ -0,0 +1,424 @@ +/*************************************************************************** + qgsdecoratedscrollbar.cpp + -------------------------------------- + Date : May 2024 + Copyright : (C) 2024 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsdecoratedscrollbar.h" +#include +#include +#include +#include +#include +#include + +///@cond PRIVATE + +// +// QgsScrollBarHighlightOverlay +// + +QgsScrollBarHighlightOverlay::QgsScrollBarHighlightOverlay( QgsScrollBarHighlightController *scrollBarController ) + : QWidget( scrollBarController->scrollArea() ) + , mHighlightController( scrollBarController ) +{ + setAttribute( Qt::WA_TransparentForMouseEvents ); + scrollBar()->parentWidget()->installEventFilter( this ); + doResize(); + doMove(); + setVisible( scrollBar()->isVisible() ); +} + +void QgsScrollBarHighlightOverlay::doResize() +{ + resize( scrollBar()->size() ); +} + +void QgsScrollBarHighlightOverlay::doMove() +{ + move( parentWidget()->mapFromGlobal( scrollBar()->mapToGlobal( scrollBar()->pos() ) ) ); +} + +void QgsScrollBarHighlightOverlay::scheduleUpdate() +{ + if ( mIsCacheUpdateScheduled ) + return; + + mIsCacheUpdateScheduled = true; + QMetaObject::invokeMethod( this, QOverload<>::of( &QWidget::update ), Qt::QueuedConnection ); +} + +void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) +{ + QWidget::paintEvent( paintEvent ); + + updateCache(); + + if ( mHighlightCache.isEmpty() ) + return; + + QPainter painter( this ); + painter.setRenderHint( QPainter::Antialiasing, false ); + + const QRect &gRect = overlayRect(); + const QRect &hRect = handleRect(); + + constexpr int marginX = 3; + constexpr int marginH = -2 * marginX + 1; + const QRect aboveHandleRect = QRect( gRect.x() + marginX, + gRect.y(), + gRect.width() + marginH, + hRect.y() - gRect.y() ); + const QRect handleRect = QRect( gRect.x() + marginX, + hRect.y(), + gRect.width() + marginH, + hRect.height() ); + const QRect belowHandleRect = QRect( gRect.x() + marginX, + hRect.y() + hRect.height(), + gRect.width() + marginH, + gRect.height() - hRect.height() + gRect.y() - hRect.y() ); + + const int aboveValue = scrollBar()->value(); + const int belowValue = scrollBar()->maximum() - scrollBar()->value(); + const int sizeDocAbove = int( aboveValue * mHighlightController->lineHeight() ); + const int sizeDocBelow = int( belowValue * mHighlightController->lineHeight() ); + const int sizeDocVisible = int( mHighlightController->visibleRange() ); + + const int scrollBarBackgroundHeight = aboveHandleRect.height() + belowHandleRect.height(); + const int sizeDocInvisible = sizeDocAbove + sizeDocBelow; + const double backgroundRatio = sizeDocInvisible + ? ( ( double )scrollBarBackgroundHeight / sizeDocInvisible ) : 0; + + + if ( aboveValue ) + { + drawHighlights( &painter, + 0, + sizeDocAbove, + backgroundRatio, + 0, + aboveHandleRect ); + } + + if ( belowValue ) + { + // This is the hypothetical handle height if the handle would + // be stretched using the background ratio. + const double handleVirtualHeight = sizeDocVisible * backgroundRatio; + // Skip the doc above and visible part. + const int offset = qRound( aboveHandleRect.height() + handleVirtualHeight ); + + drawHighlights( &painter, + sizeDocAbove + sizeDocVisible, + sizeDocBelow, + backgroundRatio, + offset, + belowHandleRect ); + } + + const double handleRatio = sizeDocVisible + ? ( ( double )handleRect.height() / sizeDocVisible ) : 0; + + // This is the hypothetical handle position if the background would + // be stretched using the handle ratio. + const double aboveVirtualHeight = sizeDocAbove * handleRatio; + + // This is the accurate handle position (double) + const double accurateHandlePos = sizeDocAbove * backgroundRatio; + // The correction between handle position (int) and accurate position (double) + const double correction = aboveHandleRect.height() - accurateHandlePos; + // Skip the doc above and apply correction + const int offset = qRound( aboveVirtualHeight + correction ); + + drawHighlights( &painter, + sizeDocAbove, + sizeDocVisible, + handleRatio, + offset, + handleRect ); +} + +void QgsScrollBarHighlightOverlay::drawHighlights( QPainter *painter, + int docStart, + int docSize, + double docSizeToHandleSizeRatio, + int handleOffset, + const QRect &viewport ) +{ + if ( docSize <= 0 ) + return; + + painter->save(); + painter->setClipRect( viewport ); + + const double lineHeight = mHighlightController->lineHeight(); + + for ( const QMap> &colors : std::as_const( mHighlightCache ) ) + { + const auto itColorEnd = colors.constEnd(); + for ( auto itColor = colors.constBegin(); itColor != itColorEnd; ++itColor ) + { + const QColor color = itColor.key(); + const QMap &positions = itColor.value(); + const auto itPosEnd = positions.constEnd(); + const auto firstPos = int( docStart / lineHeight ); + auto itPos = positions.upperBound( firstPos ); + if ( itPos != positions.constBegin() ) + --itPos; + while ( itPos != itPosEnd ) + { + const double posStart = itPos.key() * lineHeight; + const double posEnd = ( itPos.value() + 1 ) * lineHeight; + if ( posEnd < docStart ) + { + ++itPos; + continue; + } + if ( posStart > docStart + docSize ) + break; + + const int height = qMax( qRound( ( posEnd - posStart ) * docSizeToHandleSizeRatio ), 1 ); + const int top = qRound( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y(); + + const QRect rect( viewport.left(), top, viewport.width(), height ); + painter->fillRect( rect, color ); + ++itPos; + } + } + } + painter->restore(); +} + +bool QgsScrollBarHighlightOverlay::eventFilter( QObject *object, QEvent *event ) +{ + switch ( event->type() ) + { + case QEvent::Move: + doMove(); + break; + case QEvent::Resize: + doResize(); + break; + case QEvent::ZOrderChange: + raise(); + break; + case QEvent::Show: + show(); + break; + case QEvent::Hide: + hide(); + break; + default: + break; + } + return QWidget::eventFilter( object, event ); +} + +static void insertPosition( QMap *map, int position ) +{ + auto itNext = map->upperBound( position ); + + bool gluedWithPrev = false; + if ( itNext != map->begin() ) + { + auto itPrev = std::prev( itNext ); + const int keyStart = itPrev.key(); + const int keyEnd = itPrev.value(); + if ( position >= keyStart && position <= keyEnd ) + return; // pos is already included + + if ( keyEnd + 1 == position ) + { + // glue with prev + ( *itPrev )++; + gluedWithPrev = true; + } + } + + if ( itNext != map->end() && itNext.key() == position + 1 ) + { + const int keyEnd = itNext.value(); + itNext = map->erase( itNext ); + if ( gluedWithPrev ) + { + // glue with prev and next + auto itPrev = std::prev( itNext ); + *itPrev = keyEnd; + } + else + { + // glue with next + itNext = map->insert( itNext, position, keyEnd ); + } + return; // glued + } + + if ( gluedWithPrev ) + return; // glued + + map->insert( position, position ); +} + +void QgsScrollBarHighlightOverlay::updateCache() +{ + if ( !mIsCacheUpdateScheduled ) + return; + + mHighlightCache.clear(); + + const QHash> highlightsForId = mHighlightController->highlights(); + for ( const QVector &highlights : highlightsForId ) + { + for ( const QgsScrollBarHighlight &highlight : highlights ) + { + QMap &highlightMap = mHighlightCache[highlight.priority][highlight.color.rgba()]; + insertPosition( &highlightMap, highlight.position ); + } + } + + mIsCacheUpdateScheduled = false; +} + +QRect QgsScrollBarHighlightOverlay::overlayRect() const +{ + QStyleOptionSlider opt = qt_qscrollbarStyleOption( scrollBar() ); + return scrollBar()->style()->subControlRect( QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, scrollBar() ); +} + +QRect QgsScrollBarHighlightOverlay::handleRect() const +{ + QStyleOptionSlider opt = qt_qscrollbarStyleOption( scrollBar() ); + return scrollBar()->style()->subControlRect( QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, scrollBar() ); +} + +///@endcond PRIVATE + +// +// QgsScrollBarHighlight +// + +QgsScrollBarHighlight::QgsScrollBarHighlight( int category, int position, + const QColor &color, QgsScrollBarHighlight::Priority priority ) + : category( category ) + , position( position ) + , color( color ) + , priority( priority ) +{ +} + + +// +// QgsScrollBarHighlightController +// + +QgsScrollBarHighlightController::QgsScrollBarHighlightController() = default; + +QgsScrollBarHighlightController::~QgsScrollBarHighlightController() +{ + if ( mOverlay ) + delete mOverlay; +} + +QScrollBar *QgsScrollBarHighlightController::scrollBar() const +{ + if ( mScrollArea ) + return mScrollArea->verticalScrollBar(); + + return nullptr; +} + +QAbstractScrollArea *QgsScrollBarHighlightController::scrollArea() const +{ + return mScrollArea; +} + +void QgsScrollBarHighlightController::setScrollArea( QAbstractScrollArea *scrollArea ) +{ + if ( mScrollArea == scrollArea ) + return; + + if ( mOverlay ) + { + delete mOverlay; + mOverlay = nullptr; + } + + mScrollArea = scrollArea; + + if ( mScrollArea ) + { + mOverlay = new QgsScrollBarHighlightOverlay( this ); + mOverlay->scheduleUpdate(); + } +} + +double QgsScrollBarHighlightController::lineHeight() const +{ + return std::ceil( mLineHeight ); +} + +void QgsScrollBarHighlightController::setLineHeight( double lineHeight ) +{ + mLineHeight = lineHeight; +} + +double QgsScrollBarHighlightController::visibleRange() const +{ + return mVisibleRange; +} + +void QgsScrollBarHighlightController::setVisibleRange( double visibleRange ) +{ + mVisibleRange = visibleRange; +} + +double QgsScrollBarHighlightController::margin() const +{ + return mMargin; +} + +void QgsScrollBarHighlightController::setMargin( double margin ) +{ + mMargin = margin; +} + +QHash> QgsScrollBarHighlightController::highlights() const +{ + return mHighlights; +} + +void QgsScrollBarHighlightController::addHighlight( const QgsScrollBarHighlight &highlight ) +{ + if ( !mOverlay ) + return; + + mHighlights[highlight.category] << highlight; + mOverlay->scheduleUpdate(); +} + +void QgsScrollBarHighlightController::removeHighlights( int category ) +{ + if ( !mOverlay ) + return; + + mHighlights.remove( category ); + mOverlay->scheduleUpdate(); +} + +void QgsScrollBarHighlightController::removeAllHighlights() +{ + if ( !mOverlay ) + return; + + mHighlights.clear(); + mOverlay->scheduleUpdate(); +} diff --git a/src/gui/qgsdecoratedscrollbar.h b/src/gui/qgsdecoratedscrollbar.h new file mode 100644 index 000000000000..a2084b0f1600 --- /dev/null +++ b/src/gui/qgsdecoratedscrollbar.h @@ -0,0 +1,215 @@ +/*************************************************************************** + qgsdecoratedscrollbar.h + -------------------------------------- + Date : May 2024 + Copyright : (C) 2024 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSDECORATEDSCROLLBAR_H +#define QGSDECORATEDSCROLLBAR_H + +#include "qgis_gui.h" +#include "qgis_sip.h" + +#include +#include +#include +#include +#include + +class QScrollBar; +class QAbstractScrollArea; +class QgsScrollBarHighlightOverlay; + +// ported from QtCreator's HighlightScrollBarController implementation + +/** + * \ingroup gui + * \brief Encapsulates the details of a highlight in a scrollbar, used alongside QgsScrollBarHighlightController. + * \since QGIS 3.38 + */ +class GUI_EXPORT QgsScrollBarHighlight +{ + public: + + /** + * Priority, which dictates how overlapping highlights are rendered + */ + enum class Priority : int + { + Invalid = -1, //!< Invalid + LowPriority = 0, //!< Low priority, rendered below all other highlights + NormalPriority = 1, //!< Normal priority + HighPriority = 2, //!< High priority + HighestPriority = 3 //!< Highest priority, rendered above all other highlights + }; + + /** + * Constructor for QgsScrollBarHighlight. + */ + QgsScrollBarHighlight( int category, int position, const QColor &color, QgsScrollBarHighlight::Priority priority = QgsScrollBarHighlight::Priority::NormalPriority ); + QgsScrollBarHighlight() = default; + + //! Category ID + int category = -1; + + //! Position in scroll bar + int position = -1; + + //! Highlight color + QColor color; + + //! Priority, which dictates how overlapping highlights are rendered + QgsScrollBarHighlight::Priority priority = QgsScrollBarHighlight::Priority::Invalid; +}; + +/** + * \ingroup gui + * \brief Adds highlights (colored markers) to a scrollbar. + * \since QGIS 3.38 + */ +class GUI_EXPORT QgsScrollBarHighlightController +{ + public: + + QgsScrollBarHighlightController(); + ~QgsScrollBarHighlightController(); + + /** + * Returns the associated scroll bar. + */ + QScrollBar *scrollBar() const; + + /** + * Returns the associated scroll area. + * + * \see setScrollArea() + */ + QAbstractScrollArea *scrollArea() const; + + /** + * Sets the associated scroll bar. + * + * \see scrollArea() + */ + void setScrollArea( QAbstractScrollArea *scrollArea ); + + /** + * Returns the line height for text associated with the scroll area. + * + * \see setLineHeight() + */ + double lineHeight() const; + + /** + * Sets the line \a height for text associated with the scroll area. + * + * \see lineHeight() + */ + void setLineHeight( double height ); + + /** + * Returns the visible range of the scroll area (i.e. the viewport's height). + * + * \see setVisibleRange() + */ + double visibleRange() const; + + /** + * Sets the visible range of the scroll area (i.e. the viewport's height). + * + * \see visibleRange() + */ + void setVisibleRange( double visibleRange ); + + /** + * Returns the document margins for the associated viewport. + * + * \see setMargin() + */ + double margin() const; + + /** + * Sets the document \a margin for the associated viewport. + * + * \see margin() + */ + void setMargin( double margin ); + + /** + * Returns the hash of all highlights in the scrollbar, with highlight categories as hash keys. + * + * \note Not available in Python bindings + */ + QHash> highlights() const SIP_SKIP; + + /** + * Adds a \a highlight to the scrollbar. + */ + void addHighlight( const QgsScrollBarHighlight &highlight ); + + /** + * Removes all highlights with matching \a category from the scrollbar. + */ + void removeHighlights( int category ); + + /** + * Removes all highlights from the scroll bar. + */ + void removeAllHighlights(); + + private: + + QHash > mHighlights; + double mLineHeight = 0.0; + double mVisibleRange = 0.0; // in pixels + double mMargin = 0.0; // in pixels + QAbstractScrollArea *mScrollArea = nullptr; + QPointer mOverlay; +}; + +///@cond PRIVATE +#ifndef SIP_RUN +class QgsScrollBarHighlightOverlay : public QWidget +{ + public: + QgsScrollBarHighlightOverlay( QgsScrollBarHighlightController *scrollBarController ); + + void doResize(); + void doMove(); + void scheduleUpdate(); + + protected: + void paintEvent( QPaintEvent *paintEvent ) override; + bool eventFilter( QObject *object, QEvent *event ) override; + + private: + void drawHighlights( QPainter *painter, + int docStart, + int docSize, + double docSizeToHandleSizeRatio, + int handleOffset, + const QRect &viewport ); + void updateCache(); + QRect overlayRect() const; + QRect handleRect() const; + + // line start to line end + QMap>> mHighlightCache; + + inline QScrollBar *scrollBar() const { return mHighlightController->scrollBar(); } + QgsScrollBarHighlightController *mHighlightController = nullptr; + bool mIsCacheUpdateScheduled = true; +}; +#endif +///@endcond PRIVATE + +#endif // QGSDECORATEDSCROLLBAR_H From e2a39502e63ba2aeb88012c7a68be2b24c2a6289 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 12:16:57 +1000 Subject: [PATCH 2/4] Fix some checks --- .../PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in | 4 ++++ python/gui/auto_generated/qgsdecoratedscrollbar.sip.in | 4 ++++ src/gui/qgsdecoratedscrollbar.cpp | 8 ++++---- src/gui/qgsdecoratedscrollbar.h | 6 ++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in b/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in index b5de42ab39bd..66663e70b669 100644 --- a/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in @@ -37,7 +37,11 @@ Encapsulates the details of a highlight in a scrollbar, used alongside :py:class %Docstring Constructor for QgsScrollBarHighlight. %End + QgsScrollBarHighlight(); +%Docstring +Default constructor for QgsScrollBarHighlight. +%End int category; diff --git a/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in b/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in index c9a8a0fb389c..d26e35aaf6b7 100644 --- a/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in +++ b/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in @@ -37,7 +37,11 @@ Encapsulates the details of a highlight in a scrollbar, used alongside :py:class %Docstring Constructor for QgsScrollBarHighlight. %End + QgsScrollBarHighlight(); +%Docstring +Default constructor for QgsScrollBarHighlight. +%End int category; diff --git a/src/gui/qgsdecoratedscrollbar.cpp b/src/gui/qgsdecoratedscrollbar.cpp index 6c27a47d0132..e7d0804b8736 100644 --- a/src/gui/qgsdecoratedscrollbar.cpp +++ b/src/gui/qgsdecoratedscrollbar.cpp @@ -115,7 +115,7 @@ void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) // be stretched using the background ratio. const double handleVirtualHeight = sizeDocVisible * backgroundRatio; // Skip the doc above and visible part. - const int offset = qRound( aboveHandleRect.height() + handleVirtualHeight ); + const int offset = std::round( aboveHandleRect.height() + handleVirtualHeight ); drawHighlights( &painter, sizeDocAbove + sizeDocVisible, @@ -137,7 +137,7 @@ void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) // The correction between handle position (int) and accurate position (double) const double correction = aboveHandleRect.height() - accurateHandlePos; // Skip the doc above and apply correction - const int offset = qRound( aboveVirtualHeight + correction ); + const int offset = std::round( aboveVirtualHeight + correction ); drawHighlights( &painter, sizeDocAbove, @@ -186,8 +186,8 @@ void QgsScrollBarHighlightOverlay::drawHighlights( QPainter *painter, if ( posStart > docStart + docSize ) break; - const int height = qMax( qRound( ( posEnd - posStart ) * docSizeToHandleSizeRatio ), 1 ); - const int top = qRound( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y(); + const int height = std::max( static_cast< int >( std::round( ( posEnd - posStart ) * docSizeToHandleSizeRatio ) ), 1 ); + const int top = std::round( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y(); const QRect rect( viewport.left(), top, viewport.width(), height ); painter->fillRect( rect, color ); diff --git a/src/gui/qgsdecoratedscrollbar.h b/src/gui/qgsdecoratedscrollbar.h index a2084b0f1600..fc85120faf9b 100644 --- a/src/gui/qgsdecoratedscrollbar.h +++ b/src/gui/qgsdecoratedscrollbar.h @@ -56,6 +56,10 @@ class GUI_EXPORT QgsScrollBarHighlight * Constructor for QgsScrollBarHighlight. */ QgsScrollBarHighlight( int category, int position, const QColor &color, QgsScrollBarHighlight::Priority priority = QgsScrollBarHighlight::Priority::NormalPriority ); + + /** + * Default constructor for QgsScrollBarHighlight. + */ QgsScrollBarHighlight() = default; //! Category ID @@ -180,6 +184,8 @@ class GUI_EXPORT QgsScrollBarHighlightController #ifndef SIP_RUN class QgsScrollBarHighlightOverlay : public QWidget { + Q_OBJECT + public: QgsScrollBarHighlightOverlay( QgsScrollBarHighlightController *scrollBarController ); From 4fca7e0749d6d083d78352c26a00b45212fa73df Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 12:29:35 +1000 Subject: [PATCH 3/4] Use qOverload --- src/gui/qgsdecoratedscrollbar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/qgsdecoratedscrollbar.cpp b/src/gui/qgsdecoratedscrollbar.cpp index e7d0804b8736..5506df773d3b 100644 --- a/src/gui/qgsdecoratedscrollbar.cpp +++ b/src/gui/qgsdecoratedscrollbar.cpp @@ -54,7 +54,7 @@ void QgsScrollBarHighlightOverlay::scheduleUpdate() return; mIsCacheUpdateScheduled = true; - QMetaObject::invokeMethod( this, QOverload<>::of( &QWidget::update ), Qt::QueuedConnection ); + QMetaObject::invokeMethod( this, qOverload<>( &QWidget::update ), Qt::QueuedConnection ); } void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) From 395bc5d848512723fc69b011c0e4f4944d2f8005 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 13:48:56 +1000 Subject: [PATCH 4/4] Fix clang tidy warnings --- src/gui/qgsdecoratedscrollbar.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/gui/qgsdecoratedscrollbar.cpp b/src/gui/qgsdecoratedscrollbar.cpp index 5506df773d3b..791ccbcdd85e 100644 --- a/src/gui/qgsdecoratedscrollbar.cpp +++ b/src/gui/qgsdecoratedscrollbar.cpp @@ -54,7 +54,10 @@ void QgsScrollBarHighlightOverlay::scheduleUpdate() return; mIsCacheUpdateScheduled = true; +// silence false positive leak warning +#ifndef __clang_analyzer__ QMetaObject::invokeMethod( this, qOverload<>( &QWidget::update ), Qt::QueuedConnection ); +#endif } void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) @@ -115,7 +118,7 @@ void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) // be stretched using the background ratio. const double handleVirtualHeight = sizeDocVisible * backgroundRatio; // Skip the doc above and visible part. - const int offset = std::round( aboveHandleRect.height() + handleVirtualHeight ); + const int offset = static_cast< int >( std::round( aboveHandleRect.height() + handleVirtualHeight ) ); drawHighlights( &painter, sizeDocAbove + sizeDocVisible, @@ -137,7 +140,7 @@ void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) // The correction between handle position (int) and accurate position (double) const double correction = aboveHandleRect.height() - accurateHandlePos; // Skip the doc above and apply correction - const int offset = std::round( aboveVirtualHeight + correction ); + const int offset = static_cast< int >( std::round( aboveVirtualHeight + correction ) ); drawHighlights( &painter, sizeDocAbove, @@ -187,7 +190,7 @@ void QgsScrollBarHighlightOverlay::drawHighlights( QPainter *painter, break; const int height = std::max( static_cast< int >( std::round( ( posEnd - posStart ) * docSizeToHandleSizeRatio ) ), 1 ); - const int top = std::round( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y(); + const int top = static_cast< int >( std::round( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y() ); const QRect rect( viewport.left(), top, viewport.width(), height ); painter->fillRect( rect, color );