diff options
5 files changed, 385 insertions, 19 deletions
diff --git a/examples/quickcontrols2/gallery/pages/DelegatePage.qml b/examples/quickcontrols2/gallery/pages/DelegatePage.qml index ed817938..525487d1 100644 --- a/examples/quickcontrols2/gallery/pages/DelegatePage.qml +++ b/examples/quickcontrols2/gallery/pages/DelegatePage.qml @@ -70,17 +70,17 @@ Pane { text: labelText width: parent.width - onClicked: if (swipe.complete) view.model.remove(ourIndex) - Component { id: removeComponent Rectangle { - color: swipeDelegate.swipe.complete && swipeDelegate.pressed ? "#333" : "#444" + color: SwipeDelegate.pressed ? "#333" : "#444" width: parent.width height: parent.height clip: true + SwipeDelegate.onClicked: view.model.remove(ourIndex) + Label { font.pixelSize: swipeDelegate.font.pixelSize text: "Remove" diff --git a/src/imports/controls/doc/snippets/qtquickcontrols2-swipedelegate-custom.qml b/src/imports/controls/doc/snippets/qtquickcontrols2-swipedelegate-custom.qml index c30dd76f..087e17fe 100644 --- a/src/imports/controls/doc/snippets/qtquickcontrols2-swipedelegate-custom.qml +++ b/src/imports/controls/doc/snippets/qtquickcontrols2-swipedelegate-custom.qml @@ -37,7 +37,7 @@ SwipeDelegate { id: component Rectangle { - color: control.swipe.complete && control.down ? "#333" : "#444" + color: SwipeDelegate.pressed ? "#333" : "#444" width: parent.width height: parent.height clip: true diff --git a/src/quicktemplates2/qquickswipedelegate.cpp b/src/quicktemplates2/qquickswipedelegate.cpp index a8ba2e80..04f3f7cf 100644 --- a/src/quicktemplates2/qquickswipedelegate.cpp +++ b/src/quicktemplates2/qquickswipedelegate.cpp @@ -92,6 +92,14 @@ QT_BEGIN_NAMESPACE \sa {Customizing SwipeDelegate}, {Delegate Controls} */ +namespace { + typedef QQuickSwipeDelegateAttached Attached; + + Attached *attachedObject(QQuickItem *item) { + return qobject_cast<Attached*>(qmlAttachedPropertiesObject<QQuickSwipeDelegate>(item, false)); + } +} + class QQuickSwipePrivate : public QObjectPrivate { Q_DECLARE_PUBLIC(QQuickSwipe) @@ -561,9 +569,22 @@ bool QQuickSwipeDelegatePrivate::handleMousePressEvent(QQuickItem *item, QMouseE return true; } + // The position is non-zero, this press could be either for a delegate or the control itself + // (the control can be clicked to e.g. close the swipe). Either way, we must begin measuring + // mouse movement in case it turns into a swipe, in which case we grab the mouse. swipePrivate->positionBeforePress = swipePrivate->position; swipePrivate->velocityCalculator.startMeasuring(event->pos(), event->timestamp()); pressPoint = item->mapToItem(q, event->pos()); + + // When a delegate uses the attached properties and signals, it declares that it wants mouse events. + Attached *attached = attachedObject(item); + if (attached) { + attached->setPressed(true); + // Stop the event from propagating, as QQuickItem explicitly ignores events. + event->accept(); + return true; + } + return false; } @@ -589,7 +610,8 @@ bool QQuickSwipeDelegatePrivate::handleMouseMoveEvent(QQuickItem *item, QMouseEv if (item == q && !pressed) return false; - const qreal distance = (event->pos() - pressPoint).x(); + const QPointF mappedEventPos = item->mapToItem(q, event->pos()); + const qreal distance = (mappedEventPos - pressPoint).x(); if (!q->keepMouseGrab()) { // Taken from QQuickDrawer::handleMouseMoveEvent; see comments there. int threshold = qMax(20, QGuiApplication::styleHints()->startDragDistance() + 5); @@ -598,9 +620,12 @@ bool QQuickSwipeDelegatePrivate::handleMouseMoveEvent(QQuickItem *item, QMouseEv QQuickItem *grabber = q->window()->mouseGrabberItem(); if (!grabber || !grabber->keepMouseGrab()) { q->grabMouse(); - q->setKeepMouseGrab(overThreshold); + q->setKeepMouseGrab(true); q->setPressed(true); swipe.setComplete(false); + + if (Attached *attached = attachedObject(item)) + attached->setPressed(false); } } } @@ -660,7 +685,7 @@ bool QQuickSwipeDelegatePrivate::handleMouseMoveEvent(QQuickItem *item, QMouseEv static const qreal exposeVelocityThreshold = 300.0; -bool QQuickSwipeDelegatePrivate::handleMouseReleaseEvent(QQuickItem *, QMouseEvent *event) +bool QQuickSwipeDelegatePrivate::handleMouseReleaseEvent(QQuickItem *item, QMouseEvent *event) { Q_Q(QQuickSwipeDelegate); QQuickSwipePrivate *swipePrivate = QQuickSwipePrivate::get(&swipe); @@ -687,6 +712,14 @@ bool QQuickSwipeDelegatePrivate::handleMouseReleaseEvent(QQuickItem *, QMouseEve swipePrivate->wasComplete = false; } + if (Attached *attached = attachedObject(item)) { + const bool wasPressed = attached->isPressed(); + if (wasPressed) { + attached->setPressed(false); + emit attached->clicked(); + } + } + // Only consume child events if we had grabbed the mouse. return hadGrabbedMouse; } @@ -805,6 +838,11 @@ QQuickSwipe *QQuickSwipeDelegate::swipe() const return const_cast<QQuickSwipe*>(&d->swipe); } +QQuickSwipeDelegateAttached *QQuickSwipeDelegate::qmlAttachedProperties(QObject *object) +{ + return new QQuickSwipeDelegateAttached(object); +} + static bool isChildOrGrandchildOf(QQuickItem *child, QQuickItem *item) { return item && (child == item || item->isAncestorOf(child)); @@ -835,6 +873,14 @@ bool QQuickSwipeDelegate::childMouseEventFilter(QQuickItem *child, QEvent *event QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event); QQuickItemDelegate::mouseReleaseEvent(mouseEvent); return d->handleMouseReleaseEvent(child, mouseEvent); + } case QEvent::UngrabMouse: { + // If the mouse was pressed over e.g. rightItem and then dragged down, + // the ListView would eventually grab the mouse, at which point we must + // clear the pressed flag so that it doesn't stay pressed after the release. + Attached *attached = attachedObject(child); + if (attached) + attached->setPressed(false); + return false; } default: return false; } @@ -876,4 +922,155 @@ QAccessible::Role QQuickSwipeDelegate::accessibleRole() const } #endif +class QQuickSwipeDelegateAttachedPrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QQuickSwipeDelegateAttached) + +public: + QQuickSwipeDelegateAttachedPrivate() : + pressed(false) + { + } + + // True when left/right/behind is non-interactive and is pressed. + bool pressed; +}; + +/*! + \since QtQuick.Controls 2.1 + \qmlattachedsignal QtQuick.Controls::SwipeDelegate::clicked() + + This signal can be attached to a non-interactive item declared in + \c swipe.left, \c swipe.right, or \c swipe.behind, in order to react to + clicks. Items can only be clicked when \c swipe.complete is \c true. + + For interactive controls (such as \l Button) declared in these + items, use their respective \c clicked() signal instead. + + To respond to clicks on the SwipeDelegate itself, use its + \l {AbstractButton::}{clicked()} signal. + + \note See the documentation for \l pressed for information on + how to use the event-related properties correctly. + + \sa pressed +*/ + +QQuickSwipeDelegateAttached::QQuickSwipeDelegateAttached(QObject *object) : + QObject(*(new QQuickSwipeDelegateAttachedPrivate), object) +{ + QQuickItem *item = qobject_cast<QQuickItem *>(object); + if (item) { + // This allows us to be notified when an otherwise non-interactive item + // is pressed and clicked. The alternative is much more more complex: + // iterating through children that contain the event pos and finding + // the first one with an attached object. + item->setAcceptedMouseButtons(Qt::AllButtons); + } else { + qWarning() << "Attached properties of SwipeDelegate must be accessed through an Item"; + } +} + +/*! + \since QtQuick.Controls 2.1 + \qmlattachedproperty bool QtQuick.Controls::SwipeDelegate::pressed + \readonly + + This property can be attached to a non-interactive item declared in + \c swipe.left, \c swipe.right, or \c swipe.behind, in order to detect if it + is pressed. Items can only be pressed when \c swipe.complete is \c true. + + For example: + + \code + swipe.right: Label { + anchors.right: parent.right + height: parent.height + text: "Action" + color: "white" + padding: 12 + background: Rectangle { + color: SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) : "tomato" + } + } + \endcode + + It is possible to have multiple items which individually receive mouse and + touch events. For example, to have two actions in the \c swipe.right item, + use the following code: + + \code + swipe.right: Row { + anchors.right: parent.right + height: parent.height + + Label { + id: moveLabel + text: qsTr("Move") + color: "white" + verticalAlignment: Label.AlignVCenter + padding: 12 + height: parent.height + + SwipeDelegate.onClicked: console.log("Moving...") + + background: Rectangle { + color: moveLabel.SwipeDelegate.pressed ? Qt.darker("#ffbf47", 1.1) : "#ffbf47" + } + } + Label { + id: deleteLabel + text: qsTr("Delete") + color: "white" + verticalAlignment: Label.AlignVCenter + padding: 12 + height: parent.height + + SwipeDelegate.onClicked: console.log("Deleting...") + + background: Rectangle { + color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) : "tomato" + } + } + } + \endcode + + Note how the \c color assignment in each \l {Control::}{background} item + qualifies the attached property with the \c id of the label. This + is important; using the attached properties on an item causes that item + to accept events. Suppose we had left out the \c id in the previous example: + + \code + color: SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) : "tomato" + \endcode + + The \l Rectangle background item is a child of the label, so it naturally + receives events before it. In practice, this means that the background + color will change, but the \c onClicked handler in the label will never + get called. + + For interactive controls (such as \l Button) declared in these + items, use their respective \c pressed property instead. + + For presses on the SwipeDelegate itself, use its + \l {AbstractButton::}{pressed} property. + + \sa clicked() +*/ +bool QQuickSwipeDelegateAttached::isPressed() const +{ + Q_D(const QQuickSwipeDelegateAttached); + return d->pressed; +} + +void QQuickSwipeDelegateAttached::setPressed(bool pressed) +{ + Q_D(QQuickSwipeDelegateAttached); + if (pressed == d->pressed) + return; + + d->pressed = pressed; + emit pressedChanged(); +} + QT_END_NAMESPACE diff --git a/src/quicktemplates2/qquickswipedelegate_p.h b/src/quicktemplates2/qquickswipedelegate_p.h index f94219cd..7ef995d1 100644 --- a/src/quicktemplates2/qquickswipedelegate_p.h +++ b/src/quicktemplates2/qquickswipedelegate_p.h @@ -54,6 +54,8 @@ QT_BEGIN_NAMESPACE class QQuickSwipeDelegatePrivate; class QQuickSwipe; +class QQuickSwipeDelegateAttached; +class QQuickSwipeDelegateAttachedPrivate; class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickSwipeDelegate : public QQuickItemDelegate { @@ -65,6 +67,8 @@ public: QQuickSwipe *swipe() const; + static QQuickSwipeDelegateAttached *qmlAttachedProperties(QObject *object); + protected: bool childMouseEventFilter(QQuickItem *child, QEvent *event) override; void mousePressEvent(QMouseEvent *event) override; @@ -141,8 +145,29 @@ private: Q_DECLARE_PRIVATE(QQuickSwipe) }; +class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickSwipeDelegateAttached : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool pressed READ isPressed NOTIFY pressedChanged FINAL) + +public: + explicit QQuickSwipeDelegateAttached(QObject *object = nullptr); + + bool isPressed() const; + void setPressed(bool pressed); + +Q_SIGNALS: + void pressedChanged(); + void clicked(); + +private: + Q_DISABLE_COPY(QQuickSwipeDelegateAttached) + Q_DECLARE_PRIVATE(QQuickSwipeDelegateAttached) +}; + QT_END_NAMESPACE QML_DECLARE_TYPE(QQuickSwipeDelegate) +QML_DECLARE_TYPEINFO(QQuickSwipeDelegate, QML_HAS_ATTACHED_PROPERTIES) #endif // QQUICKSWIPEDELEGATE_P_H diff --git a/tests/auto/controls/data/tst_swipedelegate.qml b/tests/auto/controls/data/tst_swipedelegate.qml index b034ca36..e2f94076 100644 --- a/tests/auto/controls/data/tst_swipedelegate.qml +++ b/tests/auto/controls/data/tst_swipedelegate.qml @@ -40,6 +40,7 @@ import QtQuick 2.6 import QtTest 1.0 +import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 TestCase { @@ -138,7 +139,7 @@ TestCase { verify(control); ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + - ":78:9: QML SwipeDelegate: cannot set both behind and left/right properties") + ":79:9: QML SwipeDelegate: cannot set both behind and left/right properties") control.swipe.behind = itemComponent; // Shouldn't be any warnings when unsetting delegates. @@ -147,7 +148,7 @@ TestCase { // right is still set. ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + - ":78:9: QML SwipeDelegate: cannot set both behind and left/right properties") + ":79:9: QML SwipeDelegate: cannot set both behind and left/right properties") control.swipe.behind = itemComponent; control.swipe.right = null; @@ -156,11 +157,11 @@ TestCase { control.swipe.behind = itemComponent; ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + - ":78:9: QML SwipeDelegate: cannot set both behind and left/right properties") + ":79:9: QML SwipeDelegate: cannot set both behind and left/right properties") control.swipe.left = itemComponent; ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + - ":78:9: QML SwipeDelegate: cannot set both behind and left/right properties") + ":79:9: QML SwipeDelegate: cannot set both behind and left/right properties") control.swipe.right = itemComponent; control.swipe.behind = null; @@ -175,7 +176,7 @@ TestCase { var oldLeft = control.swipe.left; var oldLeftItem = control.swipe.leftItem; ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + - ":78:9: QML SwipeDelegate: left/right/behind properties may only be set when swipe.position is 0") + ":79:9: QML SwipeDelegate: left/right/behind properties may only be set when swipe.position is 0") control.swipe.left = null; compare(control.swipe.left, oldLeft); compare(control.swipe.leftItem, oldLeftItem); @@ -186,7 +187,7 @@ TestCase { var oldRight = control.swipe.right; var oldRightItem = control.swipe.rightItem; ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + - ":78:9: QML SwipeDelegate: left/right/behind properties may only be set when swipe.position is 0") + ":79:9: QML SwipeDelegate: left/right/behind properties may only be set when swipe.position is 0") control.swipe.right = null; compare(control.swipe.right, oldRight); compare(control.swipe.rightItem, oldRightItem); @@ -212,7 +213,7 @@ TestCase { var oldBehind = control.swipe.behind; var oldBehindItem = control.swipe.behindItem; ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + - ":78:9: QML SwipeDelegate: left/right/behind properties may only be set when swipe.position is 0") + ":79:9: QML SwipeDelegate: left/right/behind properties may only be set when swipe.position is 0") control.swipe.behind = null; compare(control.swipe.behind, oldBehind); compare(control.swipe.behindItem, oldBehindItem); @@ -564,8 +565,6 @@ TestCase { text: modelData width: listView.width - onClicked: if (swipe.complete) ListView.view.model.remove(index) - property alias removeAnimation: onRemoveAnimation ListView.onRemove: SequentialAnimation { @@ -590,9 +589,12 @@ TestCase { } swipe.left: Rectangle { - color: rootDelegate.swipe.complete && rootDelegate.pressed ? "#333" : "#444" + objectName: "rectangle" + color: SwipeDelegate.pressed ? "#333" : "#444" anchors.fill: parent + SwipeDelegate.onClicked: listView.model.remove(index) + Label { objectName: "label" text: "Remove" @@ -615,11 +617,14 @@ TestCase { verify(firstItem.pressed); compare(firstItem.swipe.position, 0.0); verify(!firstItem.swipe.complete); + verify(!firstItem.swipe.leftItem); mouseMove(listView, firstItem.width * 1.1, firstItem.height / 2); verify(firstItem.pressed); compare(firstItem.swipe.position, 0.6); verify(!firstItem.swipe.complete); + verify(firstItem.swipe.leftItem); + verify(!firstItem.swipe.leftItem.SwipeDelegate.pressed); mouseRelease(listView, firstItem.width / 2, firstItem.height / 2); verify(!firstItem.pressed); @@ -630,9 +635,23 @@ TestCase { // Wait for it to settle down. tryCompare(firstItem.contentItem, "x", firstItem.leftPadding + firstItem.width); - // Click the button to remove the item. + var leftClickedSpy = signalSpyComponent.createObject(firstItem.swipe.leftItem, + { target: firstItem.swipe.leftItem.SwipeDelegate, signalName: "clicked" }); + verify(leftClickedSpy); + verify(leftClickedSpy.valid); + + // Click the left item to remove the delegate from the list. var contentItemX = firstItem.contentItem.x; - mouseClick(listView, firstItem.width / 2, firstItem.height / 2); + mousePress(listView, firstItem.width / 2, firstItem.height / 2); + verify(firstItem.swipe.leftItem.SwipeDelegate.pressed); + compare(leftClickedSpy.count, 0); + verify(!firstItem.pressed); + + mouseRelease(listView, firstItem.width / 2, firstItem.height / 2); + verify(!firstItem.swipe.leftItem.SwipeDelegate.pressed); + compare(leftClickedSpy.count, 1); + verify(!firstItem.pressed); + leftClickedSpy = null; tryCompare(firstItem.removeAnimation, "running", true); // There was a bug where the resizeContent() would be called because the height // of the control was changing due to the animation. contentItem would then @@ -955,4 +974,129 @@ TestCase { control.destroy(); } + + Component { + id: multiActionSwipeDelegateComponent + + SwipeDelegate { + text: "SwipeDelegate" + width: 150 + + swipe.right: Item { + objectName: "rightItemRoot" + width: parent.width + height: parent.height + + property alias firstAction: firstAction + property alias secondAction: secondAction + + property int firstClickCount: 0 + property int secondClickCount: 0 + + RowLayout { + anchors.fill: parent + anchors.margins: 5 + + Rectangle { + id: firstAction + Layout.fillWidth: true + Layout.fillHeight: true + color: "tomato" + + SwipeDelegate.onClicked: ++firstClickCount + } + Rectangle { + id: secondAction + Layout.fillWidth: true + Layout.fillHeight: true + color: "navajowhite" + + SwipeDelegate.onClicked: ++secondClickCount + } + } + } + } + } + + // Tests that it's possible to have multiple non-interactive items in one delegate + // (e.g. left/right/behind) that can each receive clicks. + function test_multipleClickableActions() { + var control = multiActionSwipeDelegateComponent.createObject(testCase); + verify(control); + + swipe(control, 0.0, -1.0); + verify(control.swipe.rightItem); + tryCompare(control.swipe, "complete", true); + + var firstClickedSpy = signalSpyComponent.createObject(control, + { target: control.swipe.rightItem.firstAction.SwipeDelegate, signalName: "clicked" }); + verify(firstClickedSpy); + verify(firstClickedSpy.valid); + + // Clicked within rightItem, but not within an item using the attached properties. + mousePress(control, 2, 2); + compare(control.swipe.rightItem.firstAction.SwipeDelegate.pressed, false); + compare(firstClickedSpy.count, 0); + + mouseRelease(control, 2, 2); + compare(control.swipe.rightItem.firstAction.SwipeDelegate.pressed, false); + compare(firstClickedSpy.count, 0); + + // Click within the first item. + mousePress(control.swipe.rightItem.firstAction, 0, 0); + compare(control.swipe.rightItem.firstAction.SwipeDelegate.pressed, true); + compare(firstClickedSpy.count, 0); + + mouseRelease(control.swipe.rightItem.firstAction, 0, 0); + compare(control.swipe.rightItem.firstAction.SwipeDelegate.pressed, false); + compare(firstClickedSpy.count, 1); + compare(control.swipe.rightItem.firstClickCount, 1); + + var secondClickedSpy = signalSpyComponent.createObject(control, + { target: control.swipe.rightItem.secondAction.SwipeDelegate, signalName: "clicked" }); + verify(secondClickedSpy); + verify(secondClickedSpy.valid); + + // Click within the second item. + mousePress(control.swipe.rightItem.secondAction, 0, 0); + compare(control.swipe.rightItem.secondAction.SwipeDelegate.pressed, true); + compare(secondClickedSpy.count, 0); + + mouseRelease(control.swipe.rightItem.secondAction, 0, 0); + compare(control.swipe.rightItem.secondAction.SwipeDelegate.pressed, false); + compare(secondClickedSpy.count, 1); + compare(control.swipe.rightItem.secondClickCount, 1); + + control.destroy(); + } + + // Pressing on a "side action" and then dragging should eventually + // cause the ListView to grab the mouse and start changing its contentY. + // When this happens, it will grab the mouse and hence we must clear + // that action's pressed state so that it doesn't stay pressed after releasing. + function test_dragSideAction() { + var listView = removableDelegatesComponent.createObject(testCase); + verify(listView); + + var control = listView.itemAt(0, 0); + verify(control); + + // Expose the side action. + swipe(control, 0.0, 1.0); + verify(control.swipe.leftItem); + tryCompare(control.swipe, "complete", true); + + var pressedSpy = signalSpyComponent.createObject(control, + { target: control.swipe.leftItem.SwipeDelegate, signalName: "pressedChanged" }); + verify(pressedSpy); + verify(pressedSpy.valid); + + mouseDrag(listView, 20, 20, 0, listView.height); + compare(pressedSpy.count, 2); + verify(listView.contentY !== 0); + + compare(control.swipe.leftItem.SwipeDelegate.pressed, false); + + listView.destroy(); + } } |