diff options
Diffstat (limited to 'src/quick')
27 files changed, 2324 insertions, 118 deletions
diff --git a/src/quick/configure.json b/src/quick/configure.json index 9ec3531ef4..70fe6d2129 100644 --- a/src/quick/configure.json +++ b/src/quick/configure.json @@ -2,7 +2,8 @@ "module": "quick", "depends": [ "qml-private", - "gui-private" + "gui-private", + "qmlmodels-private" ], "testDir": "../../config.tests", diff --git a/src/quick/doc/snippets/pointerHandlers/handlerFlick.qml b/src/quick/doc/snippets/pointerHandlers/handlerFlick.qml new file mode 100644 index 0000000000..f74b075357 --- /dev/null +++ b/src/quick/doc/snippets/pointerHandlers/handlerFlick.qml @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, 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$ +** +****************************************************************************/ +//![0] +import QtQuick 2.14 +import Qt.labs.animation 1.0 + +Item { + width: 320; height: 480 + Flow { + id: content + width: parent.width + spacing: 2; padding: 2 + + WheelHandler { + orientation: Qt.Vertical + property: "y" + rotationScale: 15 + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onActiveChanged: if (!active) ybr.returnToBounds() + } + + DragHandler { + xAxis.enabled: false + onActiveChanged: if (!active) ybr.returnToBounds() + } + + BoundaryRule on y { + id: ybr + minimum: content.parent.height - content.height + maximum: 0 + minimumOvershoot: 400; maximumOvershoot: 400 + overshootFilter: BoundaryRule.Peak + } + + Repeater { + model: 1000 + Rectangle { color: "gray"; width: 10 + Math.random() * 100; height: 15 } + } + } +} +//![0] diff --git a/src/quick/doc/snippets/pointerHandlers/wheelHandler.qml b/src/quick/doc/snippets/pointerHandlers/wheelHandler.qml new file mode 100644 index 0000000000..2c9913ac97 --- /dev/null +++ b/src/quick/doc/snippets/pointerHandlers/wheelHandler.qml @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, 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$ +** +****************************************************************************/ +//![0] +import QtQuick 2.14 + +Rectangle { + width: 170; height: 120 + color: "green"; antialiasing: true + + WheelHandler { + property: "rotation" + onWheel: console.log("rotation", event.angleDelta.y, + "scaled", rotation, "@", point.position, "=>", parent.rotation) + } +} +//![0] diff --git a/src/quick/doc/snippets/qml/boundaryRule.qml b/src/quick/doc/snippets/qml/boundaryRule.qml new file mode 100644 index 0000000000..c010f5de5e --- /dev/null +++ b/src/quick/doc/snippets/qml/boundaryRule.qml @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, 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$ +** +****************************************************************************/ +//![0] +import QtQuick 2.14 +import Qt.labs.animation 1.0 + +Rectangle { + id: root + width: 170; height: 120 + color: "green" + + DragHandler { + id: dragHandler + yAxis.minimum: -1000 + xAxis.minimum: -1000 + onActiveChanged: if (!active) xbr.returnToBounds(); + } + + BoundaryRule on x { + id: xbr + minimum: -50 + maximum: 100 + minimumOvershoot: 40 + maximumOvershoot: 40 + } +} +//![0] diff --git a/src/quick/doc/snippets/qml/tableview/tablemodel.cpp b/src/quick/doc/snippets/qml/tableview/cpp-tablemodel.cpp index ea9f76f131..ea9f76f131 100644 --- a/src/quick/doc/snippets/qml/tableview/tablemodel.cpp +++ b/src/quick/doc/snippets/qml/tableview/cpp-tablemodel.cpp diff --git a/src/quick/doc/snippets/qml/tableview/tablemodel.qml b/src/quick/doc/snippets/qml/tableview/cpp-tablemodel.qml index 8a8ec94958..8a8ec94958 100644 --- a/src/quick/doc/snippets/qml/tableview/tablemodel.qml +++ b/src/quick/doc/snippets/qml/tableview/cpp-tablemodel.qml diff --git a/src/quick/doc/snippets/qml/tableview/qml-tablemodel.qml b/src/quick/doc/snippets/qml/tableview/qml-tablemodel.qml new file mode 100644 index 0000000000..a01a2a628a --- /dev/null +++ b/src/quick/doc/snippets/qml/tableview/qml-tablemodel.qml @@ -0,0 +1,92 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, 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$ +** +****************************************************************************/ + +//![0] +import QtQuick 2.14 +import Qt.labs.qmlmodels 1.0 + +TableView { + anchors.fill: parent + columnSpacing: 1 + rowSpacing: 1 + clip: true + + model: TableModel { + TableModelColumn { display: "name" } + TableModelColumn { display: "color" } + + rows: [ + { + "name": "cat", + "color": "black" + }, + { + "name": "dog", + "color": "brown" + }, + { + "name": "bird", + "color": "white" + } + ] + } + + delegate: Rectangle { + implicitWidth: 100 + implicitHeight: 50 + border.width: 1 + + Text { + text: display + anchors.centerIn: parent + } + } +} +//![0] diff --git a/src/quick/handlers/handlers.pri b/src/quick/handlers/handlers.pri index fa2f25c793..49975bd1ca 100644 --- a/src/quick/handlers/handlers.pri +++ b/src/quick/handlers/handlers.pri @@ -27,3 +27,9 @@ SOURCES += \ $$PWD/qquicksinglepointhandler.cpp \ $$PWD/qquicktaphandler.cpp \ $$PWD/qquickdragaxis.cpp + +qtConfig(wheelevent) { + HEADERS += $$PWD/qquickwheelhandler_p.h $$PWD/qquickwheelhandler_p_p.h + SOURCES += $$PWD/qquickwheelhandler.cpp +} + diff --git a/src/quick/handlers/qquickmultipointhandler.cpp b/src/quick/handlers/qquickmultipointhandler.cpp index 5c10ecce75..0afc9997aa 100644 --- a/src/quick/handlers/qquickmultipointhandler.cpp +++ b/src/quick/handlers/qquickmultipointhandler.cpp @@ -70,14 +70,15 @@ bool QQuickMultiPointHandler::wantsPointerEvent(QQuickPointerEvent *event) if (!QQuickPointerDeviceHandler::wantsPointerEvent(event)) return false; -#if QT_CONFIG(gestures) - if (event->asPointerNativeGestureEvent()) - return true; -#endif - if (event->asPointerScrollEvent()) return false; + bool ret = false; +#if QT_CONFIG(gestures) + if (event->asPointerNativeGestureEvent() && event->point(0)->state() != QQuickEventPoint::Released) + ret = true; +#endif + // If points were pressed or released within parentItem, reset stored state // and check eligible points again. This class of handlers is intended to // handle a specific number of points, so a differing number of points will @@ -97,7 +98,7 @@ bool QQuickMultiPointHandler::wantsPointerEvent(QQuickPointerEvent *event) return true; } - const bool ret = (candidatePoints.size() >= minimumPointCount() && candidatePoints.size() <= maximumPointCount()); + ret = ret || (candidatePoints.size() >= minimumPointCount() && candidatePoints.size() <= maximumPointCount()); if (ret) { const int c = candidatePoints.count(); d->currentPoints.resize(c); diff --git a/src/quick/handlers/qquickpinchhandler.cpp b/src/quick/handlers/qquickpinchhandler.cpp index 4025cd7fbf..a5a867015c 100644 --- a/src/quick/handlers/qquickpinchhandler.cpp +++ b/src/quick/handlers/qquickpinchhandler.cpp @@ -279,9 +279,9 @@ void QQuickPinchHandler::onActiveChanged() m_startScale = m_accumulatedScale; m_startRotation = 0; } - qCInfo(lcPinchHandler) << "activated with starting scale" << m_startScale << "rotation" << m_startRotation; + qCDebug(lcPinchHandler) << "activated with starting scale" << m_startScale << "rotation" << m_startRotation; } else { - qCInfo(lcPinchHandler) << "deactivated with scale" << m_activeScale << "rotation" << m_activeRotation; + qCDebug(lcPinchHandler) << "deactivated with scale" << m_activeScale << "rotation" << m_activeRotation; } } diff --git a/src/quick/handlers/qquickwheelhandler.cpp b/src/quick/handlers/qquickwheelhandler.cpp new file mode 100644 index 0000000000..90e4fef97e --- /dev/null +++ b/src/quick/handlers/qquickwheelhandler.cpp @@ -0,0 +1,520 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://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.LGPL3 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-3.0.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 (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquickwheelhandler_p.h" +#include "qquickwheelhandler_p_p.h" +#include <QLoggingCategory> +#include <QtMath> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcWheelHandler, "qt.quick.handler.wheel") + +/*! + \qmltype WheelHandler + \instantiates QQuickWheelHandler + \inqmlmodule QtQuick + \ingroup qtquick-input-handlers + \brief Handler for the mouse wheel. + + WheelHandler is a handler that is used to interactively manipulate some + numeric property of an Item as the user rotates the mouse wheel. Like other + Input Handlers, by default it manipulates its \l {PointerHandler::target} + {target}. Declare \l property to control which target property will be + manipulated: + + \snippet pointerHandlers/wheelHandler.qml 0 + + \l BoundaryRule is quite useful in combination with WheelHandler (as well + as with other Input Handlers) to declare the allowed range of values that + the target property can have. For example it is possible to implement + scrolling using a combination of WheelHandler and \l DragHandler to + manipulate the scrollable Item's \l{QQuickItem::y}{y} property when the + user rotates the wheel or drags the item on a touchscreen, and + \l BoundaryRule to limit the range of motion from the top to the bottom: + + \snippet pointerHandlers/handlerFlick.qml 0 + + Alternatively if \l targetProperty is not set or \l target is null, + WheelHandler will not automatically manipulate anything; but the + \l rotation property can be used in a binding to manipulate another + property, or you can implement \c onWheel and handle the wheel event + directly. + + WheelHandler handles only a rotating mouse wheel by default. + Optionally it can handle smooth-scrolling events from touchpad gestures, + by setting \l acceptedDevices to \c{PointerDevice.Mouse | PointerDevice.TouchPad}. + + \note Some non-mouse hardware (such as a touch-sensitive Wacom tablet, or + a Linux laptop touchpad) generates real wheel events from gestures. + WheelHandler will respond to those events as wheel events regardless of the + setting of the \l acceptedDevices property. + + \sa MouseArea + \sa Flickable +*/ + +QQuickWheelHandler::QQuickWheelHandler(QQuickItem *parent) + : QQuickSinglePointHandler(*(new QQuickWheelHandlerPrivate), parent) +{ + setAcceptedDevices(QQuickPointerDevice::Mouse); +} + +/*! + \qmlproperty enum QtQuick::WheelHandler::orientation + + Which wheel to react to. The default is \c Qt.Vertical. + + Not every mouse has a \c Horizontal wheel; sometimes it is emulated by + tilting the wheel sideways. A touchpad can usually generate both vertical + and horizontal wheel events. +*/ +Qt::Orientation QQuickWheelHandler::orientation() const +{ + Q_D(const QQuickWheelHandler); + return d->orientation; +} + +void QQuickWheelHandler::setOrientation(Qt::Orientation orientation) +{ + Q_D(QQuickWheelHandler); + if (d->orientation == orientation) + return; + + d->orientation = orientation; + emit orientationChanged(); +} + +/*! + \qmlproperty bool QtQuick::WheelHandler::invertible + + Whether or not to reverse the direction of property change if + \l QQuickPointerScrollEvent::inverted is true. The default is \c true. + + If the operating system has a "natural scrolling" setting that causes + scrolling to be in the same direction as the finger movement, then if this + property is set to \c true, and WheelHandler is directly setting a property + on \l target, the direction of movement will correspond to the system setting. + If this property is set to \l false, it will invert the \l rotation so that + the direction of motion is always the same as the direction of finger movement. +*/ +bool QQuickWheelHandler::isInvertible() const +{ + Q_D(const QQuickWheelHandler); + return d->invertible; +} + +void QQuickWheelHandler::setInvertible(bool invertible) +{ + Q_D(QQuickWheelHandler); + if (d->invertible == invertible) + return; + + d->invertible = invertible; + emit invertibleChanged(); +} + +/*! + \qmlproperty real QtQuick::WheelHandler::activeTimeout + + The amount of time in seconds after which the \l active property will + revert to \c false if no more wheel events are received. The default is + \c 0.1 (100 ms). + + When WheelHandler handles events that contain + \l {Qt::ScrollPhase}{scroll phase} information, such as events from some + touchpads, the \l active property will become \c false as soon as an event + with phase \l Qt::ScrollEnd is received; in that case the timeout is not + necessary. But a conventional mouse with a wheel does not provide the + \l {QQuickPointerScrollEvent::phase}{scroll phase}: the mouse cannot detect + when the user has decided to stop scrolling, so the \l active property + transitions to \c false after this much time has elapsed. +*/ +qreal QQuickWheelHandler::activeTimeout() const +{ + Q_D(const QQuickWheelHandler); + return d->activeTimeout; +} + +void QQuickWheelHandler::setActiveTimeout(qreal timeout) +{ + Q_D(QQuickWheelHandler); + if (qFuzzyCompare(d->activeTimeout, timeout)) + return; + + if (timeout < 0) { + qWarning("activeTimeout must be positive"); + return; + } + + d->activeTimeout = timeout; + emit activeTimeoutChanged(); +} + +/*! + \qmlproperty real QtQuick::WheelHandler::rotation + + The angle through which the mouse wheel has been rotated since the last + time this property was set, in wheel degrees. + + A positive value indicates that the wheel was rotated up/right; + a negative value indicates that the wheel was rotated down/left. + + A basic mouse click-wheel works in steps of 15 degrees. + + The default is \c 0 at startup. It can be programmatically set to any value + at any time. The value will be adjusted from there as the user rotates the + mouse wheel. + + \sa orientation +*/ +qreal QQuickWheelHandler::rotation() const +{ + Q_D(const QQuickWheelHandler); + return d->rotation * d->rotationScale; +} + +void QQuickWheelHandler::setRotation(qreal rotation) +{ + Q_D(QQuickWheelHandler); + if (qFuzzyCompare(d->rotation, rotation / d->rotationScale)) + return; + + d->rotation = rotation / d->rotationScale; + emit rotationChanged(); +} + +/*! + \qmlproperty real QtQuick::WheelHandler::rotationScale + + The scaling to be applied to the \l rotation property, and to the + \l property on the \l target item, if any. The default is 1, such that + \l rotation will be in units of degrees of rotation. It can be set to a + negative number to invert the effect of the direction of mouse wheel + rotation. +*/ +qreal QQuickWheelHandler::rotationScale() const +{ + Q_D(const QQuickWheelHandler); + return d->rotationScale; +} + +void QQuickWheelHandler::setRotationScale(qreal rotationScale) +{ + Q_D(QQuickWheelHandler); + if (qFuzzyCompare(d->rotationScale, rotationScale)) + return; + if (qFuzzyIsNull(rotationScale)) { + qWarning("rotationScale cannot be set to zero"); + return; + } + + d->rotationScale = rotationScale; + emit rotationScaleChanged(); +} + +/*! + \qmlproperty string QtQuick::WheelHandler::property + + The property to be modified on the \l target when the mouse wheel is rotated. + + The default is no property (empty string). When no target property is being + automatically modified, you can use bindings to react to mouse wheel + rotation in arbitrary ways. + + You can use the mouse wheel to adjust any numeric property. For example if + \c property is set to \c x, the \l target will move horizontally as the + wheel is rotated. The following properties have special behavior: + + \value scale + \l{QQuickItem::scale}{scale} will be modified in a non-linear fashion + as described under \l targetScaleMultiplier. If + \l targetTransformAroundCursor is \c true, the \l{QQuickItem::x}{x} and + \l{QQuickItem::y}{y} properties will be simultaneously adjusted so that + the user will effectively zoom into or out of the point under the mouse + cursor. + \value rotation + \l{QQuickItem::rotation}{rotation} will be set to \l rotation. If + \l targetTransformAroundCursor is \c true, the l{QQuickItem::x}{x} and + \l{QQuickItem::y}{y} properties will be simultaneously adjusted so + that the user will effectively rotate the item around the point under + the mouse cursor. + + The adjustment of the given target property is always scaled by \l rotationScale. +*/ +QString QQuickWheelHandler::property() const +{ + Q_D(const QQuickWheelHandler); + return d->propertyName; +} + +void QQuickWheelHandler::setProperty(const QString &propertyName) +{ + Q_D(QQuickWheelHandler); + if (d->propertyName == propertyName) + return; + + d->propertyName = propertyName; + d->metaPropertyDirty = true; + emit propertyChanged(); +} + +/*! + \qmlproperty real QtQuick::WheelHandler::targetScaleMultiplier + + The amount by which the \l target \l{QQuickItem::scale}{scale} is to be + multiplied whenever the \l rotation changes by 15 degrees. This + is relevant only when \l property is \c "scale". + + The \c scale will be multiplied by + \c targetScaleMultiplier \sup {angleDelta * rotationScale / 15}. + The default is \c 2 \sup {1/3}, which means that if \l rotationScale is left + at its default value, and the mouse wheel is rotated by one "click" + (15 degrees), the \l target will be scaled by approximately 1.25; after + three "clicks" its size will be doubled or halved, depending on the + direction that the wheel is rotated. If you want to make it double or halve + with every 2 clicks of the wheel, set this to \c 2 \sup {1/2} (1.4142). + If you want to make it scale the opposite way as the wheel is rotated, + set \c rotationScale to a negative value. +*/ +qreal QQuickWheelHandler::targetScaleMultiplier() const +{ + Q_D(const QQuickWheelHandler); + return d->targetScaleMultiplier; +} + +void QQuickWheelHandler::setTargetScaleMultiplier(qreal targetScaleMultiplier) +{ + Q_D(QQuickWheelHandler); + if (qFuzzyCompare(d->targetScaleMultiplier, targetScaleMultiplier)) + return; + + d->targetScaleMultiplier = targetScaleMultiplier; + emit targetScaleMultiplierChanged(); +} + +/*! + \qmlproperty bool QtQuick::WheelHandler::targetTransformAroundCursor + + Whether the \l target should automatically be repositioned in such a way + that it is transformed around the mouse cursor position while the + \l property is adjusted. The default is \c true. + + If \l property is set to \c "rotation" and \l targetTransformAroundCursor + is \c true, then as the wheel is rotated, the \l target item will rotate in + place around the mouse cursor position. If \c targetTransformAroundCursor + is \c false, it will rotate around its + \l{QQuickItem::transformOrigin}{transformOrigin} instead. +*/ +bool QQuickWheelHandler::isTargetTransformAroundCursor() const +{ + Q_D(const QQuickWheelHandler); + return d->targetTransformAroundCursor; +} + +void QQuickWheelHandler::setTargetTransformAroundCursor(bool ttac) +{ + Q_D(QQuickWheelHandler); + if (d->targetTransformAroundCursor == ttac) + return; + + d->targetTransformAroundCursor = ttac; + emit targetTransformAroundCursorChanged(); +} + +bool QQuickWheelHandler::wantsPointerEvent(QQuickPointerEvent *event) +{ + if (!event) + return false; + QQuickPointerScrollEvent *scroll = event->asPointerScrollEvent(); + if (!scroll) + return false; + if (!acceptedDevices().testFlag(QQuickPointerDevice::DeviceType::TouchPad) + && scroll->synthSource() != Qt::MouseEventNotSynthesized) + return false; + if (!active()) { + switch (orientation()) { + case Qt::Horizontal: + if (qFuzzyIsNull(scroll->angleDelta().x()) && qFuzzyIsNull(scroll->pixelDelta().x())) + return false; + break; + case Qt::Vertical: + if (qFuzzyIsNull(scroll->angleDelta().y()) && qFuzzyIsNull(scroll->pixelDelta().y())) + return false; + break; + } + } + QQuickEventPoint *point = event->point(0); + if (QQuickPointerDeviceHandler::wantsPointerEvent(event) && wantsEventPoint(point) && parentContains(point)) { + setPointId(point->pointId()); + return true; + } + return false; +} + +void QQuickWheelHandler::handleEventPoint(QQuickEventPoint *point) +{ + Q_D(QQuickWheelHandler); + QQuickPointerScrollEvent *event = point->pointerEvent()->asPointerScrollEvent(); + setActive(true); // ScrollEnd will not happen unless it was already active (see setActive(false) below) + point->setAccepted(); + qreal inversion = !d->invertible && event->isInverted() ? -1 : 1; + qreal angleDelta = inversion * qreal(orientation() == Qt::Horizontal ? event->angleDelta().x() : + event->angleDelta().y()) / 8; + d->rotation += angleDelta; + emit rotationChanged(); + emit wheel(event); + if (!d->propertyName.isEmpty() && target()) { + QQuickItem *t = target(); + // writing target()'s property is done via QMetaProperty::write() so that any registered interceptors can react. + if (d->propertyName == QLatin1String("scale")) { + qreal multiplier = qPow(d->targetScaleMultiplier, angleDelta * d->rotationScale / 15); // wheel "clicks" + const QPointF centroidParentPos = t->parentItem()->mapFromScene(point->scenePosition()); + const QPointF positionWas = t->position(); + const qreal scaleWas = t->scale(); + const qreal activePropertyValue = scaleWas * multiplier; + qCDebug(lcWheelHandler) << objectName() << "angle delta" << event->angleDelta() << "pixel delta" << event->pixelDelta() + << "@" << point->position() << "in parent" << centroidParentPos + << "in scene" << point->scenePosition() + << "multiplier" << multiplier << "scale" << scaleWas + << "->" << activePropertyValue; + d->targetMetaProperty().write(t, activePropertyValue); + if (d->targetTransformAroundCursor) { + // If an interceptor intervened, scale may now be different than we asked for. Adjust accordingly. + multiplier = t->scale() / scaleWas; + const QPointF adjPos = QQuickItemPrivate::get(t)->adjustedPosForTransform( + centroidParentPos, positionWas, QVector2D(), scaleWas, multiplier, t->rotation(), 0); + qCDebug(lcWheelHandler) << "adjusting item pos" << adjPos << "in scene" << t->parentItem()->mapToScene(adjPos); + t->setPosition(adjPos); + } + } else if (d->propertyName == QLatin1String("rotation")) { + const QPointF positionWas = t->position(); + const qreal rotationWas = t->rotation(); + const qreal activePropertyValue = rotationWas + angleDelta * d->rotationScale; + const QPointF centroidParentPos = t->parentItem()->mapFromScene(point->scenePosition()); + qCDebug(lcWheelHandler) << objectName() << "angle delta" << event->angleDelta() << "pixel delta" << event->pixelDelta() + << "@" << point->position() << "in parent" << centroidParentPos + << "in scene" << point->scenePosition() << "rotation" << t->rotation() + << "->" << activePropertyValue; + d->targetMetaProperty().write(t, activePropertyValue); + if (d->targetTransformAroundCursor) { + // If an interceptor intervened, rotation may now be different than we asked for. Adjust accordingly. + const QPointF adjPos = QQuickItemPrivate::get(t)->adjustedPosForTransform( + centroidParentPos, positionWas, QVector2D(), + t->scale(), 1, rotationWas, t->rotation() - rotationWas); + qCDebug(lcWheelHandler) << "adjusting item pos" << adjPos << "in scene" << t->parentItem()->mapToScene(adjPos); + t->setPosition(adjPos); + } + } else { + qCDebug(lcWheelHandler) << objectName() << "angle delta" << event->angleDelta() << "scaled" << angleDelta << "total" << d->rotation << "pixel delta" << event->pixelDelta() + << "@" << point->position() << "in scene" << point->scenePosition() << "rotation" << t->rotation(); + qreal delta = 0; + if (event->hasPixelDelta()) { + delta = inversion * d->rotationScale * qreal(orientation() == Qt::Horizontal ? event->pixelDelta().x() : event->pixelDelta().y()); + qCDebug(lcWheelHandler) << "changing target" << d->propertyName << "by pixel delta" << delta << "from" << event; + } else { + delta = angleDelta * d->rotationScale; + qCDebug(lcWheelHandler) << "changing target" << d->propertyName << "by scaled angle delta" << delta << "from" << event; + } + bool ok = false; + qreal value = d->targetMetaProperty().read(t).toReal(&ok); + if (ok) + d->targetMetaProperty().write(t, value + qreal(delta)); + else + qWarning() << "failed to read property" << d->propertyName << "of" << t; + } + } + switch (event->phase()) { + case Qt::ScrollEnd: + qCDebug(lcWheelHandler) << objectName() << "deactivating due to ScrollEnd phase"; + setActive(false); + break; + case Qt::NoScrollPhase: + d->deactivationTimer.start(qRound(d->activeTimeout * 1000), this); + break; + case Qt::ScrollBegin: + case Qt::ScrollUpdate: + case Qt::ScrollMomentum: + break; + } +} + +void QQuickWheelHandler::onTargetChanged(QQuickItem *oldTarget) +{ + Q_UNUSED(oldTarget) + Q_D(QQuickWheelHandler); + d->metaPropertyDirty = true; +} + +void QQuickWheelHandler::onActiveChanged() +{ + Q_D(QQuickWheelHandler); + if (!active()) + d->deactivationTimer.stop(); +} + +void QQuickWheelHandler::timerEvent(QTimerEvent *event) +{ + Q_D(const QQuickWheelHandler); + if (event->timerId() == d->deactivationTimer.timerId()) { + qCDebug(lcWheelHandler) << objectName() << "deactivating due to timeout"; + setActive(false); + } +} + +QQuickWheelHandlerPrivate::QQuickWheelHandlerPrivate() + : QQuickSinglePointHandlerPrivate() +{ +} + +QMetaProperty &QQuickWheelHandlerPrivate::targetMetaProperty() const +{ + Q_Q(const QQuickWheelHandler); + if (metaPropertyDirty && q->target()) { + if (!propertyName.isEmpty()) { + const QMetaObject *targetMeta = q->target()->metaObject(); + metaProperty = targetMeta->property( + targetMeta->indexOfProperty(propertyName.toLocal8Bit().constData())); + } + metaPropertyDirty = false; + } + return metaProperty; +} + +QT_END_NAMESPACE diff --git a/src/quick/handlers/qquickwheelhandler_p.h b/src/quick/handlers/qquickwheelhandler_p.h new file mode 100644 index 0000000000..f8d1c00726 --- /dev/null +++ b/src/quick/handlers/qquickwheelhandler_p.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://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.LGPL3 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-3.0.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 (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKWHEELHANDLER_H +#define QQUICKWHEELHANDLER_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 "qquickitem.h" +#include "qevent.h" +#include "qquicksinglepointhandler_p.h" + +QT_BEGIN_NAMESPACE + +class QQuickWheelEvent; +class QQuickWheelHandlerPrivate; + +class Q_QUICK_PRIVATE_EXPORT QQuickWheelHandler : public QQuickSinglePointHandler +{ + Q_OBJECT + Q_PROPERTY(Qt::Orientation orientation READ orientation WRITE setOrientation NOTIFY orientationChanged) + Q_PROPERTY(bool invertible READ isInvertible WRITE setInvertible NOTIFY invertibleChanged) + Q_PROPERTY(qreal activeTimeout READ activeTimeout WRITE setActiveTimeout NOTIFY activeTimeoutChanged) + Q_PROPERTY(qreal rotation READ rotation WRITE setRotation NOTIFY rotationChanged) + Q_PROPERTY(qreal rotationScale READ rotationScale WRITE setRotationScale NOTIFY rotationScaleChanged) + Q_PROPERTY(QString property READ property WRITE setProperty NOTIFY propertyChanged) + Q_PROPERTY(qreal targetScaleMultiplier READ targetScaleMultiplier WRITE setTargetScaleMultiplier NOTIFY targetScaleMultiplierChanged) + Q_PROPERTY(bool targetTransformAroundCursor READ isTargetTransformAroundCursor WRITE setTargetTransformAroundCursor NOTIFY targetTransformAroundCursorChanged) + +public: + explicit QQuickWheelHandler(QQuickItem *parent = nullptr); + + Qt::Orientation orientation() const; + void setOrientation(Qt::Orientation orientation); + + bool isInvertible() const; + void setInvertible(bool invertible); + + qreal activeTimeout() const; + void setActiveTimeout(qreal timeout); + + qreal rotation() const; + void setRotation(qreal rotation); + + qreal rotationScale() const; + void setRotationScale(qreal rotationScale); + + QString property() const; + void setProperty(const QString &name); + + qreal targetScaleMultiplier() const; + void setTargetScaleMultiplier(qreal targetScaleMultiplier); + + bool isTargetTransformAroundCursor() const; + void setTargetTransformAroundCursor(bool ttac); + +Q_SIGNALS: + void wheel(QQuickPointerScrollEvent *event); + + void orientationChanged(); + void invertibleChanged(); + void activeTimeoutChanged(); + void rotationChanged(); + void rotationScaleChanged(); + void propertyChanged(); + void targetScaleMultiplierChanged(); + void targetTransformAroundCursorChanged(); + +protected: + bool wantsPointerEvent(QQuickPointerEvent *event) override; + void handleEventPoint(QQuickEventPoint *point) override; + void onTargetChanged(QQuickItem *oldTarget) override; + void onActiveChanged() override; + void timerEvent(QTimerEvent *event) override; + + Q_DECLARE_PRIVATE(QQuickWheelHandler) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickWheelHandler) + +#endif // QQUICKWHEELHANDLER_H diff --git a/src/quick/handlers/qquickwheelhandler_p_p.h b/src/quick/handlers/qquickwheelhandler_p_p.h new file mode 100644 index 0000000000..d35e04c51b --- /dev/null +++ b/src/quick/handlers/qquickwheelhandler_p_p.h @@ -0,0 +1,87 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://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.LGPL3 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-3.0.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 (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKWHEELHANDLER_P_P_H +#define QQUICKWHEELHANDLER_P_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 "qquicksinglepointhandler_p_p.h" +#include "qquickwheelhandler_p.h" +#include <QtCore/qbasictimer.h> + +QT_BEGIN_NAMESPACE + +class Q_QUICK_PRIVATE_EXPORT QQuickWheelHandlerPrivate : public QQuickSinglePointHandlerPrivate +{ + Q_DECLARE_PUBLIC(QQuickWheelHandler) + +public: + static QQuickWheelHandlerPrivate* get(QQuickWheelHandler *q) { return q->d_func(); } + static const QQuickWheelHandlerPrivate* get(const QQuickWheelHandler *q) { return q->d_func(); } + + QQuickWheelHandlerPrivate(); + + QMetaProperty &targetMetaProperty() const; + + QBasicTimer deactivationTimer; + qreal activeTimeout = 0.1; + qreal rotationScale = 1; + qreal rotation = 0; // in units of degrees + qreal targetScaleMultiplier = 1.25992104989487; // qPow(2, 1/3) + QString propertyName; + mutable QMetaProperty metaProperty; + Qt::Orientation orientation = Qt::Vertical; + mutable bool metaPropertyDirty = true; + bool invertible = true; + bool targetTransformAroundCursor = true; +}; + +QT_END_NAMESPACE + +#endif // QQUICKWHEELHANDLER_P_P_H diff --git a/src/quick/items/qquickevents.cpp b/src/quick/items/qquickevents.cpp index c43eab6b8a..28b217b7b3 100644 --- a/src/quick/items/qquickevents.cpp +++ b/src/quick/items/qquickevents.cpp @@ -1135,10 +1135,10 @@ void QQuickEventTouchPoint::reset(const QTouchEvent::TouchPoint &tp, ulong times struct PointVelocityData { QVector2D velocity; QPointF pos; - ulong timestamp; + ulong timestamp = 0; }; -typedef QMap<quint64, PointVelocityData*> PointDataForPointIdMap; +typedef QMap<quint64, PointVelocityData> PointDataForPointIdMap; Q_GLOBAL_STATIC(PointDataForPointIdMap, g_previousPointData) static const int PointVelocityAgeLimit = 500; // milliseconds @@ -1149,42 +1149,36 @@ static const int PointVelocityAgeLimit = 500; // milliseconds */ QVector2D QQuickEventPoint::estimatedVelocity() const { - PointVelocityData *prevPoint = g_previousPointData->value(m_pointId); - if (!prevPoint) { + auto prevPointIt = g_previousPointData->find(m_pointId); + auto end = g_previousPointData->end(); + if (prevPointIt == end) { // cleanup events older than PointVelocityAgeLimit - auto end = g_previousPointData->end(); for (auto it = g_previousPointData->begin(); it != end; ) { - PointVelocityData *data = it.value(); - if (m_timestamp - data->timestamp > PointVelocityAgeLimit) { + if (m_timestamp - it->timestamp > PointVelocityAgeLimit) it = g_previousPointData->erase(it); - delete data; - } else { + else ++it; - } } - // TODO optimize: stop this dynamic memory thrashing - prevPoint = new PointVelocityData; - prevPoint->velocity = QVector2D(); - prevPoint->timestamp = 0; - prevPoint->pos = QPointF(); - g_previousPointData->insert(m_pointId, prevPoint); + prevPointIt = g_previousPointData->insert(m_pointId, PointVelocityData()); } - const ulong timeElapsed = m_timestamp - prevPoint->timestamp; + + auto &prevPoint = prevPointIt.value(); + const ulong timeElapsed = m_timestamp - prevPoint.timestamp; if (timeElapsed == 0) // in case we call estimatedVelocity() twice on the same QQuickEventPoint return m_velocity; QVector2D newVelocity; - if (prevPoint->timestamp != 0) - newVelocity = QVector2D(m_scenePos - prevPoint->pos)/timeElapsed; + if (prevPoint.timestamp != 0) + newVelocity = QVector2D(m_scenePos - prevPoint.pos) / timeElapsed; // VERY simple kalman filter: does a weighted average // where the older velocities get less and less significant static const float KalmanGain = 0.7f; QVector2D filteredVelocity = newVelocity * KalmanGain + m_velocity * (1.0f - KalmanGain); - prevPoint->velocity = filteredVelocity; - prevPoint->pos = m_scenePos; - prevPoint->timestamp = m_timestamp; + prevPoint.velocity = filteredVelocity; + prevPoint.pos = m_scenePos; + prevPoint.timestamp = m_timestamp; return filteredVelocity; } diff --git a/src/quick/items/qquickevents_p_p.h b/src/quick/items/qquickevents_p_p.h index e614b1bd6d..1a3737091f 100644 --- a/src/quick/items/qquickevents_p_p.h +++ b/src/quick/items/qquickevents_p_p.h @@ -605,6 +605,7 @@ private: bool m_inverted = false; friend class QQuickWindowPrivate; + friend class QQuickWheelHandler; Q_DISABLE_COPY(QQuickPointerScrollEvent) }; diff --git a/src/quick/items/qquickitemsmodule.cpp b/src/quick/items/qquickitemsmodule.cpp index cd5cca5ac9..2a1c442653 100644 --- a/src/quick/items/qquickitemsmodule.cpp +++ b/src/quick/items/qquickitemsmodule.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtQuick module of the Qt Toolkit. @@ -120,6 +120,7 @@ #include "handlers/qquickpinchhandler_p.h" #include "handlers/qquickpointhandler_p.h" #include "handlers/qquicktaphandler_p.h" +#include "handlers/qquickwheelhandler_p.h" QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(lcTransient) @@ -475,6 +476,12 @@ static void qt_quickitems_defineModule(const char *uri, int major, int minor) qmlRegisterUncreatableType<QQuickItemView, 13>(uri, 2, 13, itemViewName, itemViewMessage); qmlRegisterType<QQuickPathView, 13>(uri, 2, 13, "PathView"); qmlRegisterType<QQuickGridView, 13>(uri, 2, 13, "GridView"); +#if QT_CONFIG(quick_tableview) + qmlRegisterType<QQuickTableView, 14>(uri, 2, 14, "TableView"); +#endif +#if QT_CONFIG(wheelevent) + qmlRegisterType<QQuickWheelHandler>(uri, 2, 14, "WheelHandler"); +#endif } static void initResources() diff --git a/src/quick/items/qquickitemview_p_p.h b/src/quick/items/qquickitemview_p_p.h index ea5b5df9c6..0f1594f904 100644 --- a/src/quick/items/qquickitemview_p_p.h +++ b/src/quick/items/qquickitemview_p_p.h @@ -59,9 +59,9 @@ QT_REQUIRE_CONFIG(quick_itemview); #include "qquickitemviewfxitem_p_p.h" #include "qquickitemviewtransition_p.h" #include "qquickflickable_p_p.h" -#include <QtQml/private/qqmlobjectmodel_p.h> -#include <QtQml/private/qqmldelegatemodel_p.h> -#include <QtQml/private/qqmlchangeset_p.h> +#include <QtQmlModels/private/qqmlobjectmodel_p.h> +#include <QtQmlModels/private/qqmldelegatemodel_p.h> +#include <QtQmlModels/private/qqmlchangeset_p.h> QT_BEGIN_NAMESPACE diff --git a/src/quick/items/qquickrepeater.cpp b/src/quick/items/qquickrepeater.cpp index 805b6fe190..c8a03aff33 100644 --- a/src/quick/items/qquickrepeater.cpp +++ b/src/quick/items/qquickrepeater.cpp @@ -41,7 +41,6 @@ #include "qquickrepeater_p_p.h" #include <private/qqmlglobal_p.h> -#include <private/qqmllistaccessor_p.h> #include <private/qqmlchangeset_p.h> #include <private/qqmldelegatemodel_p.h> diff --git a/src/quick/items/qquicktableview.cpp b/src/quick/items/qquicktableview.cpp index 1dca5bea4a..8f5130fc17 100644 --- a/src/quick/items/qquicktableview.cpp +++ b/src/quick/items/qquicktableview.cpp @@ -42,10 +42,10 @@ #include <QtCore/qtimer.h> #include <QtCore/qdir.h> -#include <QtQml/private/qqmldelegatemodel_p.h> -#include <QtQml/private/qqmldelegatemodel_p_p.h> +#include <QtQmlModels/private/qqmldelegatemodel_p.h> +#include <QtQmlModels/private/qqmldelegatemodel_p_p.h> #include <QtQml/private/qqmlincubator_p.h> -#include <QtQml/private/qqmlchangeset_p.h> +#include <QtQmlModels/private/qqmlchangeset_p.h> #include <QtQml/qqmlinfo.h> #include <QtQuick/private/qquickflickable_p_p.h> @@ -76,14 +76,23 @@ \section1 Example Usage + \section2 C++ Models + The following example shows how to create a model from C++ with multiple columns: - \snippet qml/tableview/tablemodel.cpp 0 + \snippet qml/tableview/cpp-tablemodel.cpp 0 And then how to use it from QML: - \snippet qml/tableview/tablemodel.qml 0 + \snippet qml/tableview/cpp-tablemodel.qml 0 + + \section2 QML Models + + For prototyping and displaying very simple data (from a web API, for + example), \l TableModel can be used: + + \snippet qml/tableview/qml-tablemodel.qml 0 \section1 Reusing items @@ -606,6 +615,11 @@ void QQuickTableViewPrivate::updateContentWidth() { Q_Q(QQuickTableView); + if (syncHorizontally) { + q->QQuickFlickable::setContentWidth(syncView->contentWidth()); + return; + } + if (explicitContentWidth.isValid()) { // Don't calculate contentWidth when it // was set explicitly by the application. @@ -625,6 +639,11 @@ void QQuickTableViewPrivate::updateContentHeight() { Q_Q(QQuickTableView); + if (syncVertically) { + q->QQuickFlickable::setContentHeight(syncView->contentHeight()); + return; + } + if (explicitContentHeight.isValid()) { // Don't calculate contentHeight when it // was set explicitly by the application. @@ -1048,6 +1067,11 @@ qreal QQuickTableViewPrivate::getColumnLayoutWidth(int column) if (explicitColumnWidth >= 0) return explicitColumnWidth; + if (syncHorizontally) { + if (syncView->d_func()->loadedColumns.contains(column)) + return syncView->d_func()->getColumnLayoutWidth(column); + } + // Iterate over the currently visible items in the column. The downside // of doing that, is that the column width will then only be based on the implicit // width of the currently loaded items (which can be different depending on which @@ -1077,6 +1101,11 @@ qreal QQuickTableViewPrivate::getRowLayoutHeight(int row) if (explicitRowHeight >= 0) return explicitRowHeight; + if (syncVertically) { + if (syncView->d_func()->loadedRows.contains(row)) + return syncView->d_func()->getRowLayoutHeight(row); + } + // Iterate over the currently visible items in the row. The downside // of doing that, is that the row height will then only be based on the implicit // height of the currently loaded items (which can be different depending on which @@ -1106,6 +1135,9 @@ qreal QQuickTableViewPrivate::getColumnWidth(int column) if (cachedColumnWidth.startIndex == column) return cachedColumnWidth.size; + if (syncHorizontally) + return syncView->d_func()->getColumnWidth(column); + if (columnWidthProvider.isUndefined()) return noExplicitColumnWidth; @@ -1140,6 +1172,9 @@ qreal QQuickTableViewPrivate::getRowHeight(int row) if (cachedRowHeight.startIndex == row) return cachedRowHeight.size; + if (syncVertically) + return syncView->d_func()->getRowHeight(row); + if (rowHeightProvider.isUndefined()) return noExplicitRowHeight; @@ -1489,48 +1524,103 @@ bool QQuickTableViewPrivate::moveToNextRebuildState() return true; } -QPoint QQuickTableViewPrivate::calculateNewTopLeft() -{ - const int firstVisibleLeft = nextVisibleEdgeIndex(Qt::RightEdge, 0); - const int firstVisibleTop = nextVisibleEdgeIndex(Qt::BottomEdge, 0); - - return QPoint(firstVisibleLeft, firstVisibleTop); -} - -void QQuickTableViewPrivate::calculateTopLeft(QPoint &topLeft, QPointF &topLeftPos) +void QQuickTableViewPrivate::calculateTopLeft(QPoint &topLeftCell, QPointF &topLeftPos) { if (tableSize.isEmpty()) { - releaseLoadedItems(QQmlTableInstanceModel::NotReusable); - topLeft = QPoint(kEdgeIndexAtEnd, kEdgeIndexAtEnd); + // There is no cell that can be top left + topLeftCell.rx() = kEdgeIndexAtEnd; + topLeftCell.ry() = kEdgeIndexAtEnd; return; } - if (rebuildOptions & RebuildOption::All) { - qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::All"; - releaseLoadedItems(QQmlTableInstanceModel::NotReusable); - topLeft = calculateNewTopLeft(); - } else if (rebuildOptions & RebuildOption::ViewportOnly) { - qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::ViewportOnly"; - releaseLoadedItems(reusableFlag); + if (syncHorizontally || syncVertically) { + const auto syncView_d = syncView->d_func(); - if (rebuildOptions & RebuildOption::CalculateNewTopLeftRow) { - const int newRow = int(viewportRect.y() / (averageEdgeSize.height() + cellSpacing.height())); - topLeft.ry() = qBound(0, newRow, tableSize.height() - 1); - topLeftPos.ry() = topLeft.y() * (averageEdgeSize.height() + cellSpacing.height()); - } else { - topLeft.ry() = qBound(0, topRow(), tableSize.height() - 1); - topLeftPos.ry() = loadedTableOuterRect.topLeft().y(); + if (syncView_d->loadedItems.isEmpty()) { + // The sync view contains no loaded items. This probably means + // that it has not been rebuilt yet. Which also means that + // we cannot rebuild anything before this happens. + topLeftCell.rx() = kEdgeIndexNotSet; + topLeftCell.ry() = kEdgeIndexNotSet; + return; + } + + // Get sync view top left, and use that as our own top left (if possible) + const QPoint syncViewTopLeftCell(syncView_d->leftColumn(), syncView_d->topRow()); + const auto syncViewTopLeftFxItem = syncView_d->loadedTableItem(syncViewTopLeftCell); + const QPointF syncViewTopLeftPos = syncViewTopLeftFxItem->geometry().topLeft(); + + if (syncHorizontally) { + topLeftCell.rx() = syncViewTopLeftCell.x(); + topLeftPos.rx() = syncViewTopLeftPos.x(); + + if (topLeftCell.x() >= tableSize.width()) { + // Top left is outside our own model. + topLeftCell.rx() = kEdgeIndexAtEnd; + topLeftPos.rx() = kEdgeIndexAtEnd; + } } - if (rebuildOptions & RebuildOption::CalculateNewTopLeftColumn) { + + if (syncVertically) { + topLeftCell.ry() = syncViewTopLeftCell.y(); + topLeftPos.ry() = syncViewTopLeftPos.y(); + + if (topLeftCell.y() >= tableSize.height()) { + // Top left is outside our own model. + topLeftCell.ry() = kEdgeIndexAtEnd; + topLeftPos.ry() = kEdgeIndexAtEnd; + } + } + + if (syncHorizontally && syncVertically) { + // We have a valid top left, so we're done + return; + } + } + + // Since we're not sync-ing both horizontal and vertical, calculate the missing + // dimention(s) ourself. If we rebuild all, we find the first visible top-left + // item starting from cell(0, 0). Otherwise, guesstimate which row or column that + // should be the new top-left given the geometry of the viewport. + + if (!syncHorizontally) { + if (rebuildOptions & RebuildOption::All) { + // Find the first visible column from the beginning + topLeftCell.rx() = nextVisibleEdgeIndex(Qt::RightEdge, 0); + if (topLeftCell.x() == kEdgeIndexAtEnd) { + // No visible column found + return; + } + } else if (rebuildOptions & RebuildOption::CalculateNewTopLeftColumn) { + // Guesstimate new top left const int newColumn = int(viewportRect.x() / (averageEdgeSize.width() + cellSpacing.width())); - topLeft.rx() = qBound(0, newColumn, tableSize.width() - 1); - topLeftPos.rx() = topLeft.x() * (averageEdgeSize.width() + cellSpacing.width()); + topLeftCell.rx() = qBound(0, newColumn, tableSize.width() - 1); + topLeftPos.rx() = topLeftCell.x() * (averageEdgeSize.width() + cellSpacing.width()); } else { - topLeft.rx() = qBound(0, leftColumn(), tableSize.width() - 1); + // Keep the current top left, unless it's outside model + topLeftCell.rx() = qBound(0, leftColumn(), tableSize.width() - 1); topLeftPos.rx() = loadedTableOuterRect.topLeft().x(); } - } else { - Q_TABLEVIEW_UNREACHABLE(rebuildOptions); + } + + if (!syncVertically) { + if (rebuildOptions & RebuildOption::All) { + // Find the first visible row from the beginning + topLeftCell.ry() = nextVisibleEdgeIndex(Qt::BottomEdge, 0); + if (topLeftCell.y() == kEdgeIndexAtEnd) { + // No visible row found + return; + } + } else if (rebuildOptions & RebuildOption::CalculateNewTopLeftRow) { + // Guesstimate new top left + const int newRow = int(viewportRect.y() / (averageEdgeSize.height() + cellSpacing.height())); + topLeftCell.ry() = qBound(0, newRow, tableSize.height() - 1); + topLeftPos.ry() = topLeftCell.y() * (averageEdgeSize.height() + cellSpacing.height()); + } else { + // Keep the current top left, unless it's outside model + topLeftCell.ry() = qBound(0, topRow(), tableSize.height() - 1); + topLeftPos.ry() = loadedTableOuterRect.topLeft().y(); + } } } @@ -1542,12 +1632,22 @@ void QQuickTableViewPrivate::beginRebuildTable() QPointF topLeftPos; calculateTopLeft(topLeft, topLeftPos); + if (rebuildOptions & RebuildOption::All) + releaseLoadedItems(QQmlTableInstanceModel::NotReusable); + else if (rebuildOptions & RebuildOption::ViewportOnly) + releaseLoadedItems(reusableFlag); + loadedColumns.clear(); loadedRows.clear(); loadedTableOuterRect = QRect(); loadedTableInnerRect = QRect(); clearEdgeSizeCache(); + if (syncHorizontally) + setLocalViewportX(syncView->contentX()); + if (syncVertically) + setLocalViewportY(syncView->contentY()); + if (!model) { qCDebug(lcTableViewDelegateLifecycle()) << "no model found, leaving table empty"; return; @@ -1564,7 +1664,12 @@ void QQuickTableViewPrivate::beginRebuildTable() } if (topLeft.x() == kEdgeIndexAtEnd || topLeft.y() == kEdgeIndexAtEnd) { - qCDebug(lcTableViewDelegateLifecycle()) << "no visible rows or columns, leaving table empty"; + qCDebug(lcTableViewDelegateLifecycle()) << "no visible row or column found, leaving table empty"; + return; + } + + if (topLeft.x() == kEdgeIndexNotSet || topLeft.y() == kEdgeIndexNotSet) { + qCDebug(lcTableViewDelegateLifecycle()) << "could not resolve top-left item, leaving table empty"; return; } @@ -1733,11 +1838,59 @@ void QQuickTableViewPrivate::scheduleRebuildTable(RebuildOptions options) { q_func()->polish(); } +QQuickTableView *QQuickTableViewPrivate::rootSyncView() const +{ + QQuickTableView *root = const_cast<QQuickTableView *>(q_func()); + while (QQuickTableView *view = root->d_func()->syncView) + root = view; + return root; +} + void QQuickTableViewPrivate::updatePolish() { + // We always start updating from the top of the syncView tree, since + // the layout of a syncView child will depend on the layout of the syncView. + // E.g when a new column is flicked in, the syncView should load and layout + // the column first, before any syncChildren gets a chance to do the same. + Q_TABLEVIEW_ASSERT(!polishing, "recursive updatePolish() calls are not allowed!"); + rootSyncView()->d_func()->updateTableRecursive(); +} + +bool QQuickTableViewPrivate::updateTableRecursive() +{ + if (polishing) { + // We're already updating the Table in this view, so + // we cannot continue. Signal this back by returning false. + // The caller can then choose to call "polish()" instead, to + // do the update later. + return false; + } + + const bool updateComplete = updateTable(); + if (!updateComplete) + return false; + + for (auto syncChild : qAsConst(syncChildren)) { + auto syncChild_d = syncChild->d_func(); + syncChild_d->scheduledRebuildOptions |= rebuildOptions; + + const bool descendantUpdateComplete = syncChild_d->updateTableRecursive(); + if (!descendantUpdateComplete) + return false; + } + + rebuildOptions = RebuildOption::None; + + return true; +} + +bool QQuickTableViewPrivate::updateTable() +{ // Whenever something changes, e.g viewport moves, spacing is set to a // new value, model changes etc, this function will end up being called. Here // we check what needs to be done, and load/unload cells accordingly. + // If we cannot complete the update (because we need to wait for an item + // to load async), we return false. Q_TABLEVIEW_ASSERT(!polishing, "recursive updatePolish() calls are not allowed!"); QBoolBlocker polishGuard(polishing, true); @@ -1747,25 +1900,27 @@ void QQuickTableViewPrivate::updatePolish() // as an atomic operation, which means that we don't continue doing anything else until all // items have been received and laid out. Note that updatePolish is then called once more // after the loadRequest has completed to handle anything that might have occurred in-between. - return; + return false; } if (rebuildState != RebuildState::Done) { processRebuildTable(); - return; + return rebuildState == RebuildState::Done; } syncWithPendingChanges(); if (rebuildState == RebuildState::Begin) { processRebuildTable(); - return; + return rebuildState == RebuildState::Done; } if (loadedItems.isEmpty()) - return; + return !loadRequest.isActive(); loadAndUnloadVisibleEdges(); + + return !loadRequest.isActive(); } void QQuickTableViewPrivate::fixup(QQuickFlickablePrivate::AxisData &data, qreal minExtent, qreal maxExtent) @@ -1855,9 +2010,14 @@ void QQuickTableViewPrivate::syncWithPendingChanges() // such assignments into effect until we're in a state that allows it. Q_Q(QQuickTableView); viewportRect = QRectF(q->contentX(), q->contentY(), q->width(), q->height()); + + // Sync rebuild options first, in case we schedule a rebuild from one of the + // other sync calls above. If so, we need to start a new rebuild from the top. syncRebuildOptions(); + syncModel(); syncDelegate(); + syncSyncView(); } void QQuickTableViewPrivate::syncRebuildOptions() @@ -1922,6 +2082,46 @@ void QQuickTableViewPrivate::syncModel() connectToModel(); } +void QQuickTableViewPrivate::syncSyncView() +{ + Q_Q(QQuickTableView); + + if (assignedSyncView != syncView) { + if (syncView) + syncView->d_func()->syncChildren.removeOne(q); + + if (assignedSyncView) { + QQuickTableView *view = assignedSyncView; + + while (view) { + if (view == q) { + if (!layoutWarningIssued) { + layoutWarningIssued = true; + qmlWarning(q) << "TableView: recursive syncView connection detected!"; + } + syncView = nullptr; + return; + } + view = view->d_func()->syncView; + } + + assignedSyncView->d_func()->syncChildren.append(q); + scheduledRebuildOptions |= RebuildOption::ViewportOnly; + q->polish(); + } + + syncView = assignedSyncView; + } + + syncHorizontally = syncView && assignedSyncDirection & Qt::Horizontal; + syncVertically = syncView && assignedSyncDirection & Qt::Vertical; + + if (syncHorizontally) + q->setColumnSpacing(syncView->columnSpacing()); + if (syncVertically) + q->setRowSpacing(syncView->rowSpacing()); +} + void QQuickTableViewPrivate::connectToModel() { Q_TABLEVIEW_ASSERT(model, ""); @@ -2051,6 +2251,84 @@ void QQuickTableViewPrivate::modelResetCallback() scheduleRebuildTable(RebuildOption::All); } +void QQuickTableViewPrivate::scheduleRebuildIfFastFlick() +{ + Q_Q(QQuickTableView); + + // If the viewport has moved more than one page vertically or horizontally, we switch + // strategy from refilling edges around the current table to instead rebuild the table + // from scratch inside the new viewport. This will greatly improve performance when flicking + // a long distance in one go, which can easily happen when dragging on scrollbars. + + // Check the viewport moved more than one page vertically + if (!viewportRect.intersects(QRectF(viewportRect.x(), q->contentY(), 1, q->height()))) { + scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftRow; + scheduledRebuildOptions |= RebuildOption::ViewportOnly; + } + + // Check the viewport moved more than one page horizontally + if (!viewportRect.intersects(QRectF(q->contentX(), viewportRect.y(), q->width(), 1))) { + scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftColumn; + scheduledRebuildOptions |= RebuildOption::ViewportOnly; + } +} + +void QQuickTableViewPrivate::setLocalViewportX(qreal contentX) +{ + // Set the new viewport position if changed, but don't trigger any + // rebuilds or updates. We use this function internally to distinguish + // external flicking from internal sync-ing of the content view. + Q_Q(QQuickTableView); + QBoolBlocker blocker(inSetLocalViewportPos, true); + + if (qFuzzyCompare(contentX, q->contentX())) + return; + + q->setContentX(contentX); +} + +void QQuickTableViewPrivate::setLocalViewportY(qreal contentY) +{ + // Set the new viewport position if changed, but don't trigger any + // rebuilds or updates. We use this function internally to distinguish + // external flicking from internal sync-ing of the content view. + Q_Q(QQuickTableView); + QBoolBlocker blocker(inSetLocalViewportPos, true); + + if (qFuzzyCompare(contentY, q->contentY())) + return; + + q->setContentY(contentY); +} + +void QQuickTableViewPrivate::syncViewportPosRecursive() +{ + Q_Q(QQuickTableView); + QBoolBlocker recursionGuard(inSyncViewportPosRecursive, true); + + if (syncView) { + auto syncView_d = syncView->d_func(); + if (!syncView_d->inSyncViewportPosRecursive) { + if (syncHorizontally) + syncView_d->setLocalViewportX(q->contentX()); + if (syncVertically) + syncView_d->setLocalViewportY(q->contentY()); + syncView_d->syncViewportPosRecursive(); + } + } + + for (auto syncChild : qAsConst(syncChildren)) { + auto syncChild_d = syncChild->d_func(); + if (!syncChild_d->inSyncViewportPosRecursive) { + if (syncChild_d->syncHorizontally) + syncChild_d->setLocalViewportX(q->contentX()); + if (syncChild_d->syncVertically) + syncChild_d->setLocalViewportY(q->contentY()); + syncChild_d->syncViewportPosRecursive(); + } + } +} + QQuickTableView::QQuickTableView(QQuickItem *parent) : QQuickFlickable(*(new QQuickTableViewPrivate), parent) { @@ -2214,6 +2492,41 @@ void QQuickTableView::setContentHeight(qreal height) QQuickFlickable::setContentHeight(height); } +QQuickTableView *QQuickTableView::syncView() const +{ + return d_func()->assignedSyncView; +} + +void QQuickTableView::setSyncView(QQuickTableView *view) +{ + Q_D(QQuickTableView); + if (d->assignedSyncView == view) + return; + + d->assignedSyncView = view; + d->scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::ViewportOnly); + + emit syncViewChanged(); +} + +Qt::Orientations QQuickTableView::syncDirection() const +{ + return d_func()->assignedSyncDirection; +} + +void QQuickTableView::setSyncDirection(Qt::Orientations direction) +{ + Q_D(QQuickTableView); + if (d->assignedSyncDirection == direction) + return; + + d->assignedSyncDirection = direction; + if (d->assignedSyncView) + d->scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::ViewportOnly); + + emit syncDirectionChanged(); +} + void QQuickTableView::forceLayout() { d_func()->forceLayout(); @@ -2241,45 +2554,42 @@ void QQuickTableView::geometryChanged(const QRectF &newGeometry, const QRectF &o void QQuickTableView::viewportMoved(Qt::Orientations orientation) { Q_D(QQuickTableView); + + // If the new viewport position was set from the setLocalViewportXY() + // functions, we just update the position silently and return. Otherwise, if + // the viewport was flicked by the user, or some other control, we + // recursively sync all the views in the hierarchy to the same position. QQuickFlickable::viewportMoved(orientation); + if (d->inSetLocalViewportPos) + return; - QQuickTableViewPrivate::RebuildOptions options = QQuickTableViewPrivate::RebuildOption::None; + // Move all views in the syncView hierarchy to the same contentX/Y. + // We need to start from this view (and not the root syncView) to + // ensure that we respect all the individual syncDirection flags + // between the individual views in the hierarchy. + d->syncViewportPosRecursive(); - // Check the viewport moved more than one page vertically - if (!d->viewportRect.intersects(QRectF(d->viewportRect.x(), contentY(), 1, height()))) - options |= QQuickTableViewPrivate::RebuildOption::CalculateNewTopLeftRow; - // Check the viewport moved more than one page horizontally - if (!d->viewportRect.intersects(QRectF(contentX(), d->viewportRect.y(), width(), 1))) - options |= QQuickTableViewPrivate::RebuildOption::CalculateNewTopLeftColumn; - - if (options) { - // When the viewport has moved more than one page vertically or horizontally, we switch - // strategy from refilling edges around the current table to instead rebuild the table - // from scratch inside the new viewport. This will greatly improve performance when flicking - // a long distance in one go, which can easily happen when dragging on scrollbars. - options |= QQuickTableViewPrivate::RebuildOption::ViewportOnly; - d->scheduleRebuildTable(options); - } - - if (d->scheduledRebuildOptions) { - // No reason to do anything, since we're about to rebuild the whole table anyway. - // Besides, calling updatePolish, which will start the rebuild, can easily cause - // binding loops to happen since we usually end up modifying the geometry of the - // viewport (contentItem) as well. - return; - } + auto rootView = d->rootSyncView(); + auto rootView_d = rootView->d_func(); - // Calling polish() will schedule a polish event. But while the user is flicking, several - // mouse events will be handled before we get an updatePolish() call. And the updatePolish() - // call will only see the last mouse position. This results in a stuttering flick experience - // (especially on windows). We improve on this by calling updatePolish() directly. But this - // has the pitfall that we open up for recursive callbacks. E.g while inside updatePolish(), we - // load/unload items, and emit signals. The application can listen to those signals and set a - // new contentX/Y on the flickable. So we need to guard for this, to avoid unexpected behavior. - if (d->polishing) - polish(); - else - d->updatePolish(); + rootView_d->scheduleRebuildIfFastFlick(); + + if (!rootView_d->polishScheduled) { + if (rootView_d->scheduledRebuildOptions) { + // When we need to rebuild, collecting several viewport + // moves and do a single polish gives a quicker UI. + rootView->polish(); + } else { + // Updating the table right away when flicking + // slowly gives a smoother experience. + const bool updated = rootView->d_func()->updateTableRecursive(); + if (!updated) { + // One, or more, of the views are already in an + // update, so we need to wait a cycle. + rootView->polish(); + } + } + } } void QQuickTableViewPrivate::_q_componentFinalized() @@ -2315,6 +2625,71 @@ void QQuickTableView::componentComplete() d_func()->registerCallbackWhenBindingsAreEvaluated(); } +class QObjectPrivate; +class QQuickTableSectionSizeProviderPrivate : public QObjectPrivate { +public: + QQuickTableSectionSizeProviderPrivate(); + ~QQuickTableSectionSizeProviderPrivate(); + QHash<int, qreal> hash; +}; + +QQuickTableSectionSizeProvider::QQuickTableSectionSizeProvider(QObject *parent) + : QObject (*(new QQuickTableSectionSizeProviderPrivate), parent) +{ +} + +void QQuickTableSectionSizeProvider::setSize(int section, qreal size) +{ + Q_D(QQuickTableSectionSizeProvider); + if (section < 0 || size < 0) { + qmlWarning(this) << "setSize: section or size less than zero"; + return; + } + if (qFuzzyCompare(QQuickTableSectionSizeProvider::size(section), size)) + return; + d->hash.insert(section, size); + emit sizeChanged(); +} + +// return -1.0 if no valid explicit size retrieved +qreal QQuickTableSectionSizeProvider::size(int section) +{ + Q_D(QQuickTableSectionSizeProvider); + auto it = d->hash.find(section); + if (it != d->hash.end()) + return *it; + return -1.0; +} + +// return true if section is valid +bool QQuickTableSectionSizeProvider::resetSize(int section) +{ + Q_D(QQuickTableSectionSizeProvider); + if (d->hash.empty()) + return false; + + auto ret = d->hash.remove(section); + if (ret) + emit sizeChanged(); + return ret; +} + +void QQuickTableSectionSizeProvider::resetAll() +{ + Q_D(QQuickTableSectionSizeProvider); + d->hash.clear(); + emit sizeChanged(); +} + +QQuickTableSectionSizeProviderPrivate::QQuickTableSectionSizeProviderPrivate() + : QObjectPrivate() +{ +} + +QQuickTableSectionSizeProviderPrivate::~QQuickTableSectionSizeProviderPrivate() +{ + +} #include "moc_qquicktableview_p.cpp" QT_END_NAMESPACE diff --git a/src/quick/items/qquicktableview_p.h b/src/quick/items/qquicktableview_p.h index f32c71b983..3d46221574 100644 --- a/src/quick/items/qquicktableview_p.h +++ b/src/quick/items/qquicktableview_p.h @@ -79,6 +79,8 @@ class Q_QUICK_PRIVATE_EXPORT QQuickTableView : public QQuickFlickable Q_PROPERTY(bool reuseItems READ reuseItems WRITE setReuseItems NOTIFY reuseItemsChanged) Q_PROPERTY(qreal contentWidth READ contentWidth WRITE setContentWidth NOTIFY contentWidthChanged) Q_PROPERTY(qreal contentHeight READ contentHeight WRITE setContentHeight NOTIFY contentHeightChanged) + Q_PROPERTY(QQuickTableView *syncView READ syncView WRITE setSyncView NOTIFY syncViewChanged REVISION 14) + Q_PROPERTY(Qt::Orientations syncDirection READ syncDirection WRITE setSyncDirection NOTIFY syncDirectionChanged REVISION 14) public: QQuickTableView(QQuickItem *parent = nullptr); @@ -110,6 +112,12 @@ public: void setContentWidth(qreal width); void setContentHeight(qreal height); + QQuickTableView *syncView() const; + void setSyncView(QQuickTableView *view); + + Qt::Orientations syncDirection() const; + void setSyncDirection(Qt::Orientations direction); + Q_INVOKABLE void forceLayout(); static QQuickTableViewAttached *qmlAttachedProperties(QObject *); @@ -124,6 +132,8 @@ Q_SIGNALS: void modelChanged(); void delegateChanged(); void reuseItemsChanged(); + Q_REVISION(14) void syncViewChanged(); + Q_REVISION(14) void syncDirectionChanged(); protected: void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; diff --git a/src/quick/items/qquicktableview_p_p.h b/src/quick/items/qquicktableview_p_p.h index 6bbfd4e2d6..7f2aee9105 100644 --- a/src/quick/items/qquicktableview_p_p.h +++ b/src/quick/items/qquicktableview_p_p.h @@ -54,9 +54,9 @@ #include "qquicktableview_p.h" #include <QtCore/qtimer.h> -#include <QtQml/private/qqmltableinstancemodel_p.h> +#include <QtQmlModels/private/qqmltableinstancemodel_p.h> #include <QtQml/private/qqmlincubator_p.h> -#include <QtQml/private/qqmlchangeset_p.h> +#include <QtQmlModels/private/qqmlchangeset_p.h> #include <QtQml/qqmlinfo.h> #include <QtQuick/private/qquickflickable_p_p.h> @@ -70,6 +70,25 @@ static const qreal kDefaultRowHeight = 50; static const qreal kDefaultColumnWidth = 50; class FxTableItem; +class QQuickTableSectionSizeProviderPrivate; + +class Q_QUICK_PRIVATE_EXPORT QQuickTableSectionSizeProvider : public QObject { + Q_OBJECT + +public: + QQuickTableSectionSizeProvider(QObject *parent=nullptr); + void setSize(int section, qreal size); + qreal size(int section); + bool resetSize(int section); + void resetAll(); + +Q_SIGNALS: + void sizeChanged(); + +private: + Q_DISABLE_COPY(QQuickTableSectionSizeProvider) + Q_DECLARE_PRIVATE(QQuickTableSectionSizeProvider) +}; class Q_QML_AUTOTEST_EXPORT QQuickTableViewPrivate : public QQuickFlickablePrivate { @@ -249,9 +268,15 @@ public: bool blockItemCreatedCallback = false; bool layoutWarningIssued = false; bool polishing = false; + bool syncVertically = false; + bool syncHorizontally = false; + bool inSetLocalViewportPos = false; + bool inSyncViewportPosRecursive = false; QJSValue rowHeightProvider; QJSValue columnWidthProvider; + QQuickTableSectionSizeProvider rowHeights; + QQuickTableSectionSizeProvider columnWidths; EdgeRange cachedNextVisibleEdgeIndex[4]; EdgeRange cachedColumnWidth; @@ -271,6 +296,11 @@ public: QSizeF averageEdgeSize; + QPointer<QQuickTableView> assignedSyncView; + QPointer<QQuickTableView> syncView; + QList<QPointer<QQuickTableView> > syncChildren; + Qt::Orientations assignedSyncDirection = Qt::Horizontal | Qt::Vertical; + const static QPoint kLeft; const static QPoint kRight; const static QPoint kUp; @@ -303,6 +333,10 @@ public: inline int leftColumn() const { return loadedColumns.firstKey(); } inline int rightColumn() const { return loadedColumns.lastKey(); } + QQuickTableView *rootSyncView() const; + + bool updateTableRecursive(); + bool updateTable(); void relayoutTable(); void relayoutTableItems(); @@ -349,7 +383,6 @@ public: void processRebuildTable(); bool moveToNextRebuildState(); - QPoint calculateNewTopLeft(); void calculateTopLeft(QPoint &topLeft, QPointF &topLeftPos); void beginRebuildTable(); void layoutAfterLoadingInitialTable(); @@ -369,6 +402,7 @@ public: inline void syncDelegate(); inline void syncModel(); inline void syncRebuildOptions(); + inline void syncSyncView(); void connectToModel(); void disconnectFromModel(); @@ -382,6 +416,11 @@ public: void layoutChangedCallback(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint); void modelResetCallback(); + void scheduleRebuildIfFastFlick(); + void setLocalViewportX(qreal contentX); + void setLocalViewportY(qreal contentY); + void syncViewportPosRecursive(); + void _q_componentFinalized(); void registerCallbackWhenBindingsAreEvaluated(); diff --git a/src/quick/items/qquickwindow.cpp b/src/quick/items/qquickwindow.cpp index 9d0f4a6893..f3a2b07620 100644 --- a/src/quick/items/qquickwindow.cpp +++ b/src/quick/items/qquickwindow.cpp @@ -2551,7 +2551,7 @@ bool QQuickWindowPrivate::deliverPressOrReleaseEvent(QQuickPointerEvent *event, if (point->grabberPointerHandler()) cancelTouchMouseSynthesis(); } else { - qCWarning(DBG_TOUCH_TARGET) << "during delivery of touch press, synth-mouse ID" << touchMouseId << "is missing from" << event; + qCWarning(DBG_TOUCH_TARGET) << "during delivery of touch press, synth-mouse ID" << hex << touchMouseId << "is missing from" << event; } } for (int i = 0; i < pointCount; ++i) { diff --git a/src/quick/quick.pro b/src/quick/quick.pro index 37d2ad1172..700f794af4 100644 --- a/src/quick/quick.pro +++ b/src/quick/quick.pro @@ -1,6 +1,6 @@ TARGET = QtQuick -QT = core-private gui-private qml-private +QT = core-private gui-private qml-private qmlmodels-private qtConfig(qml-network): \ QT_PRIVATE += network diff --git a/src/quick/util/qquickanimation.cpp b/src/quick/util/qquickanimation.cpp index 02be9daac0..2043b50545 100644 --- a/src/quick/util/qquickanimation.cpp +++ b/src/quick/util/qquickanimation.cpp @@ -812,7 +812,7 @@ QQuickColorAnimation::~QQuickColorAnimation() // States are defined here... ] - transition: Transition { + transitions: Transition { ColorAnimation { from: "#c0c0c0"; duration: 2000 } } } diff --git a/src/quick/util/qquickboundaryrule.cpp b/src/quick/util/qquickboundaryrule.cpp new file mode 100644 index 0000000000..3558c8bfa0 --- /dev/null +++ b/src/quick/util/qquickboundaryrule.cpp @@ -0,0 +1,574 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://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.LGPL3 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-3.0.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 (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquickboundaryrule_p.h" + +#include <qqmlcontext.h> +#include <qqmlinfo.h> +#include <private/qqmlproperty_p.h> +#include <private/qqmlengine_p.h> +#include <private/qobject_p.h> +#include <private/qquickanimation_p_p.h> +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcBR, "qt.quick.boundaryrule") + +class QQuickBoundaryReturnJob; +class QQuickBoundaryRulePrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QQuickBoundaryRule) +public: + QQuickBoundaryRulePrivate() {} + + QQmlProperty property; + QEasingCurve easing = QEasingCurve(QEasingCurve::OutQuad); + QQuickBoundaryReturnJob *returnAnimationJob = nullptr; + // read-only properties, updated on each write() + qreal targetValue = 0; // after easing was applied + qreal peakOvershoot = 0; + qreal currentOvershoot = 0; + // settable properties + qreal minimum = 0; + qreal maximum = 0; + qreal minimumOvershoot = 0; + qreal maximumOvershoot = 0; + qreal overshootScale = 0.5; + int returnDuration = 100; + QQuickBoundaryRule::OvershootFilter overshootFilter = QQuickBoundaryRule::OvershootFilter::None; + bool enabled = true; + bool finalized = false; + + qreal easedOvershoot(qreal overshootingValue); + void resetOvershoot(); +}; + +class QQuickBoundaryReturnJob : public QAbstractAnimationJob +{ +public: + QQuickBoundaryReturnJob(QQuickBoundaryRulePrivate *br, qreal to) + : QAbstractAnimationJob() + , boundaryRule(br) + , fromValue(br->targetValue) + , toValue(to) {} + + int duration() const override { return boundaryRule->returnDuration; } + + void updateCurrentTime(int) override; + + void updateState(QAbstractAnimationJob::State newState, + QAbstractAnimationJob::State oldState) override; + + QQuickBoundaryRulePrivate *boundaryRule; + qreal fromValue; // snapshot of initial value from which we're returning + qreal toValue; // target property value to which we're returning +}; + +void QQuickBoundaryReturnJob::updateCurrentTime(int t) +{ + // The easing property tells how to behave when the property is being + // externally manipulated beyond the bounds. During returnToBounds() + // we run it in reverse, by reversing time. + qreal progress = (duration() - t) / qreal(duration()); + qreal easingValue = boundaryRule->easing.valueForProgress(progress); + qreal delta = qAbs(fromValue - toValue) * easingValue; + qreal value = (fromValue > toValue ? toValue + delta : toValue - delta); + qCDebug(lcBR) << t << "ms" << qRound(progress * 100) << "% easing" << easingValue << "->" << value; + QQmlPropertyPrivate::write(boundaryRule->property, value, + QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding); +} + +void QQuickBoundaryReturnJob::updateState(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State oldState) +{ + Q_UNUSED(oldState) + if (newState == QAbstractAnimationJob::Stopped) { + qCDebug(lcBR) << "return animation done"; + boundaryRule->resetOvershoot(); + boundaryRule->returnAnimationJob = nullptr; + delete this; + } +} + +/*! + \qmltype BoundaryRule + \instantiates QQuickBoundaryRule + \inqmlmodule Qt.labs.animation + \ingroup qtquick-transitions-animations + \ingroup qtquick-interceptors + \brief Defines a restriction on the range of values that can be set on a numeric property. + \since 5.14 + + A BoundaryRule defines the range of values that a particular property is + allowed to have. When an out-of-range value would otherwise be set, + it applies "resistance" via an easing curve. + + For example, the following BoundaryRule prevents DragHandler from dragging + the Rectangle too far: + + \snippet qml/boundaryRule.qml 0 + + Note that a property cannot have more than one assigned BoundaryRule. + + \sa {Animation and Transitions in Qt Quick}, {Qt Quick Examples - Animation#Behaviors}{Behavior example}, {Qt QML} +*/ + +QQuickBoundaryRule::QQuickBoundaryRule(QObject *parent) + : QObject(*(new QQuickBoundaryRulePrivate), parent) + , QQmlPropertyValueInterceptor() +{ +} + +QQuickBoundaryRule::~QQuickBoundaryRule() +{ + Q_D(QQuickBoundaryRule); + // stop any running animation and + // prevent QQuickBoundaryReturnJob::updateState() from accessing QQuickBoundaryRulePrivate + delete d->returnAnimationJob; +} + +/*! + \qmlproperty bool QtQuick::BoundaryRule::enabled + + This property holds whether the rule will be enforced when the tracked + property changes value. + + By default a BoundaryRule is enabled. +*/ +bool QQuickBoundaryRule::enabled() const +{ + Q_D(const QQuickBoundaryRule); + return d->enabled; +} + +void QQuickBoundaryRule::setEnabled(bool enabled) +{ + Q_D(QQuickBoundaryRule); + if (d->enabled == enabled) + return; + d->enabled = enabled; + emit enabledChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::minimum + + This property holds the smallest unconstrained value that the property is + allowed to have. If the property is set to a smaller value, it will be + constrained by \l easing and \l minimumOvershoot. + + The default is \c 0. +*/ +qreal QQuickBoundaryRule::minimum() const +{ + Q_D(const QQuickBoundaryRule); + return d->minimum; +} + +void QQuickBoundaryRule::setMinimum(qreal minimum) +{ + Q_D(QQuickBoundaryRule); + if (qFuzzyCompare(d->minimum, minimum)) + return; + d->minimum = minimum; + emit minimumChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::minimumOvershoot + + This property holds the amount that the property is allowed to be + less than \l minimum. Whenever the value is less than \l minimum + and greater than \c {minimum - minimumOvershoot}, it is constrained + by the \l easing curve. When the value attempts to go under + \c {minimum - minimumOvershoots} there is a hard stop. + + The default is \c 0. +*/ +qreal QQuickBoundaryRule::minimumOvershoot() const +{ + Q_D(const QQuickBoundaryRule); + return d->minimumOvershoot; +} + +void QQuickBoundaryRule::setMinimumOvershoot(qreal minimumOvershoot) +{ + Q_D(QQuickBoundaryRule); + if (qFuzzyCompare(d->minimumOvershoot, minimumOvershoot)) + return; + d->minimumOvershoot = minimumOvershoot; + emit minimumOvershootChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::maximum + + This property holds the largest unconstrained value that the property is + allowed to have. If the property is set to a larger value, it will be + constrained by \l easing and \l maximumOvershoot. + + The default is \c 1. +*/ +qreal QQuickBoundaryRule::maximum() const +{ + Q_D(const QQuickBoundaryRule); + return d->maximum; +} + +void QQuickBoundaryRule::setMaximum(qreal maximum) +{ + Q_D(QQuickBoundaryRule); + if (qFuzzyCompare(d->maximum, maximum)) + return; + d->maximum = maximum; + emit maximumChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::maximumOvershoot + + This property holds the amount that the property is allowed to be + more than \l maximum. Whenever the value is greater than \l maximum + and less than \c {maximum + maximumOvershoot}, it is constrained + by the \l easing curve. When the value attempts to exceed + \c {maximum + maximumOvershoot} there is a hard stop. + + The default is 0. +*/ +qreal QQuickBoundaryRule::maximumOvershoot() const +{ + Q_D(const QQuickBoundaryRule); + return d->maximumOvershoot; +} + +void QQuickBoundaryRule::setMaximumOvershoot(qreal maximumOvershoot) +{ + Q_D(QQuickBoundaryRule); + if (qFuzzyCompare(d->maximumOvershoot, maximumOvershoot)) + return; + d->maximumOvershoot = maximumOvershoot; + emit maximumOvershootChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::overshootScale + + This property holds the amount by which the \l easing is scaled during the + overshoot condition. For example if an Item is restricted from moving more + than 100 pixels beyond some limit, and the user (by means of some Input + Handler) is trying to drag it 100 pixels past the limit, if overshootScale + is set to 1, the user will succeed: the only effect of the easing curve is + to change the rate at which the item moves from overshoot 0 to overshoot + 100. But if it is set to 0.5, the BoundaryRule provides resistance such + that when the user tries to move 100 pixels, the Item will only move 50 + pixels; and the easing curve modulates the rate of movement such that it + may move in sync with the user's attempted movement at the beginning, and + then slows down, depending on the shape of the easing curve. + + The default is 0.5. +*/ +qreal QQuickBoundaryRule::overshootScale() const +{ + Q_D(const QQuickBoundaryRule); + return d->overshootScale; +} + +void QQuickBoundaryRule::setOvershootScale(qreal overshootScale) +{ + Q_D(QQuickBoundaryRule); + if (qFuzzyCompare(d->overshootScale, overshootScale)) + return; + d->overshootScale = overshootScale; + emit overshootScaleChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::currentOvershoot + + This property holds the amount by which the most recently set value of the + intercepted property exceeds \l maximum or is less than \l minimum. + + It is positive if the property value exceeds \l maximum, negative if the + property value is less than \l minimum, or 0 if the property value is + within both boundaries. +*/ +qreal QQuickBoundaryRule::currentOvershoot() const +{ + Q_D(const QQuickBoundaryRule); + return d->currentOvershoot; +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::peakOvershoot + + This property holds the most-positive or most-negative value of + \l currentOvershoot that has been seen, until \l returnToBounds() is called. + + This can be useful when the intercepted property value is known to + fluctuate, and you want to find and react to the maximum amount of + overshoot rather than to the fluctuations. + + \sa overshootFilter +*/ +qreal QQuickBoundaryRule::peakOvershoot() const +{ + Q_D(const QQuickBoundaryRule); + return d->peakOvershoot; +} + +/*! + \qmlproperty enum QtQuick::BoundaryRule::overshootFilter + + This property specifies the aggregation function that will be applied to + the intercepted property value. + + If this is set to \c BoundaryRule.None (the default), the intercepted + property will hold a value whose overshoot is limited to \l currentOvershoot. + If this is set to \c BoundaryRule.Peak, the intercepted property will hold + a value whose overshoot is limited to \l peakOvershoot. +*/ +QQuickBoundaryRule::OvershootFilter QQuickBoundaryRule::overshootFilter() const +{ + Q_D(const QQuickBoundaryRule); + return d->overshootFilter; +} + +void QQuickBoundaryRule::setOvershootFilter(OvershootFilter overshootFilter) +{ + Q_D(QQuickBoundaryRule); + if (d->overshootFilter == overshootFilter) + return; + d->overshootFilter = overshootFilter; + emit overshootFilterChanged(); +} + +/*! + \qmlmethod bool QtQuick::BoundaryRule::returnToBounds + + Returns the intercepted property to a value between \l minimum and + \l maximum, such that \l currentOvershoot and \l peakOvershoot are both + zero. This will be animated if \l returnDuration is greater than zero. + + Returns true if the value needed to be adjusted, or false if it was already + within bounds. +*/ +bool QQuickBoundaryRule::returnToBounds() +{ + Q_D(QQuickBoundaryRule); + if (d->returnAnimationJob) { + qCDebug(lcBR) << "animation already in progress"; + return true; + } + if (currentOvershoot() > 0) { + if (d->returnDuration > 0) + d->returnAnimationJob = new QQuickBoundaryReturnJob(d, maximum()); + else + write(maximum()); + } else if (currentOvershoot() < 0) { + if (d->returnDuration > 0) + d->returnAnimationJob = new QQuickBoundaryReturnJob(d, minimum()); + else + write(minimum()); + } else { + return false; + } + if (d->returnAnimationJob) { + qCDebug(lcBR) << "animating from" << d->returnAnimationJob->fromValue << "to" << d->returnAnimationJob->toValue; + d->returnAnimationJob->start(); + } else { + d->resetOvershoot(); + qCDebug(lcBR) << "returned to" << d->property.read(); + } + return true; +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::easing + + This property holds the easing curve to be applied in overshoot mode + (whenever the \l minimum or \l maximum constraint is violated, while + the value is still within the respective overshoot range). + + The default easing curve is \l QEasingCurve::OutQuad. +*/ +QEasingCurve QQuickBoundaryRule::easing() const +{ + Q_D(const QQuickBoundaryRule); + return d->easing; +} + +void QQuickBoundaryRule::setEasing(const QEasingCurve &easing) +{ + Q_D(QQuickBoundaryRule); + if (d->easing == easing) + return; + d->easing = easing; + emit easingChanged(); +} + +/*! + \qmlproperty int QtQuick::BoundaryRule::returnDuration + + This property holds the amount of time in milliseconds that + \l returnToBounds() will take to return the target property to the nearest bound. + If it is set to 0, returnToBounds() will set the property immediately + rather than creating an animation job. + + The default is 100 ms. +*/ +int QQuickBoundaryRule::returnDuration() const +{ + Q_D(const QQuickBoundaryRule); + return d->returnDuration; +} + +void QQuickBoundaryRule::setReturnDuration(int duration) +{ + Q_D(QQuickBoundaryRule); + if (d->returnDuration == duration) + return; + d->returnDuration = duration; + emit returnDurationChanged(); +} + +void QQuickBoundaryRule::write(const QVariant &value) +{ + bool conversionOk = false; + qreal rValue = value.toReal(&conversionOk); + if (!conversionOk) { + qWarning() << "BoundaryRule doesn't work with non-numeric values:" << value; + return; + } + Q_D(QQuickBoundaryRule); + bool bypass = !d->enabled || !d->finalized || QQmlEnginePrivate::designerMode(); + if (bypass) { + QQmlPropertyPrivate::write(d->property, value, + QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding); + return; + } + + qmlExecuteDeferred(this); + d->targetValue = d->easedOvershoot(rValue); + QQmlPropertyPrivate::write(d->property, d->targetValue, + QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding); +} + +void QQuickBoundaryRule::setTarget(const QQmlProperty &property) +{ + Q_D(QQuickBoundaryRule); + d->property = property; + + QQmlEnginePrivate *engPriv = QQmlEnginePrivate::get(qmlEngine(this)); + static int finalizedIdx = -1; + if (finalizedIdx < 0) + finalizedIdx = metaObject()->indexOfSlot("componentFinalized()"); + engPriv->registerFinalizeCallback(this, finalizedIdx); +} + +void QQuickBoundaryRule::componentFinalized() +{ + Q_D(QQuickBoundaryRule); + d->finalized = true; +} + +/*! + \internal + Given that something is trying to set the target property to \a value, + this function applies the easing curve and returns the value that the + property should actually get instead. +*/ +qreal QQuickBoundaryRulePrivate::easedOvershoot(qreal value) +{ + qreal ret = value; + Q_Q(QQuickBoundaryRule); + if (value > maximum) { + qreal overshootWas = currentOvershoot; + currentOvershoot = value - maximum; + if (!qFuzzyCompare(overshootWas, currentOvershoot)) + emit q->currentOvershootChanged(); + overshootWas = peakOvershoot; + peakOvershoot = qMax(currentOvershoot, peakOvershoot); + if (!qFuzzyCompare(overshootWas, peakOvershoot)) + emit q->peakOvershootChanged(); + ret = maximum + maximumOvershoot * easing.valueForProgress( + (overshootFilter == QQuickBoundaryRule::OvershootFilter::Peak ? peakOvershoot : currentOvershoot) + * overshootScale / maximumOvershoot); + qCDebug(lcBR).nospace() << value << " overshoots maximum " << maximum << " by " + << currentOvershoot << " (peak " << peakOvershoot << "): eased to " << ret; + } else if (value < minimum) { + qreal overshootWas = currentOvershoot; + currentOvershoot = value - minimum; + if (!qFuzzyCompare(overshootWas, currentOvershoot)) + emit q->currentOvershootChanged(); + overshootWas = peakOvershoot; + peakOvershoot = qMin(currentOvershoot, peakOvershoot); + if (!qFuzzyCompare(overshootWas, peakOvershoot)) + emit q->peakOvershootChanged(); + ret = minimum - minimumOvershoot * easing.valueForProgress( + -(overshootFilter == QQuickBoundaryRule::OvershootFilter::Peak ? peakOvershoot : currentOvershoot) + * overshootScale / minimumOvershoot); + qCDebug(lcBR).nospace() << value << " overshoots minimum " << minimum << " by " + << currentOvershoot << " (peak " << peakOvershoot << "): eased to " << ret; + } else { + resetOvershoot(); + } + return ret; +} + +/*! + \internal + Resets the currentOvershoot and peakOvershoot + properties to zero. +*/ +void QQuickBoundaryRulePrivate::resetOvershoot() +{ + Q_Q(QQuickBoundaryRule); + if (!qFuzzyCompare(peakOvershoot, 0)) { + peakOvershoot = 0; + emit q->peakOvershootChanged(); + } + if (!qFuzzyCompare(currentOvershoot, 0)) { + currentOvershoot = 0; + emit q->currentOvershootChanged(); + } +} + +QT_END_NAMESPACE + +#include "moc_qquickboundaryrule_p.cpp" diff --git a/src/quick/util/qquickboundaryrule_p.h b/src/quick/util/qquickboundaryrule_p.h new file mode 100644 index 0000000000..3325b675c5 --- /dev/null +++ b/src/quick/util/qquickboundaryrule_p.h @@ -0,0 +1,145 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://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.LGPL3 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-3.0.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 (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKBOUNDARYRULE_H +#define QQUICKBOUNDARYRULE_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 <private/qtquickglobal_p.h> + +#include <private/qqmlpropertyvalueinterceptor_p.h> +#include <qqml.h> + +QT_BEGIN_NAMESPACE + +class QQuickAbstractAnimation; +class QQuickBoundaryRulePrivate; +class Q_QUICK_PRIVATE_EXPORT QQuickBoundaryRule : public QObject, public QQmlPropertyValueInterceptor +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QQuickBoundaryRule) + + Q_INTERFACES(QQmlPropertyValueInterceptor) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(qreal minimum READ minimum WRITE setMinimum NOTIFY minimumChanged) + Q_PROPERTY(qreal minimumOvershoot READ minimumOvershoot WRITE setMinimumOvershoot NOTIFY minimumOvershootChanged) + Q_PROPERTY(qreal maximum READ maximum WRITE setMaximum NOTIFY maximumChanged) + Q_PROPERTY(qreal maximumOvershoot READ maximumOvershoot WRITE setMaximumOvershoot NOTIFY maximumOvershootChanged) + Q_PROPERTY(qreal overshootScale READ overshootScale WRITE setOvershootScale NOTIFY overshootScaleChanged) + Q_PROPERTY(qreal currentOvershoot READ currentOvershoot NOTIFY currentOvershootChanged) + Q_PROPERTY(qreal peakOvershoot READ peakOvershoot NOTIFY peakOvershootChanged) + Q_PROPERTY(OvershootFilter overshootFilter READ overshootFilter WRITE setOvershootFilter NOTIFY overshootFilterChanged) + Q_PROPERTY(QEasingCurve easing READ easing WRITE setEasing NOTIFY easingChanged) + Q_PROPERTY(int returnDuration READ returnDuration WRITE setReturnDuration NOTIFY returnDurationChanged) + +public: + enum OvershootFilter { + None, + Peak + }; + Q_ENUM(OvershootFilter) + + QQuickBoundaryRule(QObject *parent=nullptr); + ~QQuickBoundaryRule(); + + void setTarget(const QQmlProperty &) override; + void write(const QVariant &value) override; + + bool enabled() const; + void setEnabled(bool enabled); + + qreal minimum() const; + void setMinimum(qreal minimum); + qreal minimumOvershoot() const; + void setMinimumOvershoot(qreal minimum); + + qreal maximum() const; + void setMaximum(qreal maximum); + qreal maximumOvershoot() const; + void setMaximumOvershoot(qreal maximum); + + qreal overshootScale() const; + void setOvershootScale(qreal scale); + + qreal currentOvershoot() const; + qreal peakOvershoot() const; + + OvershootFilter overshootFilter() const; + void setOvershootFilter(OvershootFilter overshootFilter); + + Q_INVOKABLE bool returnToBounds(); + + QEasingCurve easing() const; + void setEasing(const QEasingCurve &easing); + + int returnDuration() const; + void setReturnDuration(int duration); + +Q_SIGNALS: + void enabledChanged(); + void minimumChanged(); + void minimumOvershootChanged(); + void maximumChanged(); + void maximumOvershootChanged(); + void overshootScaleChanged(); + void currentOvershootChanged(); + void peakOvershootChanged(); + void overshootFilterChanged(); + void easingChanged(); + void returnDurationChanged(); + +private Q_SLOTS: + void componentFinalized(); +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickBoundaryRule) + +#endif // QQUICKBOUNDARYRULE_H diff --git a/src/quick/util/util.pri b/src/quick/util/util.pri index c51f082d03..63d995e34c 100644 --- a/src/quick/util/util.pri +++ b/src/quick/util/util.pri @@ -15,6 +15,7 @@ SOURCES += \ $$PWD/qquicktimeline.cpp \ $$PWD/qquickpixmapcache.cpp \ $$PWD/qquickbehavior.cpp \ + $$PWD/qquickboundaryrule.cpp \ $$PWD/qquickfontloader.cpp \ $$PWD/qquickstyledtext.cpp \ $$PWD/qquickimageprovider.cpp \ @@ -50,6 +51,7 @@ HEADERS += \ $$PWD/qquicktimeline_p_p.h \ $$PWD/qquickpixmapcache_p.h \ $$PWD/qquickbehavior_p.h \ + $$PWD/qquickboundaryrule_p.h \ $$PWD/qquickfontloader_p.h \ $$PWD/qquickstyledtext_p.h \ $$PWD/qquickimageprovider.h \ |