diff options
author | Mitch Curtis <[email protected]> | 2024-05-15 16:07:08 +0800 |
---|---|---|
committer | Mitch Curtis <[email protected]> | 2024-05-30 14:38:46 +0800 |
commit | bb370edebafd79c9f2af0780251b89c617b6bd41 (patch) | |
tree | 4f8350385fa13e49cf978b2ddf1dc8e24e31dd9d | |
parent | 67fcd9f035c21e613ff52d8ffb019ccbfa93964b (diff) |
AbstractButton: Add click() and animateClick()
[ChangeLog][Controls][AbstractButton] Added click() and
animateClick() functions to allow programmatically clicking a button
with or without visual changes.
Fixes: QTBUG-96784
Task-number: QTBUG-69558
Change-Id: I53cdccd90e2e4b831105e90e2541cbc674760c0b
Reviewed-by: Shawn Rutledge <[email protected]>
-rw-r--r-- | src/quicktemplates/qquickabstractbutton.cpp | 85 | ||||
-rw-r--r-- | src/quicktemplates/qquickabstractbutton_p.h | 2 | ||||
-rw-r--r-- | src/quicktemplates/qquickabstractbutton_p_p.h | 1 | ||||
-rw-r--r-- | tests/auto/quickcontrols/controls/data/tst_abstractbutton.qml | 172 |
4 files changed, 259 insertions, 1 deletions
diff --git a/src/quicktemplates/qquickabstractbutton.cpp b/src/quicktemplates/qquickabstractbutton.cpp index d244685011..ff2d9715c1 100644 --- a/src/quicktemplates/qquickabstractbutton.cpp +++ b/src/quicktemplates/qquickabstractbutton.cpp @@ -66,7 +66,7 @@ QT_BEGIN_NAMESPACE This signal is emitted when the button is interactively clicked by the user via touch, mouse, or keyboard. - \sa {Call a C++ function from QML when a Button is clicked} + \sa click(), animateClick(), {Call a C++ function from QML when a Button is clicked} */ /*! @@ -1059,6 +1059,8 @@ qreal QQuickAbstractButton::implicitIndicatorHeight() const \qmlmethod void QtQuick.Controls::AbstractButton::toggle() Toggles the checked state of the button. + + \sa click(), animateClick() */ void QQuickAbstractButton::toggle() { @@ -1066,6 +1068,81 @@ void QQuickAbstractButton::toggle() setChecked(!d->checked); } +/*! + \since Qt 6.8 + \qmlmethod void QtQuick.Controls::AbstractButton::click() + + Simulates the button being clicked with no delay between press and release. + + All signals associated with a click are emitted as appropriate. + + If the \l focusPolicy includes \c Qt.ClickFocus, \l activeFocus will + become \c true. + + This function does nothing if the button is \l {enabled}{disabled}. + + Calling this function again before the button is released resets + the release timer. + + \sa animateClick(), pressed(), released(), clicked() +*/ +void QQuickAbstractButton::click() +{ + Q_D(QQuickAbstractButton); + if (!isEnabled()) + return; + + // QQuickItemPrivate::deliverPointerEvent calls setFocusIfNeeded on real clicks, + // so we need to do it ourselves. + const bool setFocusOnPress = !QGuiApplication::styleHints()->setFocusOnTouchRelease(); + if (setFocusOnPress && focusPolicy() & Qt::ClickFocus) + forceActiveFocus(Qt::MouseFocusReason); + + const QPointF eventPos(d->width / 2, d->height / 2); + d->handlePress(eventPos, 0); + d->handleRelease(eventPos, 0); +} + +/*! + \since Qt 6.8 + \qmlmethod void QtQuick.Controls::AbstractButton::animateClick() + + Simulates the button being clicked, with a 100 millisecond delay + between press and release, animating its visual state in the + process. + + All signals associated with a click are emitted as appropriate. + + If the \l focusPolicy includes \c Qt.ClickFocus, \l activeFocus will + become \c true. + + This function does nothing if the button is \l {enabled}{disabled}. + + Calling this function again before the button is released resets + the release timer. + + \sa click(), pressed(), released(), clicked() +*/ +void QQuickAbstractButton::animateClick() +{ + Q_D(QQuickAbstractButton); + if (!isEnabled()) + return; + + // See comment in click() for why we do this. + const bool setFocusOnPress = !QGuiApplication::styleHints()->setFocusOnTouchRelease(); + if (setFocusOnPress && focusPolicy() & Qt::ClickFocus) + forceActiveFocus(Qt::MouseFocusReason); + + // If the timer was already running, kill it so we can restart it. + if (d->animateTimer != 0) + killTimer(d->animateTimer); + else + d->handlePress(QPointF(d->width / 2, d->height / 2), 0); + + d->animateTimer = startTimer(100); +} + void QQuickAbstractButton::componentComplete() { Q_D(QQuickAbstractButton); @@ -1166,6 +1243,12 @@ void QQuickAbstractButton::timerEvent(QTimerEvent *event) emit released(); d->trigger(); emit pressed(); + } else if (event->timerId() == d->animateTimer) { + const bool setFocusOnRelease = QGuiApplication::styleHints()->setFocusOnTouchRelease(); + if (setFocusOnRelease && focusPolicy() & Qt::ClickFocus) + forceActiveFocus(Qt::MouseFocusReason); + d->handleRelease(QPointF(d->width / 2, d->height / 2), 0); + d->animateTimer = 0; } } diff --git a/src/quicktemplates/qquickabstractbutton_p.h b/src/quicktemplates/qquickabstractbutton_p.h index 0ac27db156..c9e7407920 100644 --- a/src/quicktemplates/qquickabstractbutton_p.h +++ b/src/quicktemplates/qquickabstractbutton_p.h @@ -119,6 +119,8 @@ public: public Q_SLOTS: void toggle(); + Q_REVISION(6, 8) void click(); + Q_REVISION(6, 8) void animateClick(); Q_SIGNALS: void pressed(); diff --git a/src/quicktemplates/qquickabstractbutton_p_p.h b/src/quicktemplates/qquickabstractbutton_p_p.h index ea9a02c99d..0d5eb65940 100644 --- a/src/quicktemplates/qquickabstractbutton_p_p.h +++ b/src/quicktemplates/qquickabstractbutton_p_p.h @@ -103,6 +103,7 @@ public: int repeatTimer = 0; int repeatDelay = AUTO_REPEAT_DELAY; int repeatInterval = AUTO_REPEAT_INTERVAL; + int animateTimer = 0; #if QT_CONFIG(shortcut) int shortcutId = 0; QKeySequence shortcut; diff --git a/tests/auto/quickcontrols/controls/data/tst_abstractbutton.qml b/tests/auto/quickcontrols/controls/data/tst_abstractbutton.qml index 822c703a42..bce13b37f2 100644 --- a/tests/auto/quickcontrols/controls/data/tst_abstractbutton.qml +++ b/tests/auto/quickcontrols/controls/data/tst_abstractbutton.qml @@ -42,6 +42,45 @@ TestCase { SignalSpy { } } + property var expectedPressSignals: [ + ["activeFocusChanged", { "activeFocus": true }], + ["pressedChanged", { "pressed": true }], + ["downChanged", { "down": true }], + "pressed" + ] + + property var expectedReleaseSignals: [ + ["pressedChanged", { "pressed": false }], + ["downChanged", { "down": false }], + "released", + "clicked" + ] + + property var expectedClickSignals + + property var expectedCheckableClickSignals: [ + ["activeFocusChanged", { "activeFocus": true }], + ["pressedChanged", { "pressed": true }], + ["downChanged", { "down": true }], + "pressed", + ["pressedChanged", { "pressed": false }], + ["downChanged", { "down": false }], + ["checkedChanged", { "checked": true }], + "toggled", + "released", + "clicked" + ] + + function initTestCase() { + // AbstractButton has TabFocus on macOS, not StrongFocus. + if (Qt.platform.os === "osx") { + expectedPressSignals.splice(0, 1) + expectedCheckableClickSignals.splice(0, 1) + } + + expectedClickSignals = [...expectedPressSignals, ...expectedReleaseSignals] + } + function init() { failOnWarning(/.?/) } @@ -1004,4 +1043,137 @@ TestCase { compare(releasedSpy.count, 0) compare(clickedSpy.count, 0) } + + Component { + id: signalSequenceSpy + SignalSequenceSpy { + // List all signals, even ones we might not be interested in for a particular test, + // so that it can catch unwanted ones and fail the test. + signals: ["pressed", "released", "canceled", "clicked", "toggled", "doubleClicked", + "pressedChanged", "downChanged", "checkedChanged", "activeFocusChanged"] + } + } + + function test_click() { + let control = createTemporaryObject(button, testCase) + verify(control) + + let sequenceSpy = signalSequenceSpy.createObject(control, { target: control }) + sequenceSpy.expectedSequence = testCase.expectedClickSignals + control.click() + verify(sequenceSpy.success) + } + + function test_clickCheckableButton() { + let control = createTemporaryObject(button, testCase, { checkable: true }) + verify(control) + + let sequenceSpy = signalSequenceSpy.createObject(control, { target: control }) + sequenceSpy.expectedSequence = testCase.expectedCheckableClickSignals + control.click() + verify(sequenceSpy.success) + } + + function test_animateClick() { + let control = createTemporaryObject(button, testCase) + verify(control) + + let sequenceSpy = signalSequenceSpy.createObject(control, { target: control }) + sequenceSpy.expectedSequence = testCase.expectedClickSignals + control.animateClick() + tryVerify(() => { return sequenceSpy.success }, 1000) + } + + function test_animateClickCheckableButton() { + let control = createTemporaryObject(button, testCase, { checkable: true }) + verify(control) + + let sequenceSpy = signalSequenceSpy.createObject(control, { target: control }) + sequenceSpy.expectedSequence = testCase.expectedCheckableClickSignals + control.animateClick() + tryVerify(() => { return sequenceSpy.success }, 1000) + } + + function test_animateClickTwice() { + let control = createTemporaryObject(button, testCase) + verify(control) + + let sequenceSpy = signalSequenceSpy.createObject(control, { target: control }) + sequenceSpy.expectedSequence = testCase.expectedPressSignals + // Check that calling it again before it finishes works as expected. + control.animateClick() + verify(sequenceSpy.success) + // Let the timer progress a bit. + wait(0) + sequenceSpy.expectedSequence = testCase.expectedReleaseSignals + control.animateClick() + tryVerify(() => { return sequenceSpy.success }, 1000) + } + + function test_clickOnDisabledButton() { + let control = createTemporaryObject(button, testCase, { enabled: false }) + verify(control) + + let sequenceSpy = signalSequenceSpy.createObject(control, { target: control }) + sequenceSpy.expectedSequence = [] + control.click() + verify(sequenceSpy.success) + } + + function test_animateClickOnDisabledButton() { + let control = createTemporaryObject(button, testCase, { enabled: false }) + verify(control) + + let sequenceSpy = signalSequenceSpy.createObject(control, { target: control }) + sequenceSpy.expectedSequence = [] + control.animateClick() + verify(sequenceSpy.success) + } + + Component { + id: destroyOnPressButtonComponent + + AbstractButton { + width: 100 + height: 50 + + onPressed: destroy(this) + } + } + + function test_clickDestroyOnPress() { + let control = createTemporaryObject(destroyOnPressButtonComponent, testCase) + verify(control) + + // Parent it to the testCase, otherwise it will be destroyed when the control is. + let destructionSpy = createTemporaryObject(signalSpy, testCase, + { target: control.Component, signalName: "destruction" }) + verify(destructionSpy.valid) + + let sequenceSpy = signalSequenceSpy.createObject(control, { target: control }) + sequenceSpy.expectedSequence = testCase.expectedClickSignals + // Shouldn't crash, etc. Note that destroy() isn't synchronous, and so + // the destruction will happen after the release. + control.click() + verify(sequenceSpy.success) + tryCompare(destructionSpy, "count", 1) + } + + function test_animateClickDestroyOnPress() { + let control = createTemporaryObject(destroyOnPressButtonComponent, testCase) + verify(control) + + // Parent it to the testCase, otherwise it will be destroyed when the control is. + let destructionSpy = createTemporaryObject(signalSpy, testCase, + { target: control.Component, signalName: "destruction" }) + verify(destructionSpy.valid) + + let sequenceSpy = signalSequenceSpy.createObject(control, { target: control }) + sequenceSpy.expectedSequence = testCase.expectedPressSignals + // Shouldn't crash, etc. Note that destroy() isn't synchronous, but it is processed + // on the next frame, so should always come before the release's 100 ms delay. + control.animateClick() + verify(sequenceSpy.success) + tryCompare(destructionSpy, "count", 1) + } } |