Avoid accidental child widget value changes when scrolling scroll areas

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.
This commit is contained in:
Nyall Dawson 2017-03-17 16:01:31 +10:00
parent f60dc81102
commit 907ad02af8
4 changed files with 275 additions and 40 deletions

2
src/gui/CMakeLists.txt Normal file → Executable file
View File

@ -296,6 +296,7 @@ SET(QGIS_GUI_SRCS
qgsscalerangewidget.cpp
qgsscalevisibilitydialog.cpp
qgsscalewidget.cpp
qgsscrollarea.cpp
qgssearchquerybuilder.cpp
qgsshortcutsmanager.cpp
qgsslider.cpp
@ -442,6 +443,7 @@ SET(QGIS_GUI_MOC_HDRS
qgsscalerangewidget.h
qgsscalevisibilitydialog.h
qgsscalewidget.h
qgsscrollarea.h
qgssearchquerybuilder.h
qgsshortcutsmanager.h
qgsslider.h

127
src/gui/qgsscrollarea.cpp Normal file
View File

@ -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
src/gui/qgsscrollarea.h Normal file
View File

@ -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

86
src/ui/qgsoptionsbase.ui Normal file → Executable file
View File

@ -311,7 +311,7 @@
<item>
<widget class="QStackedWidget" name="mOptionsStackedWidget">
<property name="currentIndex">
<number>13</number>
<number>2</number>
</property>
<widget class="QWidget" name="mOptionsPageGeneral">
<layout class="QVBoxLayout" name="verticalLayout_3">
@ -328,7 +328,7 @@
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_01">
<widget class="QgsScrollArea" name="mOptionsScrollArea_01">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -340,8 +340,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>857</width>
<height>680</height>
<width>856</width>
<height>679</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_28">
@ -1014,7 +1014,7 @@
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_03">
<widget class="QgsScrollArea" name="mOptionsScrollArea_03">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -1026,8 +1026,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>541</width>
<height>1065</height>
<width>839</width>
<height>1009</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_22">
@ -1544,7 +1544,7 @@
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_11">
<widget class="QgsScrollArea" name="mOptionsScrollArea_11">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -1556,8 +1556,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>495</width>
<height>685</height>
<width>856</width>
<height>679</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_27">
@ -1912,7 +1912,7 @@
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_04">
<widget class="QgsScrollArea" name="mOptionsScrollArea_04">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -1924,8 +1924,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>674</width>
<height>936</height>
<width>839</width>
<height>827</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_22">
@ -2663,7 +2663,7 @@
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea">
<widget class="QgsScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -2675,8 +2675,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>142</width>
<height>239</height>
<width>112</width>
<height>219</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_46">
@ -2814,7 +2814,7 @@
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_06">
<widget class="QgsScrollArea" name="mOptionsScrollArea_06">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -2826,8 +2826,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>501</width>
<height>316</height>
<width>453</width>
<height>281</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_25">
@ -3157,7 +3157,7 @@
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_05">
<widget class="QgsScrollArea" name="mOptionsScrollArea_05">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -3169,8 +3169,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>604</width>
<height>589</height>
<width>506</width>
<height>533</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_30">
@ -3601,7 +3601,7 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_12">
<widget class="QgsScrollArea" name="mOptionsScrollArea_12">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -3613,8 +3613,8 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>571</height>
<width>393</width>
<height>530</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_39">
@ -3870,7 +3870,7 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_07">
<widget class="QgsScrollArea" name="mOptionsScrollArea_07">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -3882,8 +3882,8 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<rect>
<x>0</x>
<y>0</y>
<width>543</width>
<height>696</height>
<width>488</width>
<height>612</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_31">
@ -4448,7 +4448,7 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_02">
<widget class="QgsScrollArea" name="mOptionsScrollArea_02">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -4460,8 +4460,8 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<rect>
<x>0</x>
<y>0</y>
<width>412</width>
<height>368</height>
<width>345</width>
<height>350</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
@ -4587,7 +4587,7 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_08">
<widget class="QgsScrollArea" name="mOptionsScrollArea_08">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -4599,8 +4599,8 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<rect>
<x>0</x>
<y>0</y>
<width>408</width>
<height>531</height>
<width>332</width>
<height>499</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_15">
@ -4800,7 +4800,7 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_09">
<widget class="QgsScrollArea" name="mOptionsScrollArea_09">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -4812,8 +4812,8 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<rect>
<x>0</x>
<y>0</y>
<width>273</width>
<height>236</height>
<width>221</width>
<height>201</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_32">
@ -4909,7 +4909,7 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="mOptionsScrollArea_10">
<widget class="QgsScrollArea" name="mOptionsScrollArea_10">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@ -4921,8 +4921,8 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
<rect>
<x>0</x>
<y>0</y>
<width>843</width>
<height>689</height>
<width>856</width>
<height>679</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_33">
@ -5435,6 +5435,12 @@ The bigger the number, the faster zooming with the mouse wheel will be.</string>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
<class>QgsScrollArea</class>
<extends>QScrollArea</extends>
<header>qgsscrollarea.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsPasswordLineEdit</class>
<extends>QLineEdit</extends>