diff --git a/python/PyQt6/gui/auto_additions/qgsdoublespinbox.py b/python/PyQt6/gui/auto_additions/qgsdoublespinbox.py index 2099901d1439..70156b3f7bce 100644 --- a/python/PyQt6/gui/auto_additions/qgsdoublespinbox.py +++ b/python/PyQt6/gui/auto_additions/qgsdoublespinbox.py @@ -3,8 +3,8 @@ QgsDoubleSpinBox.MaximumValue = QgsDoubleSpinBox.ClearValueMode.MaximumValue QgsDoubleSpinBox.CustomValue = QgsDoubleSpinBox.ClearValueMode.CustomValue try: - QgsDoubleSpinBox.__attribute_docs__ = {'returnPressed': 'Emitted when the Return or Enter key is used in the line edit.\n\n.. versionadded:: 3.40\n', 'textEdited': 'Emitted when the the value has been manually edited via line edit.\n\n.. versionadded:: 3.40\n'} - QgsDoubleSpinBox.__signal_arguments__ = {'textEdited': ['text: str']} + QgsDoubleSpinBox.__attribute_docs__ = {'returnPressed': 'Emitted when the Return or Enter key is used in the line edit.\n\n.. versionadded:: 3.40\n', 'textEdited': 'Emitted when the the value has been manually edited via line edit.\n\n.. versionadded:: 3.40\n', 'editingTimeout': 'Emitted when either:\n\n1. 2 seconds has elapsed since the last value change in the widget (eg last key press or scroll wheel event)\n2. or, immediately after the widget has lost focus after its value was changed.\n\nThis signal can be used to respond semi-instantly to changes in the spin box, without responding too quickly\nwhile the user in the middle of setting the value.\n\n.. versionadded:: 3.42\n'} + QgsDoubleSpinBox.__signal_arguments__ = {'textEdited': ['text: str'], 'editingTimeout': ['value: float']} QgsDoubleSpinBox.__group__ = ['editorwidgets'] except (NameError, AttributeError): pass diff --git a/python/PyQt6/gui/auto_additions/qgsspinbox.py b/python/PyQt6/gui/auto_additions/qgsspinbox.py index befda5f1ddb1..749408be1941 100644 --- a/python/PyQt6/gui/auto_additions/qgsspinbox.py +++ b/python/PyQt6/gui/auto_additions/qgsspinbox.py @@ -3,8 +3,8 @@ QgsSpinBox.MaximumValue = QgsSpinBox.ClearValueMode.MaximumValue QgsSpinBox.CustomValue = QgsSpinBox.ClearValueMode.CustomValue try: - QgsSpinBox.__attribute_docs__ = {'returnPressed': 'Emitted when the Return or Enter key is used in the line edit\n\n.. versionadded:: 3.40\n', 'textEdited': 'Emitted when the the value has been manually edited via line edit.\n\n.. versionadded:: 3.40\n'} - QgsSpinBox.__signal_arguments__ = {'textEdited': ['text: str']} + QgsSpinBox.__attribute_docs__ = {'returnPressed': 'Emitted when the Return or Enter key is used in the line edit\n\n.. versionadded:: 3.40\n', 'textEdited': 'Emitted when the the value has been manually edited via line edit.\n\n.. versionadded:: 3.40\n', 'editingTimeout': 'Emitted when either:\n\n1. 2 seconds has elapsed since the last value change in the widget (eg last key press or scroll wheel event)\n2. or, immediately after the widget has lost focus after its value was changed.\n\nThis signal can be used to respond semi-instantly to changes in the spin box, without responding too quickly\nwhile the user in the middle of setting the value.\n\n.. versionadded:: 3.42\n'} + QgsSpinBox.__signal_arguments__ = {'textEdited': ['text: str'], 'editingTimeout': ['value: int']} QgsSpinBox.__group__ = ['editorwidgets'] except (NameError, AttributeError): pass diff --git a/python/PyQt6/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in b/python/PyQt6/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in index c84cdb6244d1..1d2ebf654243 100644 --- a/python/PyQt6/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in +++ b/python/PyQt6/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in @@ -161,6 +161,19 @@ Emitted when the Return or Enter key is used in the line edit. Emitted when the the value has been manually edited via line edit. .. versionadded:: 3.40 +%End + + void editingTimeout( double value ); +%Docstring +Emitted when either: + +1. 2 seconds has elapsed since the last value change in the widget (eg last key press or scroll wheel event) +2. or, immediately after the widget has lost focus after its value was changed. + +This signal can be used to respond semi-instantly to changes in the spin box, without responding too quickly +while the user in the middle of setting the value. + +.. versionadded:: 3.42 %End protected: @@ -168,6 +181,8 @@ Emitted when the the value has been manually edited via line edit. virtual void wheelEvent( QWheelEvent *event ); + virtual void focusOutEvent( QFocusEvent *event ); + virtual void timerEvent( QTimerEvent *event ); diff --git a/python/PyQt6/gui/auto_generated/editorwidgets/qgsspinbox.sip.in b/python/PyQt6/gui/auto_generated/editorwidgets/qgsspinbox.sip.in index abba6a80ab70..7173035225e2 100644 --- a/python/PyQt6/gui/auto_generated/editorwidgets/qgsspinbox.sip.in +++ b/python/PyQt6/gui/auto_generated/editorwidgets/qgsspinbox.sip.in @@ -149,6 +149,19 @@ Emitted when the Return or Enter key is used in the line edit Emitted when the the value has been manually edited via line edit. .. versionadded:: 3.40 +%End + + void editingTimeout( int value ); +%Docstring +Emitted when either: + +1. 2 seconds has elapsed since the last value change in the widget (eg last key press or scroll wheel event) +2. or, immediately after the widget has lost focus after its value was changed. + +This signal can be used to respond semi-instantly to changes in the spin box, without responding too quickly +while the user in the middle of setting the value. + +.. versionadded:: 3.42 %End protected: @@ -160,6 +173,8 @@ Emitted when the the value has been manually edited via line edit. virtual void timerEvent( QTimerEvent *event ); + virtual void focusOutEvent( QFocusEvent *event ); + }; diff --git a/python/gui/auto_additions/qgsdoublespinbox.py b/python/gui/auto_additions/qgsdoublespinbox.py index ef6d646dab1f..0323f763208f 100644 --- a/python/gui/auto_additions/qgsdoublespinbox.py +++ b/python/gui/auto_additions/qgsdoublespinbox.py @@ -1,7 +1,7 @@ # The following has been generated automatically from src/gui/editorwidgets/qgsdoublespinbox.h try: - QgsDoubleSpinBox.__attribute_docs__ = {'returnPressed': 'Emitted when the Return or Enter key is used in the line edit.\n\n.. versionadded:: 3.40\n', 'textEdited': 'Emitted when the the value has been manually edited via line edit.\n\n.. versionadded:: 3.40\n'} - QgsDoubleSpinBox.__signal_arguments__ = {'textEdited': ['text: str']} + QgsDoubleSpinBox.__attribute_docs__ = {'returnPressed': 'Emitted when the Return or Enter key is used in the line edit.\n\n.. versionadded:: 3.40\n', 'textEdited': 'Emitted when the the value has been manually edited via line edit.\n\n.. versionadded:: 3.40\n', 'editingTimeout': 'Emitted when either:\n\n1. 2 seconds has elapsed since the last value change in the widget (eg last key press or scroll wheel event)\n2. or, immediately after the widget has lost focus after its value was changed.\n\nThis signal can be used to respond semi-instantly to changes in the spin box, without responding too quickly\nwhile the user in the middle of setting the value.\n\n.. versionadded:: 3.42\n'} + QgsDoubleSpinBox.__signal_arguments__ = {'textEdited': ['text: str'], 'editingTimeout': ['value: float']} QgsDoubleSpinBox.__group__ = ['editorwidgets'] except (NameError, AttributeError): pass diff --git a/python/gui/auto_additions/qgsspinbox.py b/python/gui/auto_additions/qgsspinbox.py index 36fc8f57a174..1454efb3b5e4 100644 --- a/python/gui/auto_additions/qgsspinbox.py +++ b/python/gui/auto_additions/qgsspinbox.py @@ -1,7 +1,7 @@ # The following has been generated automatically from src/gui/editorwidgets/qgsspinbox.h try: - QgsSpinBox.__attribute_docs__ = {'returnPressed': 'Emitted when the Return or Enter key is used in the line edit\n\n.. versionadded:: 3.40\n', 'textEdited': 'Emitted when the the value has been manually edited via line edit.\n\n.. versionadded:: 3.40\n'} - QgsSpinBox.__signal_arguments__ = {'textEdited': ['text: str']} + QgsSpinBox.__attribute_docs__ = {'returnPressed': 'Emitted when the Return or Enter key is used in the line edit\n\n.. versionadded:: 3.40\n', 'textEdited': 'Emitted when the the value has been manually edited via line edit.\n\n.. versionadded:: 3.40\n', 'editingTimeout': 'Emitted when either:\n\n1. 2 seconds has elapsed since the last value change in the widget (eg last key press or scroll wheel event)\n2. or, immediately after the widget has lost focus after its value was changed.\n\nThis signal can be used to respond semi-instantly to changes in the spin box, without responding too quickly\nwhile the user in the middle of setting the value.\n\n.. versionadded:: 3.42\n'} + QgsSpinBox.__signal_arguments__ = {'textEdited': ['text: str'], 'editingTimeout': ['value: int']} QgsSpinBox.__group__ = ['editorwidgets'] except (NameError, AttributeError): pass diff --git a/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in b/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in index bf27e2570f6c..3bd32197d4d0 100644 --- a/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in +++ b/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in @@ -161,6 +161,19 @@ Emitted when the Return or Enter key is used in the line edit. Emitted when the the value has been manually edited via line edit. .. versionadded:: 3.40 +%End + + void editingTimeout( double value ); +%Docstring +Emitted when either: + +1. 2 seconds has elapsed since the last value change in the widget (eg last key press or scroll wheel event) +2. or, immediately after the widget has lost focus after its value was changed. + +This signal can be used to respond semi-instantly to changes in the spin box, without responding too quickly +while the user in the middle of setting the value. + +.. versionadded:: 3.42 %End protected: @@ -168,6 +181,8 @@ Emitted when the the value has been manually edited via line edit. virtual void wheelEvent( QWheelEvent *event ); + virtual void focusOutEvent( QFocusEvent *event ); + virtual void timerEvent( QTimerEvent *event ); diff --git a/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in b/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in index 50f73c286dfe..77046d729e03 100644 --- a/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in +++ b/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in @@ -149,6 +149,19 @@ Emitted when the Return or Enter key is used in the line edit Emitted when the the value has been manually edited via line edit. .. versionadded:: 3.40 +%End + + void editingTimeout( int value ); +%Docstring +Emitted when either: + +1. 2 seconds has elapsed since the last value change in the widget (eg last key press or scroll wheel event) +2. or, immediately after the widget has lost focus after its value was changed. + +This signal can be used to respond semi-instantly to changes in the spin box, without responding too quickly +while the user in the middle of setting the value. + +.. versionadded:: 3.42 %End protected: @@ -160,6 +173,8 @@ Emitted when the the value has been manually edited via line edit. virtual void timerEvent( QTimerEvent *event ); + virtual void focusOutEvent( QFocusEvent *event ); + }; diff --git a/src/gui/editorwidgets/qgsdoublespinbox.cpp b/src/gui/editorwidgets/qgsdoublespinbox.cpp index dad21c2a97a9..180db673925d 100644 --- a/src/gui/editorwidgets/qgsdoublespinbox.cpp +++ b/src/gui/editorwidgets/qgsdoublespinbox.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "qgsdoublespinbox.h" #include "moc_qgsdoublespinbox.cpp" @@ -51,6 +52,11 @@ QgsDoubleSpinBox::QgsDoubleSpinBox( QWidget *parent ) connect( mLineEdit, &QgsFilterLineEdit::cleared, this, &QgsDoubleSpinBox::clear ); connect( this, static_cast( &QDoubleSpinBox::valueChanged ), this, &QgsDoubleSpinBox::changed ); + + mLastEditTimer = new QTimer( this ); + mLastEditTimer->setSingleShot( true ); + mLastEditTimer->setInterval( 2000 ); + connect( mLastEditTimer, &QTimer::timeout, this, &QgsDoubleSpinBox::onLastEditTimeout ); } void QgsDoubleSpinBox::setShowClearButton( const bool showClearButton ) @@ -98,6 +104,12 @@ void QgsDoubleSpinBox::wheelEvent( QWheelEvent *event ) setSingleStep( step ); } +void QgsDoubleSpinBox::focusOutEvent( QFocusEvent *event ) +{ + QDoubleSpinBox::focusOutEvent( event ); + onLastEditTimeout(); +} + void QgsDoubleSpinBox::timerEvent( QTimerEvent *event ) { // Process all events, which may include a mouse release event @@ -132,6 +144,18 @@ void QgsDoubleSpinBox::stepBy( int steps ) void QgsDoubleSpinBox::changed( double value ) { mLineEdit->setShowClearButton( shouldShowClearForValue( value ) ); + mLastEditTimer->start(); +} + +void QgsDoubleSpinBox::onLastEditTimeout() +{ + mLastEditTimer->stop(); + const double currentValue = value(); + if ( std::isnan( mLastEditTimeoutValue ) || mLastEditTimeoutValue != currentValue ) + { + mLastEditTimeoutValue = currentValue; + emit editingTimeout( mLastEditTimeoutValue ); + } } void QgsDoubleSpinBox::clear() diff --git a/src/gui/editorwidgets/qgsdoublespinbox.h b/src/gui/editorwidgets/qgsdoublespinbox.h index ef88d5de94b3..ca170a366466 100644 --- a/src/gui/editorwidgets/qgsdoublespinbox.h +++ b/src/gui/editorwidgets/qgsdoublespinbox.h @@ -162,9 +162,23 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox */ void textEdited( const QString &text ); + /** + * Emitted when either: + * + * 1. 2 seconds has elapsed since the last value change in the widget (eg last key press or scroll wheel event) + * 2. or, immediately after the widget has lost focus after its value was changed. + * + * This signal can be used to respond semi-instantly to changes in the spin box, without responding too quickly + * while the user in the middle of setting the value. + * + * \since QGIS 3.42 + */ + void editingTimeout( double value ); + protected: void changeEvent( QEvent *event ) override; void wheelEvent( QWheelEvent *event ) override; + void focusOutEvent( QFocusEvent *event ) override; // This is required because private implementation of // QAbstractSpinBoxPrivate may trigger a second // undesired event from the auto-repeat mouse timer @@ -172,6 +186,7 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox private slots: void changed( double value ); + void onLastEditTimeout(); private: int frameWidth() const; @@ -185,6 +200,9 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox bool mExpressionsEnabled = true; + QTimer *mLastEditTimer = nullptr; + double mLastEditTimeoutValue = std::numeric_limits::quiet_NaN(); + QString stripped( const QString &originalText ) const; friend class TestQgsRangeWidgetWrapper; diff --git a/src/gui/editorwidgets/qgsspinbox.cpp b/src/gui/editorwidgets/qgsspinbox.cpp index 99be443e0b85..52e21976ea8e 100644 --- a/src/gui/editorwidgets/qgsspinbox.cpp +++ b/src/gui/editorwidgets/qgsspinbox.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "qgsspinbox.h" #include "moc_qgsspinbox.cpp" @@ -49,6 +50,11 @@ QgsSpinBox::QgsSpinBox( QWidget *parent ) connect( mLineEdit, &QgsFilterLineEdit::cleared, this, &QgsSpinBox::clear ); connect( this, static_cast( &QSpinBox::valueChanged ), this, &QgsSpinBox::changed ); + + mLastEditTimer = new QTimer( this ); + mLastEditTimer->setSingleShot( true ); + mLastEditTimer->setInterval( 2000 ); + connect( mLastEditTimer, &QTimer::timeout, this, &QgsSpinBox::onLastEditTimeout ); } void QgsSpinBox::setShowClearButton( const bool showClearButton ) @@ -112,9 +118,28 @@ void QgsSpinBox::timerEvent( QTimerEvent *event ) QSpinBox::timerEvent( event ); } +void QgsSpinBox::focusOutEvent( QFocusEvent *event ) +{ + QSpinBox::focusOutEvent( event ); + onLastEditTimeout(); +} + void QgsSpinBox::changed( int value ) { mLineEdit->setShowClearButton( shouldShowClearForValue( value ) ); + mLastEditTimer->start(); +} + +void QgsSpinBox::onLastEditTimeout() +{ + mLastEditTimer->stop(); + const int currentValue = value(); + if ( !mHasEmittedEditTimeout || mLastEditTimeoutValue != currentValue ) + { + mHasEmittedEditTimeout = true; + mLastEditTimeoutValue = currentValue; + emit editingTimeout( mLastEditTimeoutValue ); + } } void QgsSpinBox::clear() diff --git a/src/gui/editorwidgets/qgsspinbox.h b/src/gui/editorwidgets/qgsspinbox.h index b0b09fdce737..d34297db778b 100644 --- a/src/gui/editorwidgets/qgsspinbox.h +++ b/src/gui/editorwidgets/qgsspinbox.h @@ -153,6 +153,19 @@ class GUI_EXPORT QgsSpinBox : public QSpinBox */ void textEdited( const QString &text ); + /** + * Emitted when either: + * + * 1. 2 seconds has elapsed since the last value change in the widget (eg last key press or scroll wheel event) + * 2. or, immediately after the widget has lost focus after its value was changed. + * + * This signal can be used to respond semi-instantly to changes in the spin box, without responding too quickly + * while the user in the middle of setting the value. + * + * \since QGIS 3.42 + */ + void editingTimeout( int value ); + protected: void changeEvent( QEvent *event ) override; void paintEvent( QPaintEvent *event ) override; @@ -161,9 +174,11 @@ class GUI_EXPORT QgsSpinBox : public QSpinBox // QAbstractSpinBoxPrivate may trigger a second // undesired event from the auto-repeat mouse timer void timerEvent( QTimerEvent *event ) override; + void focusOutEvent( QFocusEvent *event ) override; private slots: void changed( int value ); + void onLastEditTimeout(); private: int frameWidth() const; @@ -177,6 +192,10 @@ class GUI_EXPORT QgsSpinBox : public QSpinBox bool mExpressionsEnabled = true; + QTimer *mLastEditTimer = nullptr; + bool mHasEmittedEditTimeout = false; + int mLastEditTimeoutValue = 0; + QString stripped( const QString &originalText ) const; friend class TestQgsRangeWidgetWrapper; diff --git a/tests/src/gui/testqgsdoublespinbox.cpp b/tests/src/gui/testqgsdoublespinbox.cpp index 59e407083425..aa6a07b69609 100644 --- a/tests/src/gui/testqgsdoublespinbox.cpp +++ b/tests/src/gui/testqgsdoublespinbox.cpp @@ -18,6 +18,8 @@ #include +#include + class TestQgsDoubleSpinBox : public QObject { Q_OBJECT @@ -30,6 +32,7 @@ class TestQgsDoubleSpinBox : public QObject void clear(); void expression(); void step(); + void editingTimeout(); private: }; @@ -206,5 +209,41 @@ void TestQgsDoubleSpinBox::step() QCOMPARE( spin.value(), -1000 ); } +void TestQgsDoubleSpinBox::editingTimeout() +{ + QgsDoubleSpinBox spin; + spin.setMinimum( -1000 ); + spin.setMaximum( 1000 ); + spin.setSingleStep( 1 ); + spin.setFocus(); + + QSignalSpy spy( &spin, &QgsDoubleSpinBox::editingTimeout ); + spin.selectAll(); + QTest::keyClicks( &spin, QStringLiteral( "3" ) ); + QTest::qWait( 500 ); + // too short, should not be signal + QCOMPARE( spy.count(), 0 ); + QTest::qWait( 2000 ); + // long enough, signal should have been emitted + QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.at( 0 ).at( 0 ).toInt(), 3 ); + + QTest::keyClicks( &spin, QStringLiteral( "2" ) ); + QCOMPARE( spy.count(), 1 ); + QTest::qWait( 2500 ); + // long enough, signal should have been emitted + QCOMPARE( spy.count(), 2 ); + QCOMPARE( spy.at( 1 ).at( 0 ).toInt(), 32 ); + + // no signal if value not changed + QTest::keyClicks( &spin, QStringLiteral( "4" ) ); + QTest::qWait( 500 ); + QCOMPARE( spy.count(), 2 ); + QTest::keyPress( &spin, Qt::Key_Backspace ); + QTest::qWait( 2500 ); + // no signal, value did not change + QCOMPARE( spy.count(), 2 ); +} + QGSTEST_MAIN( TestQgsDoubleSpinBox ) #include "testqgsdoublespinbox.moc" diff --git a/tests/src/gui/testqgsspinbox.cpp b/tests/src/gui/testqgsspinbox.cpp index 37423b98324c..e3ff1cb371c0 100644 --- a/tests/src/gui/testqgsspinbox.cpp +++ b/tests/src/gui/testqgsspinbox.cpp @@ -17,6 +17,7 @@ #include "qgstest.h" #include +#include class TestQgsSpinBox : public QObject { @@ -30,6 +31,7 @@ class TestQgsSpinBox : public QObject void clear(); void expression(); void step(); + void editingTimeout(); private: }; @@ -200,5 +202,41 @@ void TestQgsSpinBox::step() QCOMPARE( spin.value(), -1000 ); } +void TestQgsSpinBox::editingTimeout() +{ + QgsSpinBox spin; + spin.setMinimum( -1000 ); + spin.setMaximum( 1000 ); + spin.setSingleStep( 1 ); + spin.setFocus(); + + QSignalSpy spy( &spin, &QgsSpinBox::editingTimeout ); + spin.selectAll(); + QTest::keyClicks( &spin, QStringLiteral( "3" ) ); + QTest::qWait( 500 ); + // too short, should not be signal + QCOMPARE( spy.count(), 0 ); + QTest::qWait( 2000 ); + // long enough, signal should have been emitted + QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.at( 0 ).at( 0 ).toInt(), 3 ); + + QTest::keyClicks( &spin, QStringLiteral( "2" ) ); + QCOMPARE( spy.count(), 1 ); + QTest::qWait( 2500 ); + // long enough, signal should have been emitted + QCOMPARE( spy.count(), 2 ); + QCOMPARE( spy.at( 1 ).at( 0 ).toInt(), 32 ); + + // no signal if value not changed + QTest::keyClicks( &spin, QStringLiteral( "4" ) ); + QTest::qWait( 500 ); + QCOMPARE( spy.count(), 2 ); + QTest::keyPress( &spin, Qt::Key_Backspace ); + QTest::qWait( 2500 ); + // no signal, value did not change + QCOMPARE( spy.count(), 2 ); +} + QGSTEST_MAIN( TestQgsSpinBox ) #include "testqgsspinbox.moc"