Skip to content

Commit

Permalink
Avoid accidental child widget value changes when scrolling scroll areas
Browse files Browse the repository at this point in the history
This adds a new QgsScrollArea widget which is a subclass of QScrollArea.
QgsScrollArea has extra logic which temporarily blocks wheel events
from hitting child widgets for a short period following a scroll
of the area. This means that when scrolling a scroll area using the
mouse wheel, values won't be accidentally changed if the mouse
cursor momentarily lands on top of a widget.

QScrollArea should no longer be used in any QGIS code or plugins,
instead use QgsScrollArea to benefit from this fix.
  • Loading branch information
nyalldawson committed Mar 20, 2017
1 parent f60dc81 commit 907ad02
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 40 deletions.
2 changes: 2 additions & 0 deletions src/gui/CMakeLists.txt 100644 → 100755
Expand Up @@ -296,6 +296,7 @@ SET(QGIS_GUI_SRCS
qgsscalerangewidget.cpp
qgsscalevisibilitydialog.cpp
qgsscalewidget.cpp
qgsscrollarea.cpp
qgssearchquerybuilder.cpp
qgsshortcutsmanager.cpp
qgsslider.cpp
Expand Down Expand Up @@ -442,6 +443,7 @@ SET(QGIS_GUI_MOC_HDRS
qgsscalerangewidget.h
qgsscalevisibilitydialog.h
qgsscalewidget.h
qgsscrollarea.h
qgssearchquerybuilder.h
qgsshortcutsmanager.h
qgsslider.h
Expand Down
127 changes: 127 additions & 0 deletions src/gui/qgsscrollarea.cpp
@@ -0,0 +1,127 @@
/***************************************************************************
qgsscrollarea.cpp
-----------------
begin : March 2017
copyright : (C) 2017 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 <QEvent>
#include <QMouseEvent>
#include "qgsscrollarea.h"

// milliseconds to swallow child wheel events for after a scroll occurs
#define TIMEOUT 1000

QgsScrollArea::QgsScrollArea( QWidget *parent )
: QScrollArea( parent )
, mFilter( new ScrollAreaFilter( this, viewport() ) )
{
viewport()->installEventFilter( mFilter );
}

void QgsScrollArea::wheelEvent( QWheelEvent *e )
{
//scroll occurred, reset timer
scrollOccurred();
QScrollArea::wheelEvent( e );
}

void QgsScrollArea::scrollOccurred()
{
mTimer.setSingleShot( true );
mTimer.start( TIMEOUT );
}

bool QgsScrollArea::hasScrolled() const
{
return mTimer.isActive();
}

///@cond PRIVATE

ScrollAreaFilter::ScrollAreaFilter( QgsScrollArea *parent, QWidget *viewPort )
: QObject( parent )
, mScrollAreaWidget( parent )
, mViewPort( viewPort )
{}

bool ScrollAreaFilter::eventFilter( QObject *obj, QEvent *event )
{
switch ( event->type() )
{
case QEvent::ChildAdded:
{
// need to install filter on all child widgets as well
QChildEvent *ce = static_cast<QChildEvent *>( event );
addChild( ce->child() );
break;
}

case QEvent::ChildRemoved:
{
QChildEvent *ce = static_cast<QChildEvent *>( event );
removeChild( ce->child() );
break;
}

case QEvent::Wheel:
{
if ( obj == mViewPort )
{
// scrolling scroll area - kick off the timer to block wheel events in children
mScrollAreaWidget->scrollOccurred();
}
else
{
if ( mScrollAreaWidget->hasScrolled() )
{
// swallow wheel events for children shortly after scroll occurs
return true;
}
}
break;
}

default:
break;
}
return QObject::eventFilter( obj, event );
}

void ScrollAreaFilter::addChild( QObject *child )
{
if ( child && child->isWidgetType() )
{
child->installEventFilter( this );

// also install filter on existing children
Q_FOREACH ( QObject *c, child->children() )
{
addChild( c );
}
}
}

void ScrollAreaFilter::removeChild( QObject *child )
{
if ( child && child->isWidgetType() )
{
child->removeEventFilter( this );

// also remove filter on existing children
Q_FOREACH ( QObject *c, child->children() )
{
removeChild( c );
}
}
}

///@endcond PRIVATE
100 changes: 100 additions & 0 deletions src/gui/qgsscrollarea.h
@@ -0,0 +1,100 @@
/***************************************************************************
qgsscrollarea.h
---------------
begin : March 2017
copyright : (C) 2017 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 QGSSCROLLAREA_H
#define QGSSCROLLAREA_H

#include <QScrollArea>
#include "qgis_gui.h"
#include <QTimer>
class ScrollAreaFilter;

/**
* \class QgsScrollArea
* \ingroup gui
* A QScrollArea subclass with improved scrolling behavior.
*
* QgsScrollArea should be used instead of QScrollArea widgets.
* In most cases the use is identical, however QgsScrollArea
* has extra logic to avoid wheel events changing child widget
* values when the mouse cursor is temporarily located over
* a child widget during a scroll event.
*
* All QGIS code and plugins should use QgsScrollArea in place
* of QScrollArea.
*
* \note added in QGIS 3.0
*/
class GUI_EXPORT QgsScrollArea : public QScrollArea
{
Q_OBJECT

public:

/**
* Constructor for QgsScrollArea.
*/
explicit QgsScrollArea( QWidget *parent = nullptr );

/**
* Should be called when a scroll occurs on with the
* QScrollArea itself or its child viewport().
*/
void scrollOccurred();

/**
* Returns true if a scroll recently occurred within
* the QScrollArea or its child viewport()
*/
bool hasScrolled() const;

protected:
void wheelEvent( QWheelEvent *event ) override;

private:
QTimer mTimer;
ScrollAreaFilter *mFilter = nullptr;
};

///@cond PRIVATE

/**
* \class ScrollAreaFilter
* Swallows wheel events for QScrollArea children for a short period
* following a scroll.
*/
class ScrollAreaFilter : public QObject
{
Q_OBJECT
public:

ScrollAreaFilter( QgsScrollArea *parent = nullptr,
QWidget *viewPort = nullptr );

protected:
bool eventFilter( QObject *obj, QEvent *event ) override;

private:
QgsScrollArea *mScrollAreaWidget = nullptr;
QWidget *mViewPort = nullptr;

void addChild( QObject *child );
void removeChild( QObject *child );

};

///@endcond PRIVATE

#endif // QGSSCROLLAREA_H

0 comments on commit 907ad02

Please sign in to comment.