diff options
author | Mitch Curtis <mitch.curtis@theqtcompany.com> | 2016-02-27 13:15:46 +0100 |
---|---|---|
committer | Mitch Curtis <mitch.curtis@theqtcompany.com> | 2016-03-15 21:44:12 +0000 |
commit | ef885485a19f0e70273650d88312af0195c32920 (patch) | |
tree | 48ca030228c456a53e08c45df97dee0944a7d2c8 | |
parent | e1add88186ed3ace78c7f6c1e3d683285a1a51fa (diff) |
Add SwipeDelegate
SwipeDelegate presents a view item that can be swiped left or right to
expose more options or information. It is used as a delegate in views
such as ListView.
Change-Id: I7533a2b223f652993b6cee730930ea6dc125c869
Task-number: QTBUG-51610
Reviewed-by: J-P Nurmi <jpnurmi@theqtcompany.com>
35 files changed, 3012 insertions, 0 deletions
diff --git a/examples/controls/gallery/gallery.qml b/examples/controls/gallery/gallery.qml index fd019c65..90f4b347 100644 --- a/examples/controls/gallery/gallery.qml +++ b/examples/controls/gallery/gallery.qml @@ -142,6 +142,7 @@ ApplicationWindow { ListElement { title: "CheckBox"; source: "qrc:/pages/CheckBoxPage.qml" } ListElement { title: "ComboBox"; source: "qrc:/pages/ComboBoxPage.qml" } ListElement { title: "Dial"; source: "qrc:/pages/DialPage.qml" } + ListElement { title: "Delegates"; source: "qrc:/pages/DelegatePage.qml" } ListElement { title: "Drawer"; source: "qrc:/pages/DrawerPage.qml" } ListElement { title: "Frame"; source: "qrc:/pages/FramePage.qml" } ListElement { title: "GroupBox"; source: "qrc:/pages/GroupBoxPage.qml" } diff --git a/examples/controls/gallery/gallery.qrc b/examples/controls/gallery/gallery.qrc index 89958333..e29c18ce 100644 --- a/examples/controls/gallery/gallery.qrc +++ b/examples/controls/gallery/gallery.qrc @@ -47,5 +47,6 @@ <file>pages/TextAreaPage.qml</file> <file>pages/TextFieldPage.qml</file> <file>pages/TumblerPage.qml</file> + <file>pages/DelegatePage.qml</file> </qresource> </RCC> diff --git a/examples/controls/gallery/pages/DelegatePage.qml b/examples/controls/gallery/pages/DelegatePage.qml new file mode 100644 index 00000000..049ba00c --- /dev/null +++ b/examples/controls/gallery/pages/DelegatePage.qml @@ -0,0 +1,164 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import QtQuick.Layouts 1.1 +import Qt.labs.controls 1.0 + +Pane { + padding: 0 + + property var delegateComponentMap: { + "ItemDelegate": itemDelegateComponent, + "SwipeDelegate": swipeDelegateComponent + } + + Component { + id: itemDelegateComponent + + ItemDelegate { + text: labelText + width: parent.width + } + } + + Component { + id: swipeDelegateComponent + + SwipeDelegate { + id: swipeDelegate + text: labelText + width: parent.width + + onClicked: if (exposure.active) view.model.remove(ourIndex) + + Component { + id: removeComponent + + Rectangle { + color: swipeDelegate.exposed && swipeDelegate.pressed ? "#333" : "#444" + width: parent.width + height: parent.height + + Label { + text: "Remove" + color: "white" + anchors.centerIn: parent + } + } + } + + exposure.left: removeComponent + exposure.right: removeComponent + } + } + + ColumnLayout { + id: column + spacing: 40 + anchors.fill: parent + anchors.topMargin: 20 + + Label { + Layout.fillWidth: true + wrapMode: Label.Wrap + horizontalAlignment: Qt.AlignHCenter + text: "Delegate controls are used as delegates in views such as ListView." + } + + ListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: ListModel { + ListElement { type: "ItemDelegate"; text: "ItemDelegate" } + ListElement { type: "ItemDelegate"; text: "ItemDelegate" } + ListElement { type: "ItemDelegate"; text: "ItemDelegate" } + ListElement { type: "SwipeDelegate"; text: "SwipeDelegate" } + ListElement { type: "SwipeDelegate"; text: "SwipeDelegate" } + ListElement { type: "SwipeDelegate"; text: "SwipeDelegate" } + } + + section.property: "type" + section.delegate: Pane { + width: listView.width + height: sectionLabel.implicitHeight + 20 + + Label { + id: sectionLabel + text: section + anchors.centerIn: parent + } + } + + delegate: Loader { + id: delegateLoader + width: listView.width + sourceComponent: delegateComponentMap[text] + + property string labelText: text + property ListView view: listView + property int ourIndex: index + + // Can't find a way to do this in the SwipeDelegate component itself, + // so do it here instead. + ListView.onRemove: SequentialAnimation { + PropertyAction { + target: delegateLoader + property: "ListView.delayRemove" + value: true + } + NumberAnimation { + target: item + property: "height" + to: 0 + easing.type: Easing.InOutQuad + } + PropertyAction { + target: delegateLoader + property: "ListView.delayRemove" + value: false + } + } + } + } + } +} diff --git a/src/imports/controls/SwipeDelegate.qml b/src/imports/controls/SwipeDelegate.qml new file mode 100644 index 00000000..b3cf4714 --- /dev/null +++ b/src/imports/controls/SwipeDelegate.qml @@ -0,0 +1,99 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import Qt.labs.templates 1.0 as T + +T.SwipeDelegate { + id: control + + implicitWidth: Math.max(background ? background.implicitWidth : 0, + contentItem.implicitWidth + leftPadding + rightPadding) + implicitHeight: Math.max(background ? background.implicitHeight : 0, + Math.max(contentItem.implicitHeight, + indicator ? indicator.implicitHeight : 0) + topPadding + bottomPadding) + baselineOffset: contentItem.y + contentItem.baselineOffset + + padding: 12 + spacing: 12 + + //! [contentItem] + contentItem: Text { + leftPadding: control.checkable && control.mirrored ? control.indicator.width + control.spacing : 0 + rightPadding: control.checkable && !control.mirrored ? control.indicator.width + control.spacing : 0 + + text: control.text + font: control.font + color: control.enabled ? "#26282a" : "#bdbebf" + elide: Text.ElideRight + visible: control.text + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + + Behavior on x { + enabled: !control.pressed + NumberAnimation { + easing.type: Easing.InOutCubic + duration: 400 + } + } + } + //! [contentItem] + + //! [indicator] + indicator: Image { + x: control.mirrored ? control.leftPadding : control.width - width - control.rightPadding + y: control.topPadding + (control.availableHeight - height) / 2 + + visible: control.checked + source: control.checkable ? "qrc:/qt-project.org/imports/Qt/labs/controls/images/check.png" : "" + } + //! [indicator] + + //! [background] + background: Rectangle { + color: control.pressed ? "#bdbebf" : "#eeeeee" + + Behavior on x { + enabled: !control.pressed + NumberAnimation { + easing.type: Easing.InOutCubic + duration: 400 + } + } + } + //! [background] +} diff --git a/src/imports/controls/controls.pri b/src/imports/controls/controls.pri index 32e86f3a..a0b3b51f 100644 --- a/src/imports/controls/controls.pri +++ b/src/imports/controls/controls.pri @@ -24,6 +24,7 @@ QML_CONTROLS = \ Slider.qml \ SpinBox.qml \ StackView.qml \ + SwipeDelegate.qml \ Switch.qml \ SwipeView.qml \ TabBar.qml \ diff --git a/src/imports/controls/designer/SwipeDelegateSpecifics.qml b/src/imports/controls/designer/SwipeDelegateSpecifics.qml new file mode 100644 index 00000000..32bf9141 --- /dev/null +++ b/src/imports/controls/designer/SwipeDelegateSpecifics.qml @@ -0,0 +1,56 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.1 +import HelperWidgets 2.0 +import QtQuick.Layouts 1.0 + +Column { + width: parent.width + + ButtonSection { + caption: qsTr("Item Delegate") + width: parent.width + } + + ControlSection { + width: parent.width + } + + PaddingSection { + width: parent.width + } +} diff --git a/src/imports/controls/designer/designer.pri b/src/imports/controls/designer/designer.pri index 1589723c..412f6ad6 100644 --- a/src/imports/controls/designer/designer.pri +++ b/src/imports/controls/designer/designer.pri @@ -21,6 +21,7 @@ QML_FILES += \ $$PWD/RadioButtonSpecifics.qml \ $$PWD/SliderSpecifics.qml \ $$PWD/SpinBoxSpecifics.qml \ + $$PWD/SwipeDelegateSpecifics.qml \ $$PWD/SwitchSpecifics.qml \ $$PWD/TextAreaSpecifics.qml \ $$PWD/TextFieldSpecifics.qml \ diff --git a/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-background.png b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-background.png Binary files differnew file mode 100644 index 00000000..07f388bb --- /dev/null +++ b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-background.png diff --git a/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-behind.gif b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-behind.gif Binary files differnew file mode 100644 index 00000000..97d6a592 --- /dev/null +++ b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-behind.gif diff --git a/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-contentItem.png b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-contentItem.png Binary files differnew file mode 100644 index 00000000..cec6cf15 --- /dev/null +++ b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-contentItem.png diff --git a/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-indicator.png b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-indicator.png Binary files differnew file mode 100644 index 00000000..1b43928b --- /dev/null +++ b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-indicator.png diff --git a/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-leading-trailing.gif b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-leading-trailing.gif Binary files differnew file mode 100644 index 00000000..0641bd14 --- /dev/null +++ b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate-leading-trailing.gif diff --git a/src/imports/controls/doc/images/qtlabscontrols-swipedelegate.gif b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate.gif Binary files differnew file mode 100644 index 00000000..86c380b7 --- /dev/null +++ b/src/imports/controls/doc/images/qtlabscontrols-swipedelegate.gif diff --git a/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate-background.qml b/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate-background.qml new file mode 100644 index 00000000..be665771 --- /dev/null +++ b/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate-background.qml @@ -0,0 +1,38 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: http://www.gnu.org/copyleft/fdl.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import Qt.labs.controls 1.0 + +SwipeDelegate { + text: "SwipeDelegate" + Rectangle { + anchors.fill: background + color: "transparent" + border.color: "red" + } +} diff --git a/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate-contentItem.qml b/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate-contentItem.qml new file mode 100644 index 00000000..897d4792 --- /dev/null +++ b/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate-contentItem.qml @@ -0,0 +1,38 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: http://www.gnu.org/copyleft/fdl.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import Qt.labs.controls 1.0 + +SwipeDelegate { + text: "SwipeDelegate" + Rectangle { + anchors.fill: contentItem + color: "transparent" + border.color: "red" + } +} diff --git a/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate-indicator.qml b/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate-indicator.qml new file mode 100644 index 00000000..22530e4b --- /dev/null +++ b/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate-indicator.qml @@ -0,0 +1,40 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: http://www.gnu.org/copyleft/fdl.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import Qt.labs.controls 1.0 + +SwipeDelegate { + text: "SwipeDelegate" + checked: true + checkable: true + Rectangle { + anchors.fill: indicator + color: "transparent" + border.color: "red" + } +} diff --git a/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate.qml b/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate.qml new file mode 100644 index 00000000..97f57dca --- /dev/null +++ b/src/imports/controls/doc/snippets/qtlabscontrols-swipedelegate.qml @@ -0,0 +1,87 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: http://www.gnu.org/copyleft/fdl.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import Qt.labs.controls 1.0 + +ListView { + id: listView + width: 100 + height: 120 + + model: ListModel { + ListElement { name: "Apple" } + ListElement { name: "Orange" } + ListElement { name: "Pear" } + } + + delegate: SwipeDelegate { + id: rootDelegate + width: listView.width + text: modelData + + ListView.onRemove: SequentialAnimation { + PropertyAction { + target: rootDelegate + property: "ListView.delayRemove" + value: true + } + NumberAnimation { + target: rootDelegate + property: "height" + to: 0 + easing.type: Easing.InOutQuad + } + PropertyAction { + target: rootDelegate + property: "ListView.delayRemove" + value: false + } + } + + onClicked: if (exposure.active) ListView.view.model.remove(index) + + Component { + id: removeComponent + + Rectangle { + color: rootDelegate.exposed && rootDelegate.pressed ? "#333" : "#444" + width: parent.width + height: parent.height + + Label { + text: "Remove" + color: "white" + anchors.centerIn: parent + } + } + } + + exposure.left: removeComponent + exposure.right: removeComponent + } +} diff --git a/src/imports/controls/doc/src/qtlabscontrols-customize.qdoc b/src/imports/controls/doc/src/qtlabscontrols-customize.qdoc index c122068d..dc71b596 100644 --- a/src/imports/controls/doc/src/qtlabscontrols-customize.qdoc +++ b/src/imports/controls/doc/src/qtlabscontrols-customize.qdoc @@ -472,6 +472,28 @@ \snippet StackView.qml replaceExit + \section1 Customizing SwipeDelegate + + SwipeDelegate consists of four visual items: \l {Control::background}{background}, + \l {Control::contentItem}{content item}, \c exposure.left, and \c exposure.right. + + \section3 Background + + \image qtlabscontrols-swipedelegate-background.png + + \snippet SwipeDelegate.qml background + + \section3 Content item + + \image qtlabscontrols-swipedelegate-contentItem.png + + \snippet SwipeDelegate.qml contentItem + + \section3 Left, right, and behind + + \image qtlabscontrols-swipedelegate.gif + + By default, there are no left, right, or behind items defined. \section1 Customizing SwipeView diff --git a/src/imports/controls/material/SwipeDelegate.qml b/src/imports/controls/material/SwipeDelegate.qml new file mode 100644 index 00000000..600eb480 --- /dev/null +++ b/src/imports/controls/material/SwipeDelegate.qml @@ -0,0 +1,168 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import Qt.labs.templates 1.0 as T +import Qt.labs.controls.material 1.0 +import Qt.labs.controls.material.impl 1.0 + +T.SwipeDelegate { + id: control + + implicitWidth: Math.max(background ? background.implicitWidth : 0, + contentItem.implicitWidth + leftPadding + rightPadding) + implicitHeight: Math.max(background ? background.implicitHeight : 0, + Math.max(contentItem.implicitHeight, + indicator ? indicator.implicitHeight : 0) + topPadding + bottomPadding) + baselineOffset: contentItem.y + contentItem.baselineOffset + + padding: 16 + spacing: 16 + + //! [indicator] + indicator: Rectangle { + id: indicatorItem + x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2 + y: control.topPadding + (control.availableHeight - height) / 2 + implicitWidth: 20 + implicitHeight: 20 + color: "transparent" + border.color: control.checked ? control.Material.accentColor : control.Material.secondaryTextColor + border.width: control.checked ? width / 2 : 2 + radius: 2 + + visible: control.checkable + + Behavior on border.width { + NumberAnimation { + duration: 100 + easing.type: Easing.OutCubic + } + } + + Behavior on border.color { + ColorAnimation { + duration: 100 + easing.type: Easing.OutCubic + } + } + + Ripple { + width: parent.width + height: width + control: control + colored: control.checked + opacity: control.pressed ? 1 : 0 + } + + // TODO: This needs to be transparent + Image { + id: checkImage + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + width: 16 + height: 16 + source: "qrc:/qt-project.org/imports/Qt/labs/controls/material/images/check.png" + fillMode: Image.PreserveAspectFit + + scale: control.checked ? 1 : 0 + Behavior on scale { NumberAnimation { duration: 100 } } + } + + states: State { + name: "checked" + when: control.checked + } + + transitions: Transition { + SequentialAnimation { + NumberAnimation { + target: indicatorItem + property: "scale" + // Go down 2 pixels in size. + to: 1 - 2 / indicatorItem.width + duration: 120 + } + NumberAnimation { + target: indicatorItem + property: "scale" + to: 1 + duration: 120 + } + } + } + } + //! [indicator] + + //! [contentItem] + contentItem: Text { + leftPadding: control.checkable && !control.mirrored ? control.indicator.width + control.spacing : 0 + rightPadding: control.checkable && control.mirrored ? control.indicator.width + control.spacing : 0 + + text: control.text + font: control.font + color: control.enabled ? control.Material.primaryTextColor : control.Material.hintTextColor + elide: Text.ElideRight + visible: control.text + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + + Behavior on x { + enabled: !control.pressed + NumberAnimation { + easing.type: Easing.InOutCubic + duration: 400 + } + } + } + //! [contentItem] + + //! [background] + background: Rectangle { + color: !control.enabled ? control.Material.swipeDelegateDisabledColor : + (control.pressed ? control.Material.swipeDelegatePressColor : + (control.activeFocus || control.hovered ? control.Material.swipeDelegateHoverColor : control.Material.swipeDelegateColor)) + + Behavior on x { + enabled: !control.pressed + NumberAnimation { + easing.type: Easing.InOutCubic + duration: 400 + } + } + } + //! [background] +} diff --git a/src/imports/controls/material/material.pri b/src/imports/controls/material/material.pri index e41c7d2d..6682c09b 100644 --- a/src/imports/controls/material/material.pri +++ b/src/imports/controls/material/material.pri @@ -38,6 +38,7 @@ QML_FILES += \ $$PWD/SliderHandle.qml \ $$PWD/SpinBox.qml \ $$PWD/StackView.qml \ + $$PWD/SwipeDelegate.qml \ $$PWD/SwipeView.qml \ $$PWD/Switch.qml \ $$PWD/TabBar.qml \ diff --git a/src/imports/controls/material/qquickmaterialstyle.cpp b/src/imports/controls/material/qquickmaterialstyle.cpp index 280acb77..9b6c4668 100644 --- a/src/imports/controls/material/qquickmaterialstyle.cpp +++ b/src/imports/controls/material/qquickmaterialstyle.cpp @@ -395,6 +395,14 @@ static const QRgb flatButtonPressColorLight = 0x66999999; static const QRgb flatButtonPressColorDark = 0x3FCCCCCC; static const QRgb flatButtonFocusColorLight = 0x33CCCCCC; static const QRgb flatButtonFocusColorDark = 0x26CCCCCC; +static const QRgb swipeDelegateColorLight = 0xFFD6D7D7; +static const QRgb swipeDelegateColorDark = 0xFF525252; +static const QRgb swipeDelegateHoverColorLight = 0xFFDFDFDF; +static const QRgb swipeDelegateHoverColorDark = 0xFF5D5D5D; +static const QRgb swipeDelegatePressColorLight = 0xFFCFCFCF; +static const QRgb swipeDelegatePressColorDark = 0xFF484848; +static const QRgb swipeDelegateDisabledColorLight = 0xFFEFEFEF; +static const QRgb swipeDelegateDisabledColorDark = 0xFF7C7C7C; static const QRgb frameColorLight = hintTextColorLight; static const QRgb frameColorDark = hintTextColorDark; static const QRgb switchUncheckedTrackColorLight = 0x42000000; @@ -730,6 +738,26 @@ QColor QQuickMaterialStyle::flatButtonFocusColor() const return QColor::fromRgba(m_theme == Light ? flatButtonFocusColorLight : flatButtonFocusColorDark); } +QColor QQuickMaterialStyle::swipeDelegateColor() const +{ + return QColor::fromRgba(m_theme == Light ? swipeDelegateColorLight : swipeDelegateColorDark); +} + +QColor QQuickMaterialStyle::swipeDelegateHoverColor() const +{ + return QColor::fromRgba(m_theme == Light ? swipeDelegateHoverColorLight : swipeDelegateHoverColorDark); +} + +QColor QQuickMaterialStyle::swipeDelegatePressColor() const +{ + return QColor::fromRgba(m_theme == Light ? swipeDelegatePressColorLight : swipeDelegatePressColorDark); +} + +QColor QQuickMaterialStyle::swipeDelegateDisabledColor() const +{ + return QColor::fromRgba(m_theme == Light ? swipeDelegateDisabledColorLight : swipeDelegateDisabledColorDark); +} + QColor QQuickMaterialStyle::frameColor() const { return QColor::fromRgba(m_theme == Light ? frameColorLight : frameColorDark); diff --git a/src/imports/controls/material/qquickmaterialstyle_p.h b/src/imports/controls/material/qquickmaterialstyle_p.h index 9f3dbbbd..98540cd2 100644 --- a/src/imports/controls/material/qquickmaterialstyle_p.h +++ b/src/imports/controls/material/qquickmaterialstyle_p.h @@ -81,6 +81,10 @@ class QQuickMaterialStyle : public QQuickStyle Q_PROPERTY(QColor raisedHighlightedButtonDisabledColor READ raisedHighlightedButtonDisabledColor NOTIFY paletteChanged FINAL) Q_PROPERTY(QColor flatButtonPressColor READ flatButtonPressColor NOTIFY paletteChanged FINAL) Q_PROPERTY(QColor flatButtonFocusColor READ flatButtonFocusColor NOTIFY paletteChanged FINAL) + Q_PROPERTY(QColor swipeDelegateColor READ swipeDelegateColor NOTIFY paletteChanged FINAL) + Q_PROPERTY(QColor swipeDelegateHoverColor READ swipeDelegateHoverColor NOTIFY paletteChanged FINAL) + Q_PROPERTY(QColor swipeDelegatePressColor READ swipeDelegatePressColor NOTIFY paletteChanged FINAL) + Q_PROPERTY(QColor swipeDelegateDisabledColor READ swipeDelegateDisabledColor NOTIFY paletteChanged FINAL) Q_PROPERTY(QColor frameColor READ frameColor NOTIFY paletteChanged FINAL) Q_PROPERTY(QColor checkBoxUncheckedRippleColor READ checkBoxUncheckedRippleColor NOTIFY paletteChanged FINAL) Q_PROPERTY(QColor checkBoxCheckedRippleColor READ checkBoxCheckedRippleColor NOTIFY paletteChanged FINAL) @@ -188,6 +192,10 @@ public: QColor raisedHighlightedButtonDisabledColor() const; QColor flatButtonPressColor() const; QColor flatButtonFocusColor() const; + QColor swipeDelegateColor() const; + QColor swipeDelegateHoverColor() const; + QColor swipeDelegatePressColor() const; + QColor swipeDelegateDisabledColor() const; QColor frameColor() const; QColor checkBoxUncheckedRippleColor() const; QColor checkBoxCheckedRippleColor() const; diff --git a/src/imports/controls/qtlabscontrolsplugin.cpp b/src/imports/controls/qtlabscontrolsplugin.cpp index 37b68ef5..c99e5fb1 100644 --- a/src/imports/controls/qtlabscontrolsplugin.cpp +++ b/src/imports/controls/qtlabscontrolsplugin.cpp @@ -119,6 +119,7 @@ void QtLabsControlsPlugin::registerTypes(const char *uri) qmlRegisterType(selector.select(QStringLiteral("/Slider.qml")), uri, 1, 0, "Slider"); qmlRegisterType(selector.select(QStringLiteral("/SpinBox.qml")), uri, 1, 0, "SpinBox"); qmlRegisterType(selector.select(QStringLiteral("/StackView.qml")), uri, 1, 0, "StackView"); + qmlRegisterType(selector.select(QStringLiteral("/SwipeDelegate.qml")), uri, 1, 0, "SwipeDelegate"); qmlRegisterType(selector.select(QStringLiteral("/SwipeView.qml")), uri, 1, 0, "SwipeView"); qmlRegisterType(selector.select(QStringLiteral("/Switch.qml")), uri, 1, 0, "Switch"); qmlRegisterType(selector.select(QStringLiteral("/TabBar.qml")), uri, 1, 0, "TabBar"); diff --git a/src/imports/controls/universal/SwipeDelegate.qml b/src/imports/controls/universal/SwipeDelegate.qml new file mode 100644 index 00000000..92c3a6dc --- /dev/null +++ b/src/imports/controls/universal/SwipeDelegate.qml @@ -0,0 +1,116 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import Qt.labs.templates 1.0 as T +import Qt.labs.controls.universal 1.0 + +T.SwipeDelegate { + id: control + + implicitWidth: Math.max(background ? background.implicitWidth : 0, + contentItem.implicitWidth + leftPadding + rightPadding) + implicitHeight: Math.max(background ? background.implicitHeight : 0, + Math.max(contentItem.implicitHeight, + indicator ? indicator.implicitHeight : 0) + topPadding + bottomPadding) + baselineOffset: contentItem.y + contentItem.baselineOffset + + spacing: 12 + + topPadding: 11 + leftPadding: 12 + rightPadding: 12 + bottomPadding: 13 + + //! [indicator] + indicator: Image { + x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2 + y: control.topPadding + (control.availableHeight - height) / 2 + + visible: control.checked + source: !control.checkable ? "" : "image://universal/checkmark/" + (!control.enabled ? control.Universal.baseLowColor : control.pressed ? control.Universal.baseHighColor : control.Universal.baseMediumHighColor) + } + //! [indicator] + + //! [contentItem] + contentItem: Text { + leftPadding: control.checkable && !control.mirrored ? control.indicator.width + control.spacing : 0 + rightPadding: control.checkable && control.mirrored ? control.indicator.width + control.spacing : 0 + + text: control.text + font: control.font + elide: Text.ElideRight + visible: control.text + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + renderType: Text.NativeRendering + + color: !control.enabled ? control.Universal.baseLowColor : control.Universal.baseHighColor + + Behavior on x { + enabled: !control.pressed + NumberAnimation { + easing.type: Easing.InOutCubic + duration: 400 + } + } + } + //! [contentItem] + + //! [background] + background: Rectangle { + color: !control.enabled ? control.Universal.chromeDisabledHighColor : + (control.pressed ? control.Universal.chromeHighColor : + (control.activeFocus || control.hovered ? control.Universal.chromeLowColor : control.Universal.chromeMediumColor)) + + Rectangle { + width: parent.width + height: parent.height + visible: control.activeFocus || control.highlighted + color: control.Universal.accent + opacity: control.Universal.theme === Universal.Light ? 0.4 : 0.6 + } + + Behavior on x { + enabled: !control.pressed + NumberAnimation { + easing.type: Easing.InOutCubic + duration: 400 + } + } + } + //! [background] +} diff --git a/src/imports/controls/universal/universal.pri b/src/imports/controls/universal/universal.pri index 965228cb..dc3002a3 100644 --- a/src/imports/controls/universal/universal.pri +++ b/src/imports/controls/universal/universal.pri @@ -23,6 +23,7 @@ QML_FILES += \ $$PWD/Slider.qml \ $$PWD/SpinBox.qml \ $$PWD/StackView.qml \ + $$PWD/SwipeDelegate.qml \ $$PWD/Switch.qml \ $$PWD/TabBar.qml \ $$PWD/TabButton.qml \ diff --git a/src/imports/templates/qtlabstemplatesplugin.cpp b/src/imports/templates/qtlabstemplatesplugin.cpp index bdf35802..ab50bcbc 100644 --- a/src/imports/templates/qtlabstemplatesplugin.cpp +++ b/src/imports/templates/qtlabstemplatesplugin.cpp @@ -65,6 +65,7 @@ #include <QtLabsTemplates/private/qquickslider_p.h> #include <QtLabsTemplates/private/qquickspinbox_p.h> #include <QtLabsTemplates/private/qquickstackview_p.h> +#include <QtLabsTemplates/private/qquickswipedelegate_p.h> #include <QtLabsTemplates/private/qquickswipeview_p.h> #include <QtLabsTemplates/private/qquickswitch_p.h> #include <QtLabsTemplates/private/qquicktabbar_p.h> @@ -133,6 +134,8 @@ void QtLabsTemplatesPlugin::registerTypes(const char *uri) qmlRegisterType<QQuickSpinBox>(uri, 1, 0, "SpinBox"); qmlRegisterType<QQuickSpinButton>(); qmlRegisterType<QQuickStackView>(uri, 1, 0, "StackView"); + qmlRegisterType<QQuickSwipeDelegate>(uri, 1, 0, "SwipeDelegate"); + qmlRegisterType<QQuickSwipeExposure>(); qmlRegisterType<QQuickSwipeViewAttached>(); qmlRegisterType<QQuickSwipeView>(uri, 1, 0, "SwipeView"); qmlRegisterType<QQuickSwitch>(uri, 1, 0, "Switch"); diff --git a/src/templates/qquickswipedelegate.cpp b/src/templates/qquickswipedelegate.cpp new file mode 100644 index 00000000..b66c2203 --- /dev/null +++ b/src/templates/qquickswipedelegate.cpp @@ -0,0 +1,845 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs Templates module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquickswipedelegate_p.h" +#include "qquickcontrol_p_p.h" +#include "qquickabstractbutton_p_p.h" + +#include <QtGui/qstylehints.h> +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpa/qplatformtheme.h> +#include <QtQml/qqmlinfo.h> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype SwipeDelegate + \inherits AbstractButton + \instantiates QQuickSwipeDelegate + \inqmlmodule Qt.labs.controls + \brief A swipable item delegate. + + SwipeDelegate presents a view item that can be swiped left or right to + expose more options or information. It is used as a delegate in views such + as \l ListView. + + SwipeDelegate inherits its API from AbstractButton. For instance, you can set + \l {AbstractButton::text}{text}, make items \l {AbstractButton::checkable}{checkable}, + and react to \l {AbstractButton::clicked}{clicks} using the AbstractButton API. + + Information regarding the progress of a swipe, as well as the components + that should be shown upon swiping, are both available through the + \l {SwipeDelegate::}{exposure} grouped property object. For example, + \c exposure.position holds the position of the + swipe within the range \c -1.0 to \c 1.0. The \c exposure.left + property determines which item will be displayed when the control is swiped + to the right, and vice versa for \c exposure.right. The positioning of these + components is left to applications to decide. For example, without specifying + any position for \c exposure.left or \c exposure.right, the following will + occur: + + \image qtlabscontrols-swipedelegate.gif + + If \c exposure.left and \c exposure.right are anchored to the left and + right of the \l background item (respectively), they'll behave like this: + + \image qtlabscontrols-swipedelegate-leading-trailing.gif + + When using \c exposure.left and \c exposure.right, the control cannot be + swiped past the left and right edges. To achieve this type of "wrapping" + behavior, set \c exposure.behind instead. This will result in the same + item being shown regardless of which direction the control is swiped. For + example, in the image below, we set \c exposure.behind and then swipe the + control repeatedly in both directions: + + \image qtlabscontrols-swipedelegate-behind.gif + + \labs + + \sa {Customizing SwipeDelegate} +*/ + +class QQuickSwipeExposurePrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QQuickSwipeExposure) + +public: + QQuickSwipeExposurePrivate(QQuickSwipeDelegate *control) : + control(control), + positionBeforePress(0), + position(0), + wasActive(false), + active(false), + left(nullptr), + behind(nullptr), + right(nullptr), + leftItem(nullptr), + behindItem(nullptr), + rightItem(nullptr) + { + } + + static QQuickSwipeExposurePrivate *get(QQuickSwipeExposure *exposure); + + QQuickItem *createDelegateItem(QQmlComponent *component); + QQuickItem *showRelevantItemForPosition(qreal position); + QQuickItem *createRelevantItemForDistance(qreal distance); + void createLeftItem(); + void createBehindItem(); + void createRightItem(); + void createAndShowLeftItem(); + void createAndShowBehindItem(); + void createAndShowRightItem(); + + void warnAboutMixingDelegates(); + void warnAboutSettingDelegatesWhileVisible(); + + QQuickSwipeDelegate *control; + // Same range as position, but is set before press events so that we can + // keep track of which direction the user must swipe when using left and right delegates. + qreal positionBeforePress; + qreal position; + // A "less strict" version of active that is true if active was true + // before the last press event. + bool wasActive; + bool active; + QQmlComponent *left; + QQmlComponent *behind; + QQmlComponent *right; + QQuickItem *leftItem; + QQuickItem *behindItem; + QQuickItem *rightItem; +}; + +QQuickSwipeExposurePrivate *QQuickSwipeExposurePrivate::get(QQuickSwipeExposure *exposure) +{ + return exposure->d_func(); +} + +QQuickItem *QQuickSwipeExposurePrivate::createDelegateItem(QQmlComponent *component) +{ + // If we don't use the correct context, it won't be possible to refer to + // the control's id from within the delegates. + QQmlContext *creationContext = component->creationContext(); + // The component might not have been created in QML, in which case + // the creation context will be null and we have to create it ourselves. + if (!creationContext) + creationContext = qmlContext(control); + QQmlContext *context = new QQmlContext(creationContext); + context->setContextObject(control); + QQuickItem *item = qobject_cast<QQuickItem*>(component->beginCreate(context)); + if (item) { + item->setParentItem(control); + component->completeCreate(); + } + return item; +} + +QQuickItem *QQuickSwipeExposurePrivate::showRelevantItemForPosition(qreal position) +{ + if (qFuzzyIsNull(position)) + return nullptr; + + if (behind) { + createAndShowBehindItem(); + return behindItem; + } + + if (right && position < 0.0) { + createAndShowRightItem(); + return rightItem; + } + + if (left && position > 0.0) { + createAndShowLeftItem(); + return leftItem; + } + + return nullptr; +} + +QQuickItem *QQuickSwipeExposurePrivate::createRelevantItemForDistance(qreal distance) +{ + if (qFuzzyIsNull(distance)) + return nullptr; + + if (behind) { + createBehindItem(); + return behindItem; + } + + // a) If the position before the press was 0.0, we know that *any* movement + // whose distance is negative will result in the right item being shown and + // vice versa. + // b) Once the control has been exposed (that is, swiped to the left or right, + // and hence the position is either -1.0 or 1.0), we must use the width of the + // relevant item to determine if the distance is larger than that item, + // in order to know whether or not to display it. + // c) If the control has been exposed, and the swipe is larger than the width + // of the relevant item from which the swipe started from, we must show the + // item on the other side (if any). + + if (right) { + if ((distance < 0.0 && positionBeforePress == 0.0) /* a) */ + || (rightItem && positionBeforePress == -1.0 && distance < rightItem->width()) /* b) */ + || (leftItem && positionBeforePress == 1.0 && qAbs(distance) > leftItem->width())) /* c) */ { + createRightItem(); + return rightItem; + } + } + + if (left) { + if ((distance > 0.0 && positionBeforePress == 0.0) /* a) */ + || (leftItem && positionBeforePress == 1.0 && qAbs(distance) < leftItem->width()) /* b) */ + || (rightItem && positionBeforePress == -1.0 && qAbs(distance) > rightItem->width())) /* c) */ { + createLeftItem(); + return leftItem; + } + } + + return nullptr; +} + +void QQuickSwipeExposurePrivate::createLeftItem() +{ + if (!leftItem) { + Q_Q(QQuickSwipeExposure); + q->setLeftItem(createDelegateItem(left)); + if (!leftItem) + qmlInfo(control) << "Failed to create left item:" << left->errors(); + } +} + +void QQuickSwipeExposurePrivate::createBehindItem() +{ + if (!behindItem) { + Q_Q(QQuickSwipeExposure); + q->setBehindItem(createDelegateItem(behind)); + if (!behindItem) + qmlInfo(control) << "Failed to create behind item:" << behind->errors(); + } +} + +void QQuickSwipeExposurePrivate::createRightItem() +{ + if (!rightItem) { + Q_Q(QQuickSwipeExposure); + q->setRightItem(createDelegateItem(right)); + if (!rightItem) + qmlInfo(control) << "Failed to create right item:" << right->errors(); + } +} + +void QQuickSwipeExposurePrivate::createAndShowLeftItem() +{ + createLeftItem(); + + if (leftItem) + leftItem->setVisible(true); + + if (rightItem) + rightItem->setVisible(false); +} + +void QQuickSwipeExposurePrivate::createAndShowBehindItem() +{ + createBehindItem(); + + if (behindItem) + behindItem->setVisible(true); +} + +void QQuickSwipeExposurePrivate::createAndShowRightItem() +{ + createRightItem(); + + // This item may have already existed but was hidden. + if (rightItem) + rightItem->setVisible(true); + + // The left item isn't visible when the right item is visible, so save rendering effort by hiding it. + if (leftItem) + leftItem->setVisible(false); +} + +void QQuickSwipeExposurePrivate::warnAboutMixingDelegates() +{ + qmlInfo(control) << "cannot set both behind and left/right properties"; +} + +void QQuickSwipeExposurePrivate::warnAboutSettingDelegatesWhileVisible() +{ + qmlInfo(control) << "left/right/behind properties may only be set when exposure.position is 0"; +} + +QQuickSwipeExposure::QQuickSwipeExposure(QQuickSwipeDelegate *control) : + QObject(*(new QQuickSwipeExposurePrivate(control))) +{ +} + +QQmlComponent *QQuickSwipeExposure::left() const +{ + Q_D(const QQuickSwipeExposure); + return d->left; +} + +void QQuickSwipeExposure::setLeft(QQmlComponent *left) +{ + Q_D(QQuickSwipeExposure); + if (left == d->left) + return; + + if (d->behind) { + d->warnAboutMixingDelegates(); + return; + } + + if (!qFuzzyIsNull(d->position)) { + d->warnAboutSettingDelegatesWhileVisible(); + return; + } + + d->left = left; + + if (!d->left) { + delete d->leftItem; + d->leftItem = nullptr; + } + + emit leftChanged(); +} + +QQmlComponent *QQuickSwipeExposure::behind() const +{ + Q_D(const QQuickSwipeExposure); + return d->behind; +} + +void QQuickSwipeExposure::setBehind(QQmlComponent *behind) +{ + Q_D(QQuickSwipeExposure); + if (behind == d->behind) + return; + + if (d->left || d->right) { + d->warnAboutMixingDelegates(); + return; + } + + if (!qFuzzyIsNull(d->position)) { + d->warnAboutSettingDelegatesWhileVisible(); + return; + } + + d->behind = behind; + + if (!d->behind) { + delete d->behindItem; + d->behindItem = nullptr; + } + + emit behindChanged(); +} + +QQmlComponent *QQuickSwipeExposure::right() const +{ + Q_D(const QQuickSwipeExposure); + return d->right; +} + +void QQuickSwipeExposure::setRight(QQmlComponent *right) +{ + Q_D(QQuickSwipeExposure); + if (right == d->right) + return; + + if (d->behind) { + d->warnAboutMixingDelegates(); + return; + } + + if (!qFuzzyIsNull(d->position)) { + d->warnAboutSettingDelegatesWhileVisible(); + return; + } + + d->right = right; + + if (!d->right) { + delete d->rightItem; + d->rightItem = nullptr; + } + + emit rightChanged(); +} + +QQuickItem *QQuickSwipeExposure::leftItem() const +{ + Q_D(const QQuickSwipeExposure); + return d->leftItem; +} + +void QQuickSwipeExposure::setLeftItem(QQuickItem *item) +{ + Q_D(QQuickSwipeExposure); + if (item == d->leftItem) + return; + + delete d->leftItem; + d->leftItem = item; + + if (d->leftItem) { + d->leftItem->setParentItem(d->control); + + if (qFuzzyIsNull(d->leftItem->z())) + d->leftItem->setZ(-2); + } + + emit leftItemChanged(); +} + +QQuickItem *QQuickSwipeExposure::behindItem() const +{ + Q_D(const QQuickSwipeExposure); + return d->behindItem; +} + +void QQuickSwipeExposure::setBehindItem(QQuickItem *item) +{ + Q_D(QQuickSwipeExposure); + if (item == d->behindItem) + return; + + delete d->behindItem; + d->behindItem = item; + + if (d->behindItem) { + d->behindItem->setParentItem(d->control); + + if (qFuzzyIsNull(d->behindItem->z())) + d->behindItem->setZ(-2); + } + + emit behindItemChanged(); +} + +QQuickItem *QQuickSwipeExposure::rightItem() const +{ + Q_D(const QQuickSwipeExposure); + return d->rightItem; +} + +void QQuickSwipeExposure::setRightItem(QQuickItem *item) +{ + Q_D(QQuickSwipeExposure); + if (item == d->rightItem) + return; + + delete d->rightItem; + d->rightItem = item; + + if (d->rightItem) { + d->rightItem->setParentItem(d->control); + + if (qFuzzyIsNull(d->rightItem->z())) + d->rightItem->setZ(-2); + } + + emit rightItemChanged(); +} + +qreal QQuickSwipeExposure::position() const +{ + Q_D(const QQuickSwipeExposure); + return d->position; +} + +void QQuickSwipeExposure::setPosition(qreal position) +{ + Q_D(QQuickSwipeExposure); + const qreal adjustedPosition = qBound(-1.0, position, 1.0); + if (adjustedPosition == d->position) + return; + + d->position = adjustedPosition; + + QQuickItem *relevantItem = d->showRelevantItemForPosition(d->position); + const qreal relevantWidth = relevantItem ? relevantItem->width() : 0.0; + d->control->contentItem()->setProperty("x", d->position * relevantWidth + d->control->leftPadding()); + if (QQuickItem *background = d->control->background()) + background->setProperty("x", d->position * relevantWidth); + + emit positionChanged(); +} + +bool QQuickSwipeExposure::isActive() const +{ + Q_D(const QQuickSwipeExposure); + return d->active; +} + +void QQuickSwipeExposure::setActive(bool active) +{ + Q_D(QQuickSwipeExposure); + if (active == d->active) + return; + + d->active = active; + emit activeChanged(); +} + +class QQuickSwipeDelegatePrivate : public QQuickAbstractButtonPrivate +{ + Q_DECLARE_PUBLIC(QQuickSwipeDelegate) + +public: + QQuickSwipeDelegatePrivate(QQuickSwipeDelegate *control) : + exposure(control) + { + } + + bool handleMousePressEvent(QQuickItem *item, QMouseEvent *event); + bool handleMouseMoveEvent(QQuickItem *item, QMouseEvent *event); + bool handleMouseReleaseEvent(QQuickItem *item, QMouseEvent *event); + + void resizeContent() override; + + QQuickSwipeExposure exposure; +}; + +bool QQuickSwipeDelegatePrivate::handleMousePressEvent(QQuickItem *item, QMouseEvent *event) +{ + Q_Q(QQuickSwipeDelegate); + QQuickSwipeExposurePrivate *exposurePrivate = QQuickSwipeExposurePrivate::get(&exposure); + // If the position is 0, we want to handle events ourself - we don't want child items to steal them. + // This code will only get called when a child item has been created; + // events will go through the regular channels (mousePressEvent()) until then. + if (qFuzzyIsNull(exposurePrivate->position)) { + q->mousePressEvent(event); + return true; + } + + exposurePrivate->positionBeforePress = exposurePrivate->position; + pressPoint = item->mapToItem(q, event->pos()); + return false; +} + +bool QQuickSwipeDelegatePrivate::handleMouseMoveEvent(QQuickItem *item, QMouseEvent *event) +{ + Q_Q(QQuickSwipeDelegate); + + if (autoRepeat) { + stopPressRepeat(); + } else if (holdTimer > 0) { + if (QLineF(pressPoint, event->localPos()).length() > QGuiApplication::styleHints()->startDragDistance()) + stopPressAndHold(); + } + + // Protect against division by zero. + if (width == 0) + return false; + + // Don't bother reacting to events if we don't have any delegates. + QQuickSwipeExposurePrivate *exposurePrivate = QQuickSwipeExposurePrivate::get(&exposure); + if (!exposurePrivate->left && !exposurePrivate->right && !exposurePrivate->behind) + return false; + + // Don't handle move events for the control if it wasn't pressed. + if (item == q && !pressed) + return false; + + const qreal distance = (event->pos() - pressPoint).x(); + if (!q->keepMouseGrab()) { + // Taken from QQuickDrawer::handleMouseMoveEvent; see comments there. + int threshold = qMax(20, QGuiApplication::styleHints()->startDragDistance() + 5); + const bool overThreshold = QQuickWindowPrivate::dragOverThreshold(distance, Qt::XAxis, event, threshold); + if (window && overThreshold) { + QQuickItem *grabber = q->window()->mouseGrabberItem(); + if (!grabber || !grabber->keepMouseGrab()) { + q->grabMouse(); + q->setKeepMouseGrab(overThreshold); + q->setPressed(true); + exposure.setActive(false); + } + } + } + + if (q->keepMouseGrab()) { + // Ensure we don't try to calculate a position when the user tried to drag + // to the left when the left item is already exposed, and vice versa. + // The code below assumes that the drag is valid, so if we don't have this check, + // the wrong items are visible and the swiping wraps. + if (exposurePrivate->behind + || ((exposurePrivate->left || exposurePrivate->right) + && (qFuzzyIsNull(exposurePrivate->positionBeforePress) + || (exposurePrivate->positionBeforePress == -1.0 && distance >= 0.0) + || (exposurePrivate->positionBeforePress == 1.0 && distance <= 0.0)))) { + + // We must instantiate the items here so that we can calculate the + // position against the width of the relevant item. + QQuickItem *relevantItem = exposurePrivate->createRelevantItemForDistance(distance); + // If there isn't any relevant item, the user may have swiped back to the 0 position, + // or they swiped back to a position that is equal to positionBeforePress. + const qreal normalizedDistance = relevantItem ? distance / relevantItem->width() : 0.0; + qreal position = 0; + + // If the control was exposed before the drag begun, the distance should be inverted. + // For example, if the control had been swiped to the right, the position would be 1.0. + // If the control was then swiped the left by a distance of -20 pixels, the normalized + // distance might be -0.2, for example, which cannot be used as the position; the swipe + // started from the right, so we account for that by adding the position. + if (qFuzzyIsNull(normalizedDistance)) { + // There are two cases when the normalizedDistance can be 0, + // and we must distinguish between them: + // + // a) The swipe returns to the position that it was at before the press event. + // In this case, the distance will be 0. + // There would have been many position changes in the meantime, so we can't just + // ignore the move event; we have to set position to what it was before the press. + // + // b) If the position was at, 1.0, for example, and the control was then swiped + // to the left by the exact width of the left item, there won't be any relevant item + // (because the swipe's position would be at 0.0). In turn, the normalizedDistance + // would be 0 (because of the lack of a relevant item), but the distance will be non-zero. + position = qFuzzyIsNull(distance) ? exposurePrivate->positionBeforePress : 0; + } else if (!exposurePrivate->wasActive) { + position = normalizedDistance; + } else { + position = distance > 0 ? normalizedDistance - 1.0 : normalizedDistance + 1.0; + } + + exposure.setPosition(position); + } + } + + event->accept(); + + return q->keepMouseGrab(); +} + +bool QQuickSwipeDelegatePrivate::handleMouseReleaseEvent(QQuickItem *, QMouseEvent *) +{ + Q_Q(QQuickSwipeDelegate); + + QQuickSwipeExposurePrivate *exposurePrivate = QQuickSwipeExposurePrivate::get(&exposure); + + if (exposurePrivate->position > 0.5) { + exposure.setPosition(1.0); + exposure.setActive(true); + exposurePrivate->wasActive = true; + } else if (exposurePrivate->position < -0.5) { + exposure.setPosition(-1.0); + exposure.setActive(true); + exposurePrivate->wasActive = true; + } else { + exposure.setPosition(0.0); + exposure.setActive(false); + exposurePrivate->wasActive = false; + } + + q->setKeepMouseGrab(false); + + return true; +} + +void QQuickSwipeDelegatePrivate::resizeContent() +{ + // If the background and contentItem are outside the visible bounds + // of the control (we clip anything outside the bounds), we don't want + // to call QQuickControlPrivate's implementation of this function, + // as it repositions the contentItem to be visible. + QQuickSwipeExposurePrivate *exposurePrivate = QQuickSwipeExposurePrivate::get(&exposure); + if (!exposurePrivate->active) { + QQuickAbstractButtonPrivate::resizeContent(); + } +} + +QQuickSwipeDelegate::QQuickSwipeDelegate(QQuickItem *parent) : + QQuickAbstractButton(*(new QQuickSwipeDelegatePrivate(this)), parent) +{ + setFiltersChildMouseEvents(true); +} + +/*! + \qmlpropertygroup Qt.labs.controls::SwipeDelegate::exposure + \qmlproperty real Qt.labs.controls::SwipeDelegate::exposure.position + \qmlproperty bool Qt.labs.controls::SwipeDelegate::exposure.active + \qmlproperty Component Qt.labs.controls::SwipeDelegate::exposure.left + \qmlproperty Component Qt.labs.controls::SwipeDelegate::exposure.behind + \qmlproperty Component Qt.labs.controls::SwipeDelegate::exposure.right + \qmlproperty Item Qt.labs.controls::SwipeDelegate::exposure.leftItem + \qmlproperty Item Qt.labs.controls::SwipeDelegate::exposure.behindItem + \qmlproperty Item Qt.labs.controls::SwipeDelegate::exposure.rightItem + + \table + \header + \li Property + \li Description + \row + \li position + \li This property holds the position of the swipe relative to either + side of the control. When this value reaches either + \c -1.0 (left side) or \c 1.0 (right side) and the mouse button is + released, \c active will be \c true. + \row + \li active + \li This property holds whether the control is fully exposed. It is + equivalent to \c {!pressed && (position == -1.0 || position == 1.0)}. + + When active is \c true, any interactive items declared in \l left + or \l right will receive mouse events. + \row + \li left + \li This property holds the left delegate. + + The left delegate sits behind both \l {Control::}{contentItem} and + \l background. When the SwipeDelegate is swiped to the right, this item + will be gradually revealed. + \row + \li behind + \li This property holds the delegate that is shown when the + SwipeDelegate is swiped to both the left and right. + + As with the \c left and \c right delegates, it sits behind both + \l {Control::}{contentItem} and \l background. However, a SwipeDelegate + whose \c behind has been set can be continuously swiped from either + side, and will always show the same item. + \row + \li right + \li This property holds the right delegate. + + The right delegate sits behind both \l {Control::}{contentItem} and + \l background. When the SwipeDelegate is swiped to the left, this item + will be gradually revealed. + \row + \li leftItem + \li This property holds the item instantiated from the \c left component. + + If \c left has not been set, or the position hasn't changed since + creation of the SwipeDelegate, this property will be \c null. + \row + \li behindItem + \li This property holds the item instantiated from the \c behind component. + + If \c behind has not been set, or the position hasn't changed since + creation of the SwipeDelegate, this property will be \c null. + \row + \li rightItem + \li This property holds the item instantiated from the \c right component. + + If \c right has not been set, or the position hasn't changed since + creation of the SwipeDelegate, this property will be \c null. + \endtable + + \sa {Control::}{contentItem}, {Control::}{background} +*/ +QQuickSwipeExposure *QQuickSwipeDelegate::exposure() const +{ + Q_D(const QQuickSwipeDelegate); + return const_cast<QQuickSwipeExposure*>(&d->exposure); +} + +static bool isChildOrGrandchildOf(QQuickItem *child, QQuickItem *item) +{ + return item && (child == item || item->isAncestorOf(child)); +} + +bool QQuickSwipeDelegate::childMouseEventFilter(QQuickItem *child, QEvent *event) +{ + Q_D(QQuickSwipeDelegate); + // The contentItem is, by default, usually a non-interactive item like Text, and + // the same applies to the background. This means that simply stacking the left/right/behind + // items before these items won't allow us to get mouse events when the control is not currently exposed + // but has been previously. Therefore, we instead call setFiltersChildMouseEvents(true) in the constructor + // and filter out child events only when the child is the left/right/behind item. + const QQuickSwipeExposurePrivate *exposurePrivate = QQuickSwipeExposurePrivate::get(&d->exposure); + if (!isChildOrGrandchildOf(child, exposurePrivate->leftItem) && !isChildOrGrandchildOf(child, exposurePrivate->behindItem) + && !isChildOrGrandchildOf(child, exposurePrivate->rightItem)) { + return false; + } + + switch (event->type()) { + case QEvent::MouseButtonPress: { + return d->handleMousePressEvent(child, static_cast<QMouseEvent *>(event)); + } case QEvent::MouseMove: { + return d->handleMouseMoveEvent(child, static_cast<QMouseEvent *>(event)); + } case QEvent::MouseButtonRelease: { + // Make sure that the control gets release events if it has created child + // items that are stealing events from it. + QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event); + QQuickAbstractButton::mouseReleaseEvent(mouseEvent); + return d->handleMouseReleaseEvent(child, mouseEvent); + } default: + return false; + } +} + +// We only override this to set positionBeforePress; +// otherwise, it's the same as the base class implementation. +void QQuickSwipeDelegate::mousePressEvent(QMouseEvent *event) +{ + Q_D(QQuickSwipeDelegate); + QQuickAbstractButton::mousePressEvent(event); + QQuickSwipeExposurePrivate *exposurePrivate = QQuickSwipeExposurePrivate::get(&d->exposure); + exposurePrivate->positionBeforePress = exposurePrivate->position; +} + +void QQuickSwipeDelegate::mouseMoveEvent(QMouseEvent *event) +{ + Q_D(QQuickSwipeDelegate); + d->handleMouseMoveEvent(this, event); +} + +void QQuickSwipeDelegate::mouseReleaseEvent(QMouseEvent *event) +{ + Q_D(QQuickSwipeDelegate); + QQuickAbstractButton::mouseReleaseEvent(event); + d->handleMouseReleaseEvent(this, event); +} + +QFont QQuickSwipeDelegate::defaultFont() const +{ + return QQuickControlPrivate::themeFont(QPlatformTheme::ItemViewFont); +} + +#ifndef QT_NO_ACCESSIBILITY +QAccessible::Role QQuickSwipeDelegate::accessibleRole() const +{ + return QAccessible::ListItem; +} +#endif + +QT_END_NAMESPACE diff --git a/src/templates/qquickswipedelegate_p.h b/src/templates/qquickswipedelegate_p.h new file mode 100644 index 00000000..45d6999c --- /dev/null +++ b/src/templates/qquickswipedelegate_p.h @@ -0,0 +1,145 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs Templates module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKSWIPEDELEGATE_P_H +#define QQUICKSWIPEDELEGATE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtLabsTemplates/private/qquickabstractbutton_p.h> + +QT_BEGIN_NAMESPACE + +class QQuickSwipeDelegatePrivate; +class QQuickSwipeExposure; + +class Q_LABSTEMPLATES_EXPORT QQuickSwipeDelegate : public QQuickAbstractButton +{ + Q_OBJECT + Q_PROPERTY(QQuickSwipeExposure *exposure READ exposure CONSTANT) + +public: + explicit QQuickSwipeDelegate(QQuickItem *parent = nullptr); + + QQuickSwipeExposure *exposure() const; + +protected: + bool childMouseEventFilter(QQuickItem *child, QEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + + QFont defaultFont() const override; + +#ifndef QT_NO_ACCESSIBILITY + QAccessible::Role accessibleRole() const override; +#endif + +private: + Q_DISABLE_COPY(QQuickSwipeDelegate) + Q_DECLARE_PRIVATE(QQuickSwipeDelegate) +}; + +class QQuickSwipeExposurePrivate; + +class Q_LABSTEMPLATES_EXPORT QQuickSwipeExposure : public QObject +{ + Q_OBJECT + Q_PROPERTY(qreal position READ position NOTIFY positionChanged FINAL) + Q_PROPERTY(bool active READ isActive NOTIFY activeChanged FINAL) + Q_PROPERTY(QQmlComponent *left READ left WRITE setLeft NOTIFY leftChanged FINAL) + Q_PROPERTY(QQmlComponent *behind READ behind WRITE setBehind NOTIFY behindChanged FINAL) + Q_PROPERTY(QQmlComponent *right READ right WRITE setRight NOTIFY rightChanged FINAL) + Q_PROPERTY(QQuickItem *leftItem READ leftItem NOTIFY leftItemChanged FINAL) + Q_PROPERTY(QQuickItem *behindItem READ behindItem NOTIFY behindItemChanged FINAL) + Q_PROPERTY(QQuickItem *rightItem READ rightItem NOTIFY rightItemChanged FINAL) + +public: + explicit QQuickSwipeExposure(QQuickSwipeDelegate *control); + + qreal position() const; + void setPosition(qreal position); + + bool isActive() const; + void setActive(bool active); + + QQmlComponent *left() const; + void setLeft(QQmlComponent *left); + + QQmlComponent *behind() const; + void setBehind(QQmlComponent *behind); + + QQmlComponent *right() const; + void setRight(QQmlComponent *right); + + QQuickItem *leftItem() const; + void setLeftItem(QQuickItem *item); + + QQuickItem *behindItem() const; + void setBehindItem(QQuickItem *item); + + QQuickItem *rightItem() const; + void setRightItem(QQuickItem *item); + +Q_SIGNALS: + void positionChanged(); + void activeChanged(); + void leftChanged(); + void behindChanged(); + void rightChanged(); + void leftItemChanged(); + void behindItemChanged(); + void rightItemChanged(); + +private: + Q_DISABLE_COPY(QQuickSwipeExposure) + Q_DECLARE_PRIVATE(QQuickSwipeExposure) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickSwipeDelegate) + +#endif // QQUICKSWIPEDELEGATE_P_H diff --git a/src/templates/templates.pri b/src/templates/templates.pri index c4651d82..c7e4bb4a 100644 --- a/src/templates/templates.pri +++ b/src/templates/templates.pri @@ -41,6 +41,7 @@ HEADERS += \ $$PWD/qquickspinbox_p.h \ $$PWD/qquickstackview_p.h \ $$PWD/qquickstackview_p_p.h \ + $$PWD/qquickswipedelegate_p.h \ $$PWD/qquickswipeview_p.h \ $$PWD/qquickswitch_p.h \ $$PWD/qquicktabbar_p.h \ @@ -86,6 +87,7 @@ SOURCES += \ $$PWD/qquickspinbox.cpp \ $$PWD/qquickstackview.cpp \ $$PWD/qquickstackview_p.cpp \ + $$PWD/qquickswipedelegate.cpp \ $$PWD/qquickswipeview.cpp \ $$PWD/qquickswitch.cpp \ $$PWD/qquicktabbar.cpp \ diff --git a/tests/auto/controls/data/tst_swipedelegate.qml b/tests/auto/controls/data/tst_swipedelegate.qml new file mode 100644 index 00000000..47b24c68 --- /dev/null +++ b/tests/auto/controls/data/tst_swipedelegate.qml @@ -0,0 +1,815 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import QtTest 1.0 +import Qt.labs.controls 1.0 + +TestCase { + id: testCase + width: 200 + height: 200 + visible: true + when: windowShown + name: "SwipeDelegate" + + readonly property int dragDistance: Math.max(20, Qt.styleHints.startDragDistance + 5) + + Component { + id: greenLeftComponent + + Rectangle { + objectName: "leftItem" + anchors.fill: parent + color: "green" + } + } + + Component { + id: redRightComponent + + Rectangle { + objectName: "rightItem" + anchors.fill: parent + color: "red" + } + } + + Component { + id: swipeDelegateComponent + + SwipeDelegate { + id: swipeDelegate + text: "SwipeDelegate" + width: 150 + exposure.left: greenLeftComponent + exposure.right: redRightComponent + } + } + + function test_defaults() { + var control = swipeDelegateComponent.createObject(testCase); + verify(control); + + compare(control.baselineOffset, control.contentItem.y + control.contentItem.baselineOffset); + compare(control.exposure.position, 0); + verify(!control.pressed); + verify(!control.exposure.active); + + control.destroy(); + } + + Component { + id: itemComponent + + Item {} + } + + // Assumes that the delegate is smaller than the width of the control. + function swipe(control, from, to) { + // Sanity check. + compare(control.exposure.position, from); + + var distance = (to - from) * control.width; + + mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton); + mouseMove(control, control.width / 2 + distance, control.height / 2, Qt.LeftButton); + mouseRelease(control, control.width / 2 + distance, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, to); + + if (control.exposure.position === -1.0) { + if (control.exposure.right) + verify(control.exposure.rightItem); + else if (control.exposure.behind) + verify(control.exposure.behindItem); + } else if (control.exposure.position === 1.0) { + if (control.exposure.left) + verify(control.exposure.leftItem); + else if (control.exposure.behind) + verify(control.exposure.behindItem); + } + } + + function test_settingDelegates() { + var control = swipeDelegateComponent.createObject(testCase); + verify(control); + + ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + + ":78:9: QML SwipeDelegate: cannot set both behind and left/right properties") + control.exposure.behind = itemComponent; + + // Shouldn't be any warnings when unsetting delegates. + control.exposure.left = null; + compare(control.exposure.leftItem, null); + + // right is still set. + ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + + ":78:9: QML SwipeDelegate: cannot set both behind and left/right properties") + control.exposure.behind = itemComponent; + + control.exposure.right = null; + compare(control.exposure.rightItem, null); + + control.exposure.behind = itemComponent; + + ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + + ":78:9: QML SwipeDelegate: cannot set both behind and left/right properties") + control.exposure.left = itemComponent; + + ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + + ":78:9: QML SwipeDelegate: cannot set both behind and left/right properties") + control.exposure.right = itemComponent; + + control.exposure.behind = null; + control.exposure.left = greenLeftComponent; + control.exposure.right = redRightComponent; + + // Test that the user is warned when attempting to set or unset left or + // right item while they're exposed. + // First, try the left item. + swipe(control, 0.0, 1.0); + + var oldLeft = control.exposure.left; + var oldLeftItem = control.exposure.leftItem; + ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + + ":78:9: QML SwipeDelegate: left/right/behind properties may only be set when exposure.position is 0") + control.exposure.left = null; + compare(control.exposure.left, oldLeft); + compare(control.exposure.leftItem, oldLeftItem); + + // Try the same thing with the right item. + swipe(control, 1.0, -1.0); + + var oldRight = control.exposure.right; + var oldRightItem = control.exposure.rightItem; + ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + + ":78:9: QML SwipeDelegate: left/right/behind properties may only be set when exposure.position is 0") + control.exposure.right = null; + compare(control.exposure.right, oldRight); + compare(control.exposure.rightItem, oldRightItem); + + // Return to the default position. + swipe(control, -1.0, 0.0); + + tryCompare(control.background, "x", 0, 1000); + + // Try the same thing with the behind item. + control.exposure.left = null; + verify(!control.exposure.left); + verify(!control.exposure.leftItem); + control.exposure.right = null; + verify(!control.exposure.right); + verify(!control.exposure.rightItem); + control.exposure.behind = greenLeftComponent; + verify(control.exposure.behind); + verify(!control.exposure.behindItem); + + swipe(control, 0.0, 1.0); + + var oldBehind = control.exposure.behind; + var oldBehindItem = control.exposure.behindItem; + ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") + + ":78:9: QML SwipeDelegate: left/right/behind properties may only be set when exposure.position is 0") + control.exposure.behind = null; + compare(control.exposure.behind, oldBehind); + compare(control.exposure.behindItem, oldBehindItem); + + control.destroy(); + } + + ControlSpy { + id: mouseEventControlSpy + signals: ["pressed", "released", "canceled", "clicked", "doubleClicked", "pressedChanged"] + } + + function test_swipe() { + var control = swipeDelegateComponent.createObject(testCase); + verify(control); + + var overDragDistance = dragDistance * 1.1; + + mouseEventControlSpy.target = control; + mouseEventControlSpy.expectedSequence = [["pressedChanged", { "pressed": true }], "pressed"]; + mousePress(control, control.width / 2, control.height / 2); + verify(control.pressed); + compare(control.exposure.position, 0.0); + verify(!control.exposure.active); + verify(mouseEventControlSpy.success); + verify(!control.exposure.leftItem); + verify(!control.exposure.rightItem); + + // Drag to the right so that leftItem is created and visible. + mouseMove(control, control.width / 2 + overDragDistance, control.height / 2); + verify(control.pressed); + compare(control.exposure.position, overDragDistance / control.width); + verify(!control.exposure.active); + verify(control.exposure.leftItem); + verify(control.exposure.leftItem.visible); + compare(control.exposure.leftItem.parent, control); + compare(control.exposure.leftItem.objectName, "leftItem"); + verify(!control.exposure.rightItem); + + // Go back to 0. + mouseMove(control, control.width / 2, control.height / 2); + verify(control.pressed); + compare(control.exposure.position, 0.0); + verify(!control.exposure.active); + verify(control.exposure.leftItem); + verify(control.exposure.leftItem.visible); + compare(control.exposure.leftItem.parent, control); + compare(control.exposure.leftItem.objectName, "leftItem"); + verify(!control.exposure.rightItem); + + // Try the other direction. The right item should be created and visible, + // and the left item should be hidden. + mouseMove(control, control.width / 2 - overDragDistance, control.height / 2); + verify(control.pressed); + compare(control.exposure.position, -overDragDistance / control.width); + verify(!control.exposure.active); + verify(control.exposure.leftItem); + verify(!control.exposure.leftItem.visible); + verify(control.exposure.rightItem); + verify(control.exposure.rightItem.visible); + compare(control.exposure.rightItem.parent, control); + compare(control.exposure.rightItem.objectName, "rightItem"); + + // Now release outside the right edge of the control. + mouseMove(control, control.width * 1.1, control.height / 2); + verify(control.pressed); + compare(control.exposure.position, 0.6); + verify(!control.exposure.active); + verify(control.exposure.leftItem); + verify(control.exposure.leftItem.visible); + verify(control.exposure.rightItem); + verify(!control.exposure.rightItem.visible); + + mouseEventControlSpy.expectedSequence = [["pressedChanged", { "pressed": false }], "released", "clicked"]; + mouseRelease(control, control.width / 2, control.height / 2); + verify(!control.pressed); + compare(control.exposure.position, 1.0); + verify(control.exposure.active); + verify(mouseEventControlSpy.success); + verify(control.exposure.leftItem); + verify(control.exposure.leftItem.visible); + verify(control.exposure.rightItem); + verify(!control.exposure.rightItem.visible); + tryCompare(control.contentItem, "x", control.width + control.leftPadding); + + // Swiping from the right and releasing early should return position to 1.0. + mouseEventControlSpy.expectedSequence = [["pressedChanged", { "pressed": true }], "pressed"]; + mousePress(control, control.width / 2, control.height / 2); + verify(control.pressed); + compare(control.exposure.position, 1.0); + // exposed should still be true, because we haven't moved yet, and hence + // haven't started grabbing behind's mouse events. + verify(control.exposure.active); + verify(mouseEventControlSpy.success); + + mouseMove(control, control.width / 2 - overDragDistance, control.height / 2); + verify(control.pressed); + verify(!control.exposure.active); + compare(control.exposure.position, 1.0 - overDragDistance / control.width); + + mouseEventControlSpy.expectedSequence = [["pressedChanged", { "pressed": false }], "released", "clicked"]; + mouseRelease(control, control.width * 0.4, control.height / 2); + verify(!control.pressed); + compare(control.exposure.position, 1.0); + verify(control.exposure.active); + verify(mouseEventControlSpy.success); + tryCompare(control.contentItem, "x", control.width + control.leftPadding); + + // Swiping from the right and releasing should return contents to default position. + mouseEventControlSpy.expectedSequence = [["pressedChanged", { "pressed": true }], "pressed"]; + mousePress(control, control.width / 2, control.height / 2); + verify(control.pressed); + compare(control.exposure.position, 1.0); + verify(control.exposure.active); + verify(mouseEventControlSpy.success); + + mouseMove(control, control.width * -0.1, control.height / 2); + verify(control.pressed); + verify(!control.exposure.active); + compare(control.exposure.position, 0.4); + + mouseEventControlSpy.expectedSequence = [["pressedChanged", { "pressed": false }], "released", "clicked"]; + mouseRelease(control, control.width * -0.1, control.height / 2); + verify(!control.pressed); + compare(control.exposure.position, 0.0); + verify(!control.exposure.active); + verify(mouseEventControlSpy.success); + tryCompare(control.contentItem, "x", control.leftPadding); + + control.destroy(); + } + + Component { + id: swipeDelegateWithButtonComponent + SwipeDelegate { + text: "SwipeDelegate" + width: 150 + exposure.right: Button { + width: parent.width + height: parent.height + text: "Boo!" + } + } + } + + function test_eventsToLeftAndRight() { + var control = swipeDelegateWithButtonComponent.createObject(testCase); + verify(control); + + // The button should be pressed instead of the SwipeDelegate. + mouseDrag(control, control.width / 2, control.height / 2, -control.width, 0); + verify(!control.pressed); + compare(control.exposure.position, -1.0); + verify(control.exposure.rightItem); + verify(control.exposure.rightItem.visible); + compare(control.exposure.rightItem.parent, control); + + mousePress(control, control.width / 2, control.height / 2); + verify(!control.pressed); + var button = control.exposure.rightItem; + verify(button.pressed); + + mouseRelease(control, control.width / 2, control.height / 2); + verify(!button.pressed); + + // Returning back to a position of 0 and pressing on the control should + // result in the control being pressed. + mouseDrag(control, control.width / 2, control.height / 2, control.width * 0.6, 0); + compare(control.exposure.position, 0); + mousePress(control, control.width / 2, control.height / 2); + verify(control.pressed); + verify(!button.pressed); + mouseRelease(control, control.width / 2, control.height / 2); + verify(!control.pressed); + + control.destroy(); + } + + function test_mouseButtons() { + var control = swipeDelegateComponent.createObject(testCase); + verify(control); + + // click + mouseEventControlSpy.target = control; + mouseEventControlSpy.expectedSequence = [["pressedChanged", { "pressed": true }], "pressed"]; + mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton); + compare(control.pressed, true); + + verify(mouseEventControlSpy.success); + + mouseEventControlSpy.expectedSequence = [["pressedChanged", { "pressed": false }], "released", "clicked"]; + mouseRelease(control, control.width / 2, control.height / 2, Qt.LeftButton); + compare(control.pressed, false); + verify(mouseEventControlSpy.success); + + // right button + mouseEventControlSpy.expectedSequence = []; + mousePress(control, control.width / 2, control.height / 2, Qt.RightButton); + compare(control.pressed, false); + + mouseRelease(control, control.width / 2, control.height / 2, Qt.RightButton); + compare(control.pressed, false); + verify(mouseEventControlSpy.success); + + // double click + mouseEventControlSpy.expectedSequence = [ + ["pressedChanged", { "pressed": true }], + "pressed", + ["pressedChanged", { "pressed": false }], + "released", + "clicked", + ["pressedChanged", { "pressed": true }], + "pressed", + "doubleClicked", + ["pressedChanged", { "pressed": false }], + "released", + "clicked" + ]; + mouseDoubleClickSequence(control, control.width / 2, control.height / 2, Qt.LeftButton); + verify(mouseEventControlSpy.success); + + control.destroy(); + } + + Component { + id: removableDelegatesComponent + + ListView { + id: listView + width: 100 + height: 120 + + model: ListModel { + ListElement { name: "Apple" } + ListElement { name: "Orange" } + ListElement { name: "Pear" } + } + + delegate: SwipeDelegate { + id: rootDelegate + text: modelData + width: listView.width + + onClicked: if (exposure.active) ListView.view.model.remove(index) + + property alias removeAnimation: onRemoveAnimation + + ListView.onRemove: SequentialAnimation { + id: onRemoveAnimation + + PropertyAction { + target: rootDelegate + property: "ListView.delayRemove" + value: true + } + NumberAnimation { + target: rootDelegate + property: "height" + to: 0 + easing.type: Easing.InOutQuad + } + PropertyAction { + target: rootDelegate; + property: "ListView.delayRemove"; + value: false + } + } + + exposure.left: Rectangle { + color: rootDelegate.exposure.active && rootDelegate.pressed ? "#333" : "#444" + anchors.fill: parent + + Label { + objectName: "label" + text: "Remove" + color: "white" + anchors.centerIn: parent + } + } + } + } + } + + function test_removableDelegates() { + var listView = removableDelegatesComponent.createObject(testCase); + verify(listView); + compare(listView.count, 3); + + // Expose the remove button. + var firstItem = listView.itemAt(0, 0); + mousePress(listView, firstItem.width / 2, firstItem.height / 2); + verify(firstItem.pressed); + compare(firstItem.exposure.position, 0.0); + verify(!firstItem.exposure.active); + + mouseMove(listView, firstItem.width * 1.1, firstItem.height / 2); + verify(firstItem.pressed); + compare(firstItem.exposure.position, 0.6); + verify(!firstItem.exposure.active); + + mouseRelease(listView, firstItem.width / 2, firstItem.height / 2); + verify(!firstItem.pressed); + compare(firstItem.exposure.position, 1.0); + verify(firstItem.exposure.active); + compare(listView.count, 3); + + // Wait for it to settle down. + tryCompare(firstItem.contentItem, "x", firstItem.leftPadding + firstItem.width); + + // Click the button to remove the item. + var contentItemX = firstItem.contentItem.x; + mouseClick(listView, firstItem.width / 2, firstItem.height / 2); + 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 + // change x position and hence be visible when it shouldn't be. + verify(firstItem.removeAnimation.running); + while (1) { + wait(10) + if (firstItem && firstItem.removeAnimation && firstItem.removeAnimation.running) + compare(firstItem.contentItem.x, contentItemX); + else + break; + } + compare(listView.count, 2); + + listView.destroy(); + } + + Component { + id: leadingTrailingXComponent + SwipeDelegate { + id: delegate + width: 150 + text: "SwipeDelegate" + + exposure.left: Rectangle { + x: delegate.background.x - width + width: delegate.width + height: delegate.height + color: "green" + } + + exposure.right: Rectangle { + x: delegate.background.x + delegate.background.width + width: delegate.width + height: delegate.height + color: "red" + } + } + } + + Component { + id: leadingTrailingAnchorsComponent + SwipeDelegate { + id: delegate + width: 150 + text: "SwipeDelegate" + + exposure.left: Rectangle { + anchors.right: delegate.background.left + width: delegate.width + height: delegate.height + color: "green" + } + + exposure.right: Rectangle { + anchors.left: delegate.background.right + width: delegate.width + height: delegate.height + color: "red" + } + } + } + + function test_leadingTrailing_data() { + return [ + { tag: "x", component: leadingTrailingXComponent }, + { tag: "anchors", component: leadingTrailingAnchorsComponent }, + ]; + } + + function test_leadingTrailing(data) { + var control = data.component.createObject(testCase); + verify(control); + + mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton); + mouseMove(control, control.width, control.height / 2, Qt.LeftButton); + verify(control.exposure.leftItem); + compare(control.exposure.leftItem.x, -control.width / 2); + mouseRelease(control, control.width / 2, control.height / 2, Qt.LeftButton); + + control.destroy(); + } + + function test_minMaxPosition() { + var control = leadingTrailingXComponent.createObject(testCase); + verify(control); + + // Should be limited within the range -1.0 to 1.0. + mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton); + mouseMove(control, control.width * 1.5, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, 1.0); + mouseMove(control, control.width * 1.6, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, 1.0); + mouseMove(control, control.width * -1.6, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, -1.0); + mouseRelease(control, control.width / 2, control.height / 2, Qt.LeftButton); + + control.destroy(); + } + + Component { + id: emptySwipeDelegateComponent + + SwipeDelegate { + text: "SwipeDelegate" + width: 150 + } + } + + Component { + id: smallLeftComponent + + Rectangle { + width: 80 + height: 40 + color: "green" + } + } + + // exposure.position should be scaled to the width of the relevant delegate, + // and it shouldn't be possible to drag past the delegate (so that content behind the control is visible). + function test_delegateWidth() { + var control = emptySwipeDelegateComponent.createObject(testCase); + verify(control); + + control.exposure.left = smallLeftComponent; + + // Ensure that the position is scaled to the width of the currently visible delegate. + var overDragDistance = dragDistance * 1.1; + mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton); + mouseMove(control, control.width / 2 + overDragDistance, control.height / 2, Qt.LeftButton); + verify(control.exposure.leftItem); + compare(control.exposure.position, overDragDistance / control.exposure.leftItem.width); + + mouseMove(control, control.width / 2 + control.exposure.leftItem.width, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, 1.0); + + // Ensure that it's not possible to drag past the (left) delegate. + mouseMove(control, control.width / 2 + control.exposure.leftItem.width + 1, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, 1.0); + + // Now release over the right side; the position should be 1.0 and the background + // should be "anchored" to the right side of the left delegate item. + mouseMove(control, control.width / 2 + control.exposure.leftItem.width, control.height / 2, Qt.LeftButton); + mouseRelease(control, control.width / 2 + control.exposure.leftItem.width, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, 1.0); + tryCompare(control.background, "x", control.exposure.leftItem.width, 1000); + + control.destroy(); + } + + SignalSpy { + id: leftVisibleSpy + signalName: "visibleChanged" + } + + SignalSpy { + id: rightVisibleSpy + signalName: "visibleChanged" + } + + function test_positionAfterExposureMadeActive() { + var control = swipeDelegateComponent.createObject(testCase); + verify(control); + + // Ensure that both delegates are constructed. + mousePress(control, 0, control.height / 2, Qt.LeftButton); + mouseMove(control, control.width * 1.1, control.height / 2, Qt.LeftButton); + verify(control.exposure.leftItem); + mouseMove(control, control.width * -0.1, control.height / 2, Qt.LeftButton); + verify(control.exposure.rightItem); + + // Expose the left delegate. + mouseMove(control, control.exposure.leftItem.width, control.height / 2, Qt.LeftButton); + mouseRelease(control, control.exposure.leftItem.width, control.height / 2); + verify(control.exposure.active); + compare(control.exposure.position, 1.0); + + leftVisibleSpy.target = control.exposure.leftItem; + rightVisibleSpy.target = control.exposure.rightItem; + + // Swipe from right to left without exposing the right item, + // and make sure that the right item never becomes visible + // (and hence that the left item never loses visibility). + mousePress(control, control.exposure.leftItem.width, control.height / 2, Qt.LeftButton); + compare(leftVisibleSpy.count, 0); + compare(rightVisibleSpy.count, 0); + var newX = control.exposure.leftItem.width - dragDistance * 1.1; + mouseMove(control, newX, control.height / 2, Qt.LeftButton, Qt.LeftButton); + compare(leftVisibleSpy.count, 0); + compare(rightVisibleSpy.count, 0); + compare(control.exposure.position, newX / control.exposure.leftItem.width); + + mouseMove(control, 0, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, 0); + + // Test swiping over a distance that is greater than the width of the left item. + mouseMove(control, -1, control.height / 2, Qt.LeftButton); + verify(control.exposure.rightItem); + compare(control.exposure.position, -1 / control.exposure.rightItem.width); + + // Now go back to 1.0. + mouseMove(control, control.exposure.leftItem.width, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, 1.0); + tryCompare(control.background, "x", control.exposure.leftItem.width, 1000); + mouseRelease(control, control.exposure.leftItem.width, control.height / 2, Qt.LeftButton); + + control.destroy(); + } + + // TODO: this somehow results in the behind item having a negative width +// Component { +// id: behindSwipeDelegateComponent +// SwipeDelegate { +// anchors.centerIn: parent +// exposure.behind: Rectangle { +// onXChanged: print("x changed", x) +// anchors.left: { +// print("anchors.left expression", exposure.position) +// exposure.position < 0 ? parent.background.right : undefined +// } +// anchors.right: { +// print("anchors.right expression", exposure.position) +// exposure.position > 0 ? parent.background.left : undefined +// } +// width: parent.width +// height: parent.height +// color: "green" +// } +// exposure.left: null +// exposure.right: null +// Rectangle { +// anchors.fill: parent +// color: "transparent" +// border.color: "darkorange" +// } +// } +// } + + Component { + id: behindSwipeDelegateComponent + SwipeDelegate { + text: "SwipeDelegate" + width: 150 + anchors.centerIn: parent + exposure.behind: Rectangle { + x: exposure.position < 0 ? parent.background.x + parent.background.width + : (exposure.position > 0 ? parent.background.x - width : 0) + width: parent.width + height: parent.height + color: "green" + } + exposure.left: null + exposure.right: null + } + } + + function test_leadingTrailingBehindItem() { + var control = behindSwipeDelegateComponent.createObject(testCase); + verify(control); + + swipe(control, 0.0, 1.0); + verify(control.exposure.behindItem.visible); + compare(control.exposure.behindItem.x, control.background.x - control.background.width); + + swipe(control, 1.0, -1.0); + verify(control.exposure.behindItem.visible); + compare(control.exposure.behindItem.x, control.background.x + control.background.width); + + swipe(control, -1.0, 1.0); + verify(control.exposure.behindItem.visible); + compare(control.exposure.behindItem.x, control.background.x - control.background.width); + + // Should be possible to "wrap" with a behind delegate specified. + mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton); + mouseMove(control, control.width / 2 + control.exposure.behindItem.width * 0.8, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, -0.2); + mouseRelease(control, control.width / 2 + control.exposure.behindItem.width * 0.8, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, 0.0); + + // Try wrapping the other way. + swipe(control, 0.0, -1.0); + verify(control.exposure.behindItem.visible); + compare(control.exposure.behindItem.x, control.background.x + control.background.width); + + mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton); + mouseMove(control, control.width / 2 - control.exposure.behindItem.width * 0.8, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, 0.2); + mouseRelease(control, control.width / 2 - control.exposure.behindItem.width * 0.8, control.height / 2, Qt.LeftButton); + compare(control.exposure.position, 0.0); + + control.destroy(); + } +} diff --git a/tests/auto/pressandhold/tst_pressandhold.cpp b/tests/auto/pressandhold/tst_pressandhold.cpp index f61c9f56..8ec77f46 100644 --- a/tests/auto/pressandhold/tst_pressandhold.cpp +++ b/tests/auto/pressandhold/tst_pressandhold.cpp @@ -68,6 +68,7 @@ void tst_PressAndHold::pressAndHold_data() QTest::addColumn<QByteArray>("signal"); QTest::newRow("Button") << QByteArray("import Qt.labs.controls 1.0; Button { text: 'Button' }") << QByteArray(SIGNAL(pressAndHold())); + QTest::newRow("SwipeDelegate") << QByteArray("import Qt.labs.controls 1.0; SwipeDelegate { text: 'SwipeDelegate' }") << QByteArray(SIGNAL(pressAndHold())); QTest::newRow("TextField") << QByteArray("import Qt.labs.controls 1.0; TextField { text: 'TextField' }") << QByteArray(SIGNAL(pressAndHold(QQuickMouseEvent*))); QTest::newRow("TextArea") << QByteArray("import Qt.labs.controls 1.0; TextArea { text: 'TextArea' }") << QByteArray(SIGNAL(pressAndHold(QQuickMouseEvent*))); } diff --git a/tests/manual/gifs/data/qtlabscontrols-swipedelegate-behind.qml b/tests/manual/gifs/data/qtlabscontrols-swipedelegate-behind.qml new file mode 100644 index 00000000..31246a79 --- /dev/null +++ b/tests/manual/gifs/data/qtlabscontrols-swipedelegate-behind.qml @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import QtQuick.Window 2.0 +import Qt.labs.controls 1.0 + +Window { + width: swipeDelegate.implicitWidth + height: swipeDelegate.implicitHeight + visible: true + + property alias swipeDelegate: swipeDelegate + + SwipeDelegate { + id: swipeDelegate + text: "SwipeDelegate" + anchors.centerIn: parent + + exposure.left: null + exposure.right: null + exposure.behind: Rectangle { + width: swipeDelegate.width + height: swipeDelegate.height + color: swipeDelegate.pressed ? "#333" : "#444" + + Label { + text: "Behind Action" + color: "#fff" + anchors.centerIn: parent + } + } + } +} diff --git a/tests/manual/gifs/data/qtlabscontrols-swipedelegate-leading-trailing.qml b/tests/manual/gifs/data/qtlabscontrols-swipedelegate-leading-trailing.qml new file mode 100644 index 00000000..d346a377 --- /dev/null +++ b/tests/manual/gifs/data/qtlabscontrols-swipedelegate-leading-trailing.qml @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import QtQuick.Window 2.0 +import Qt.labs.controls 1.0 + +Window { + width: swipeDelegate.implicitWidth + height: swipeDelegate.implicitHeight + visible: true + + property alias swipeDelegate: swipeDelegate + + SwipeDelegate { + id: swipeDelegate + text: "SwipeDelegate" + anchors.centerIn: parent + + exposure.left: Rectangle { + width: swipeDelegate.width + height: swipeDelegate.height + color: swipeDelegate.pressed ? "#333" : "#444" + anchors.right: parent.left + + Label { + text: "Left Action" + color: "#fff" + anchors.centerIn: parent + } + } + + exposure.right: Rectangle { + width: swipeDelegate.width + height: swipeDelegate.height + color: swipeDelegate.pressed ? "#333" : "#444" + anchors.left: parent.right + + Label { + text: "Right Action" + color: "#fff" + anchors.centerIn: parent + } + } + } +} diff --git a/tests/manual/gifs/data/qtlabscontrols-swipedelegate.qml b/tests/manual/gifs/data/qtlabscontrols-swipedelegate.qml new file mode 100644 index 00000000..78d36e3b --- /dev/null +++ b/tests/manual/gifs/data/qtlabscontrols-swipedelegate.qml @@ -0,0 +1,82 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import QtQuick.Window 2.0 +import Qt.labs.controls 1.0 + +Window { + width: swipeDelegate.implicitWidth + height: swipeDelegate.implicitHeight + visible: true + + property alias swipeDelegate: swipeDelegate + + SwipeDelegate { + id: swipeDelegate + text: "SwipeDelegate" + anchors.centerIn: parent + + exposure.left: Rectangle { + width: swipeDelegate.width + height: swipeDelegate.height + color: swipeDelegate.pressed ? "#333" : "#444" + + Label { + text: "Left Action" + color: "#fff" + anchors.centerIn: parent + } + } + + exposure.right: Rectangle { + anchors.fill: parent + width: swipeDelegate.width + height: swipeDelegate.height + color: swipeDelegate.pressed ? "#333" : "#444" + + Label { + text: "Right Action" + color: "#fff" + anchors.centerIn: parent + } + } + } +} diff --git a/tests/manual/gifs/tst_gifs.cpp b/tests/manual/gifs/tst_gifs.cpp index 7f2a985c..4ab60657 100644 --- a/tests/manual/gifs/tst_gifs.cpp +++ b/tests/manual/gifs/tst_gifs.cpp @@ -57,6 +57,9 @@ private slots: void button(); void tabBar(); void menu(); + void swipeDelegate_data(); + void swipeDelegate(); + void swipeDelegateBehind(); private: void moveSmoothly(QQuickWindow *window, const QPoint &from, const QPoint &to, int movements, @@ -372,6 +375,97 @@ void tst_Gifs::menu() gifRecorder.waitForFinish(); } +void tst_Gifs::swipeDelegate_data() +{ + QTest::addColumn<QString>("qmlFileName"); + QTest::newRow("qtlabscontrols-swipedelegate.qml") << QString::fromLatin1("qtlabscontrols-swipedelegate.qml"); + QTest::newRow("qtlabscontrols-swipedelegate-leading-trailing.qml") << QString::fromLatin1("qtlabscontrols-swipedelegate-leading-trailing.qml"); +} + +void tst_Gifs::swipeDelegate() +{ + QFETCH(QString, qmlFileName); + + GifRecorder gifRecorder; + gifRecorder.setDataDirPath(dataDirPath); + gifRecorder.setOutputDir(outputDir); + gifRecorder.setRecordingDuration(10); + gifRecorder.setQmlFileName(qmlFileName); + gifRecorder.setHighQuality(true); + + gifRecorder.start(); + + QQuickWindow *window = gifRecorder.window(); + QQuickItem *swipeDelegate = window->property("swipeDelegate").value<QQuickItem*>(); + QVERIFY(swipeDelegate); + + // Show left item. + const QPoint leftTarget = QPoint(swipeDelegate->width() * 0.2, 0); + const QPoint rightTarget = QPoint(swipeDelegate->width() * 0.8, 0); + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, leftTarget, 100); + const int movements = rightTarget.x() - leftTarget.x(); + moveSmoothly(window, leftTarget, rightTarget, movements, QEasingCurve::OutQuint, 5); + QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, rightTarget, 20); + + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, rightTarget, 1000); + moveSmoothly(window, rightTarget, leftTarget, movements, QEasingCurve::OutQuint, 5); + QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, leftTarget, 20); + + QTest::qWait(1000); + + // Show right item. + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, rightTarget, 1000); + moveSmoothly(window, rightTarget, leftTarget, movements, QEasingCurve::OutQuint, 5); + QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, leftTarget, 20); + + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, leftTarget, 1000); + moveSmoothly(window, leftTarget, rightTarget, movements, QEasingCurve::OutQuint, 5); + QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, rightTarget, 20); + + gifRecorder.waitForFinish(); +} + +void tst_Gifs::swipeDelegateBehind() +{ + GifRecorder gifRecorder; + gifRecorder.setDataDirPath(dataDirPath); + gifRecorder.setOutputDir(outputDir); + gifRecorder.setRecordingDuration(14); + gifRecorder.setQmlFileName(QStringLiteral("qtlabscontrols-swipedelegate-behind.qml")); + gifRecorder.setHighQuality(true); + + gifRecorder.start(); + + QQuickWindow *window = gifRecorder.window(); + QQuickItem *swipeDelegate = window->property("swipeDelegate").value<QQuickItem*>(); + QVERIFY(swipeDelegate); + + // Show wrapping around left item. + const QPoint leftTarget = QPoint(swipeDelegate->width() * 0.2, 0); + const QPoint rightTarget = QPoint(swipeDelegate->width() * 0.8, 0); + const int movements = rightTarget.x() - leftTarget.x(); + for (int i = 0; i < 4; ++i) { + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, leftTarget, 100); + moveSmoothly(window, leftTarget, rightTarget, movements, QEasingCurve::OutQuint, 5); + QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, rightTarget, 20); + + QTest::qWait(500); + } + + QTest::qWait(1000); + + // Show wrapping around right item. + for (int i = 0; i < 4; ++i) { + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, rightTarget, 100); + moveSmoothly(window, rightTarget, leftTarget, movements, QEasingCurve::OutQuint, 5); + QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, leftTarget, 20); + + QTest::qWait(500); + } + + gifRecorder.waitForFinish(); +} + QTEST_MAIN(tst_Gifs) #include "tst_gifs.moc" |