aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMitch Curtis <[email protected]>2024-05-15 16:07:08 +0800
committerMitch Curtis <[email protected]>2024-05-30 14:38:46 +0800
commitbb370edebafd79c9f2af0780251b89c617b6bd41 (patch)
tree4f8350385fa13e49cf978b2ddf1dc8e24e31dd9d
parent67fcd9f035c21e613ff52d8ffb019ccbfa93964b (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.cpp85
-rw-r--r--src/quicktemplates/qquickabstractbutton_p.h2
-rw-r--r--src/quicktemplates/qquickabstractbutton_p_p.h1
-rw-r--r--tests/auto/quickcontrols/controls/data/tst_abstractbutton.qml172
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)
+ }
}