diff options
Diffstat (limited to 'src/quickcontrols/material/impl')
15 files changed, 1819 insertions, 0 deletions
diff --git a/src/quickcontrols/material/impl/BoxShadow.qml b/src/quickcontrols/material/impl/BoxShadow.qml new file mode 100644 index 0000000000..f77412d554 --- /dev/null +++ b/src/quickcontrols/material/impl/BoxShadow.qml @@ -0,0 +1,39 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Controls.Material.impl + +/* + A implementation of CSS's box-shadow, used by ElevationEffect for a Material Design + elevation shadow effect. + */ +RectangularGlow { + // The 4 properties from CSS box-shadow, plus the inherited color property + property int offsetX + property int offsetY + property int blurRadius + property int spreadRadius + + // The source item the shadow is being applied to, used for correctly + // calculating the corner radious + property Item source + + property bool fullWidth + property bool fullHeight + + x: (parent.width - width)/2 + offsetX + y: (parent.height - height)/2 + offsetY + + implicitWidth: source ? source.width : parent.width + implicitHeight: source ? source.height : parent.height + + width: implicitWidth + 2 * spreadRadius + (fullWidth ? 2 * cornerRadius : 0) + height: implicitHeight + 2 * spreadRadius + (fullHeight ? 2 * cornerRadius : 0) + glowRadius: blurRadius/2 + spread: 0.05 + // qmllint disable unqualified + // Intentionally duck-typed (QTBUG-94807) + cornerRadius: blurRadius + (source && source.radius || 0) +} diff --git a/src/quickcontrols/material/impl/CMakeLists.txt b/src/quickcontrols/material/impl/CMakeLists.txt new file mode 100644 index 0000000000..8b8e2311b4 --- /dev/null +++ b/src/quickcontrols/material/impl/CMakeLists.txt @@ -0,0 +1,44 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## qtquickcontrols2materialstyleimplplugin Plugin: +##################################################################### + +set(qml_files + "BoxShadow.qml" + "CheckIndicator.qml" + "CursorDelegate.qml" + "ElevationEffect.qml" + "RadioIndicator.qml" + "RectangularGlow.qml" + "SliderHandle.qml" + "SwitchIndicator.qml" +) + +qt_internal_add_qml_module(qtquickcontrols2materialstyleimplplugin + URI "QtQuick.Controls.Material.impl" + VERSION "${PROJECT_VERSION}" + PAST_MAJOR_VERSIONS 2 + CLASS_NAME QtQuickControls2MaterialStyleImplPlugin + DEPENDENCIES + QtQuick/auto + PLUGIN_TARGET qtquickcontrols2materialstyleimplplugin + NO_PLUGIN_OPTIONAL + SOURCES + qquickmaterialbusyindicator.cpp qquickmaterialbusyindicator_p.h + qquickmaterialprogressbar.cpp qquickmaterialprogressbar_p.h + qquickmaterialripple.cpp qquickmaterialripple_p.h + QML_FILES + ${qml_files} + DEFINES + QT_NO_CAST_FROM_ASCII + QT_NO_CAST_TO_ASCII + LIBRARIES + Qt::CorePrivate + Qt::Gui + Qt::QmlPrivate + Qt::QuickControls2ImplPrivate + Qt::QuickPrivate + Qt::QuickTemplates2Private +) diff --git a/src/quickcontrols/material/impl/CheckIndicator.qml b/src/quickcontrols/material/impl/CheckIndicator.qml new file mode 100644 index 0000000000..b09567c04b --- /dev/null +++ b/src/quickcontrols/material/impl/CheckIndicator.qml @@ -0,0 +1,87 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Controls.Material.impl + +Rectangle { + id: indicatorItem + implicitWidth: 18 + implicitHeight: 18 + color: "transparent" + border.color: !control.enabled ? control.Material.hintTextColor + : checkState !== Qt.Unchecked ? control.Material.accentColor : control.Material.secondaryTextColor + border.width: checkState !== Qt.Unchecked ? width / 2 : 2 + radius: 2 + + property Item control + property int checkState: control.checkState + + Behavior on border.width { + NumberAnimation { + duration: 100 + easing.type: Easing.OutCubic + } + } + + Behavior on border.color { + ColorAnimation { + duration: 100 + easing.type: Easing.OutCubic + } + } + + // TODO: This needs to be transparent + Image { + id: checkImage + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + width: 14 + height: 14 + source: "qrc:/qt-project.org/imports/QtQuick/Controls/Material/images/check.png" + fillMode: Image.PreserveAspectFit + + scale: indicatorItem.checkState === Qt.Checked ? 1 : 0 + Behavior on scale { NumberAnimation { duration: 100 } } + } + + Rectangle { + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + width: 12 + height: 3 + + scale: indicatorItem.checkState === Qt.PartiallyChecked ? 1 : 0 + Behavior on scale { NumberAnimation { duration: 100 } } + } + + states: [ + State { + name: "checked" + when: indicatorItem.checkState === Qt.Checked + }, + State { + name: "partiallychecked" + when: indicatorItem.checkState === Qt.PartiallyChecked + } + ] + + transitions: Transition { + SequentialAnimation { + NumberAnimation { + target: indicatorItem + property: "scale" + // Go down 2 pixels in size. + to: 1 - 2 / indicatorItem.width + duration: 120 + } + NumberAnimation { + target: indicatorItem + property: "scale" + to: 1 + duration: 120 + } + } + } +} diff --git a/src/quickcontrols/material/impl/CursorDelegate.qml b/src/quickcontrols/material/impl/CursorDelegate.qml new file mode 100644 index 0000000000..811aa89e36 --- /dev/null +++ b/src/quickcontrols/material/impl/CursorDelegate.qml @@ -0,0 +1,32 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls.Material + +Rectangle { + id: cursor + + color: parent.Material.accentColor + width: 2 + visible: parent.activeFocus && !parent.readOnly && parent.selectionStart === parent.selectionEnd + + Connections { + target: cursor.parent + function onCursorPositionChanged() { + // keep a moving cursor visible + cursor.opacity = 1 + timer.restart() + } + } + + Timer { + id: timer + running: cursor.parent.activeFocus && !cursor.parent.readOnly && interval != 0 + repeat: true + interval: Qt.styleHints.cursorFlashTime / 2 + onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0 + // force the cursor visible when gaining focus + onRunningChanged: cursor.opacity = 1 + } +} diff --git a/src/quickcontrols/material/impl/ElevationEffect.qml b/src/quickcontrols/material/impl/ElevationEffect.qml new file mode 100644 index 0000000000..6690567617 --- /dev/null +++ b/src/quickcontrols/material/impl/ElevationEffect.qml @@ -0,0 +1,246 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Controls.Material.impl + +/* + An effect for standard Material Design elevation shadows. Useful for using as \c layer.effect. + */ +Item { + id: effect + + /* + The source the effect is applied to. + */ + property var source + + /* + The elevation of the \l source Item. + */ + property int elevation: 0 + + /* + Set to \c true if the \l source Item is the same width as its parent and the shadow + should be full width instead of rounding around the corner of the Item. + + \sa fullHeight + */ + property bool fullWidth: false + + /* + Set to \c true if the \l source Item is the same height as its parent and the shadow + should be full height instead of rounding around the corner of the Item. + + \sa fullWidth + */ + property bool fullHeight: false + + /* + \internal + + The actual source Item the effect is applied to. + */ + readonly property Item sourceItem: source.sourceItem + + /* + * The following shadow values are taken from Angular Material + * + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Google, Inc. http://angularjs.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + /* + \internal + + The shadows to use for each possible elevation. There are three shadows that when combined + make up the elevation. + */ + readonly property var _shadows: [ + [{offset: 0, blur: 0, spread: 0}, + {offset: 0, blur: 0, spread: 0}, + {offset: 0, blur: 0, spread: 0}], + + [{offset: 1, blur: 3, spread: 0}, + {offset: 1, blur: 1, spread: 0}, + {offset: 2, blur: 1, spread: -1}], + + [{offset: 1, blur: 5, spread: 0}, + {offset: 2, blur: 2, spread: 0}, + {offset: 3, blur: 1, spread: -2}], + + [{offset: 1, blur: 8, spread: 0}, + {offset: 3, blur: 4, spread: 0}, + {offset: 3, blur: 3, spread: -2}], + + [{offset: 2, blur: 4, spread: -1}, + {offset: 4, blur: 5, spread: 0}, + {offset: 1, blur: 10, spread: 0}], + + [{offset: 3, blur: 5, spread: -1}, + {offset: 5, blur: 8, spread: 0}, + {offset: 1, blur: 14, spread: 0}], + + [{offset: 3, blur: 5, spread: -1}, + {offset: 6, blur: 10, spread: 0}, + {offset: 1, blur: 18, spread: 0}], + + [{offset: 4, blur: 5, spread: -2}, + {offset: 7, blur: 10, spread: 1}, + {offset: 2, blur: 16, spread: 1}], + + [{offset: 5, blur: 5, spread: -3}, + {offset: 8, blur: 10, spread: 1}, + {offset: 3, blur: 14, spread: 2}], + + [{offset: 5, blur: 6, spread: -3}, + {offset: 9, blur: 12, spread: 1}, + {offset: 3, blur: 16, spread: 2}], + + [{offset: 6, blur: 6, spread: -3}, + {offset: 10, blur: 14, spread: 1}, + {offset: 4, blur: 18, spread: 3}], + + [{offset: 6, blur: 7, spread: -4}, + {offset: 11, blur: 15, spread: 1}, + {offset: 4, blur: 20, spread: 3}], + + [{offset: 7, blur: 8, spread: -4}, + {offset: 12, blur: 17, spread: 2}, + {offset: 5, blur: 22, spread: 4}], + + [{offset: 7, blur: 8, spread: -4}, + {offset: 13, blur: 19, spread: 2}, + {offset: 5, blur: 24, spread: 4}], + + [{offset: 7, blur: 9, spread: -4}, + {offset: 14, blur: 21, spread: 2}, + {offset: 5, blur: 26, spread: 4}], + + [{offset: 8, blur: 9, spread: -5}, + {offset: 15, blur: 22, spread: 2}, + {offset: 6, blur: 28, spread: 5}], + + [{offset: 8, blur: 10, spread: -5}, + {offset: 16, blur: 24, spread: 2}, + {offset: 6, blur: 30, spread: 5}], + + [{offset: 8, blur: 11, spread: -5}, + {offset: 17, blur: 26, spread: 2}, + {offset: 6, blur: 32, spread: 5}], + + [{offset: 9, blur: 11, spread: -5}, + {offset: 18, blur: 28, spread: 2}, + {offset: 7, blur: 34, spread: 6}], + + [{offset: 9, blur: 12, spread: -6}, + {offset: 19, blur: 29, spread: 2}, + {offset: 7, blur: 36, spread: 6}], + + [{offset: 10, blur: 13, spread: -6}, + {offset: 20, blur: 31, spread: 3}, + {offset: 8, blur: 38, spread: 7}], + + [{offset: 10, blur: 13, spread: -6}, + {offset: 21, blur: 33, spread: 3}, + {offset: 8, blur: 40, spread: 7}], + + [{offset: 10, blur: 14, spread: -6}, + {offset: 22, blur: 35, spread: 3}, + {offset: 8, blur: 42, spread: 7}], + + [{offset: 11, blur: 14, spread: -7}, + {offset: 23, blur: 36, spread: 3}, + {offset: 9, blur: 44, spread: 8}], + + [{offset: 11, blur: 15, spread: -7}, + {offset: 24, blur: 38, spread: 3}, + {offset: 9, blur: 46, spread: 8}] + ] + + /* + \internal + + The current shadow based on the elevation. + */ + readonly property var _shadow: _shadows[Math.max(0, Math.min(elevation, _shadows.length - 1))] + + // Nest the shadows and source view in two items rendered as a layer + // so the shadow is not clipped by the bounds of the source view + Item { + property int margin: -100 + + x: margin + y: margin + width: parent.width - 2 * margin + height: parent.height - 2 * margin + + // By rendering as a layer, the shadow will never show through the source item, + // even when the source item's opacity is less than 1 + layer.enabled: true + + // The box shadows automatically pick up the size of the source Item and not + // the size of the parent, so we don't need to worry about the extra padding + // in the parent Item + BoxShadow { + offsetY: effect._shadow[0].offset + blurRadius: effect._shadow[0].blur + spreadRadius: effect._shadow[0].spread + color: Qt.rgba(0,0,0, 0.2) + + fullWidth: effect.fullWidth + fullHeight: effect.fullHeight + source: effect.sourceItem + } + + BoxShadow { + offsetY: effect._shadow[1].offset + blurRadius: effect._shadow[1].blur + spreadRadius: effect._shadow[1].spread + color: Qt.rgba(0,0,0, 0.14) + + fullWidth: effect.fullWidth + fullHeight: effect.fullHeight + source: effect.sourceItem + } + + BoxShadow { + offsetY: effect._shadow[2].offset + blurRadius: effect._shadow[2].blur + spreadRadius: effect._shadow[2].spread + color: Qt.rgba(0,0,0, 0.12) + + fullWidth: effect.fullWidth + fullHeight: effect.fullHeight + source: effect.sourceItem + } + + ShaderEffect { + property alias source: effect.source + + x: (parent.width - width)/2 + y: (parent.height - height)/2 + width: effect.sourceItem.width + height: effect.sourceItem.height + } + } +} diff --git a/src/quickcontrols/material/impl/RadioIndicator.qml b/src/quickcontrols/material/impl/RadioIndicator.qml new file mode 100644 index 0000000000..9570065692 --- /dev/null +++ b/src/quickcontrols/material/impl/RadioIndicator.qml @@ -0,0 +1,30 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.Material +import QtQuick.Controls.Material.impl + +Rectangle { + id: indicator + implicitWidth: 20 + implicitHeight: 20 + radius: width / 2 + border.width: 2 + border.color: !control.enabled ? control.Material.hintTextColor + : control.checked || control.down ? control.Material.accentColor : control.Material.secondaryTextColor + color: "transparent" + + property T.AbstractButton control + + Rectangle { + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + width: 10 + height: 10 + radius: width / 2 + color: parent.border.color + visible: indicator.control.checked || indicator.control.down + } +} diff --git a/src/quickcontrols/material/impl/RectangularGlow.qml b/src/quickcontrols/material/impl/RectangularGlow.qml new file mode 100644 index 0000000000..b604049cf9 --- /dev/null +++ b/src/quickcontrols/material/impl/RectangularGlow.qml @@ -0,0 +1,207 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick + +/* + A cross-graphics API implementation of QtGraphicalEffects' RectangularGlow. + */ +Item { + id: rootItem + + /* + This property defines how many pixels outside the item area are reached + by the glow. + + The value ranges from 0.0 (no glow) to inf (infinite glow). By default, + the property is set to \c 0.0. + + \table + \header + \li Output examples with different glowRadius values + \li + \li + \row + \li \image RectangularGlow_glowRadius1.png + \li \image RectangularGlow_glowRadius2.png + \li \image RectangularGlow_glowRadius3.png + \row + \li \b { glowRadius: 10 } + \li \b { glowRadius: 20 } + \li \b { glowRadius: 40 } + \row + \li \l spread: 0 + \li \l spread: 0 + \li \l spread: 0 + \row + \li \l color: #ffffff + \li \l color: #ffffff + \li \l color: #ffffff + \row + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \endtable + + */ + property real glowRadius: 0.0 + + /* + This property defines how large part of the glow color is strenghtened + near the source edges. + + The value ranges from 0.0 (no strenght increase) to 1.0 (maximum + strenght increase). By default, the property is set to \c 0.0. + + \table + \header + \li Output examples with different spread values + \li + \li + \row + \li \image RectangularGlow_spread1.png + \li \image RectangularGlow_spread2.png + \li \image RectangularGlow_spread3.png + \row + \li \b { spread: 0.0 } + \li \b { spread: 0.5 } + \li \b { spread: 1.0 } + \row + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \row + \li \l color: #ffffff + \li \l color: #ffffff + \li \l color: #ffffff + \row + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \endtable + */ + property real spread: 0.0 + + /* + This property defines the RGBA color value which is used for the glow. + + By default, the property is set to \c "white". + + \table + \header + \li Output examples with different color values + \li + \li + \row + \li \image RectangularGlow_color1.png + \li \image RectangularGlow_color2.png + \li \image RectangularGlow_color3.png + \row + \li \b { color: #ffffff } + \li \b { color: #55ff55 } + \li \b { color: #5555ff } + \row + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \row + \li \l spread: 0 + \li \l spread: 0 + \li \l spread: 0 + \row + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \li \l cornerRadius: 25 + \endtable + */ + property color color: "white" + + /* + This property defines the corner radius that is used to draw a glow with + rounded corners. + + The value ranges from 0.0 to half of the effective width or height of + the glow, whichever is smaller. This can be calculated with: \c{ + min(width, height) / 2.0 + glowRadius} + + By default, the property is bound to glowRadius property. The glow + behaves as if the rectangle was blurred when adjusting the glowRadius + property. + + \table + \header + \li Output examples with different cornerRadius values + \li + \li + \row + \li \image RectangularGlow_cornerRadius1.png + \li \image RectangularGlow_cornerRadius2.png + \li \image RectangularGlow_cornerRadius3.png + \row + \li \b { cornerRadius: 0 } + \li \b { cornerRadius: 25 } + \li \b { cornerRadius: 50 } + \row + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \li \l glowRadius: 20 + \row + \li \l spread: 0 + \li \l spread: 0 + \li \l spread: 0 + \row + \li \l color: #ffffff + \li \l color: #ffffff + \li \l color: #ffffff + \endtable + */ + property real cornerRadius: glowRadius + + /* + This property allows the effect output pixels to be cached in order to + improve the rendering performance. + + Every time the source or effect properties are changed, the pixels in + the cache must be updated. Memory consumption is increased, because an + extra buffer of memory is required for storing the effect output. + + It is recommended to disable the cache when the source or the effect + properties are animated. + + By default, the property is set to \c false. + */ + property bool cached: false + + ShaderEffectSource { + id: cacheItem + anchors.fill: shaderItem + visible: rootItem.cached + smooth: true + sourceItem: shaderItem + live: true + hideSource: visible + } + + ShaderEffect { + id: shaderItem + + x: (parent.width - width) / 2.0 + y: (parent.height - height) / 2.0 + width: parent.width + rootItem.glowRadius * 2 + cornerRadius * 2 + height: parent.height + rootItem.glowRadius * 2 + cornerRadius * 2 + + function clampedCornerRadius() { + var maxCornerRadius = Math.min(rootItem.width, rootItem.height) / 2 + rootItem.glowRadius; + return Math.max(0, Math.min(rootItem.cornerRadius, maxCornerRadius)) + } + + property color color: rootItem.color + property real inverseSpread: 1.0 - rootItem.spread + property real relativeSizeX: ((inverseSpread * inverseSpread) * rootItem.glowRadius + cornerRadius * 2.0) / width + property real relativeSizeY: relativeSizeX * (width / height) + property real spread: rootItem.spread / 2.0 + property real cornerRadius: clampedCornerRadius() + + fragmentShader: "qrc:/qt-project.org/imports/QtQuick/Controls/Material/shaders/RectangularGlow.frag" + } +} diff --git a/src/quickcontrols/material/impl/SliderHandle.qml b/src/quickcontrols/material/impl/SliderHandle.qml new file mode 100644 index 0000000000..ca7912b91d --- /dev/null +++ b/src/quickcontrols/material/impl/SliderHandle.qml @@ -0,0 +1,43 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Controls.Material.impl + +Item { + id: root + implicitWidth: initialSize + implicitHeight: initialSize + + property real value: 0 + property bool handleHasFocus: false + property bool handlePressed: false + property bool handleHovered: false + readonly property int initialSize: 13 + readonly property var control: parent + + Rectangle { + id: handleRect + width: parent.width + height: parent.height + radius: width / 2 + scale: root.handlePressed ? 1.5 : 1 + color: root.control.enabled ? root.control.Material.accentColor : root.control.Material.sliderDisabledColor + + Behavior on scale { + NumberAnimation { + duration: 250 + } + } + } + + Ripple { + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + width: 22; height: 22 + pressed: root.handlePressed + active: root.handlePressed || root.handleHasFocus || (enabled && root.handleHovered) + color: root.control.Material.highlightedRippleColor + } +} diff --git a/src/quickcontrols/material/impl/SwitchIndicator.qml b/src/quickcontrols/material/impl/SwitchIndicator.qml new file mode 100644 index 0000000000..e439bc31b7 --- /dev/null +++ b/src/quickcontrols/material/impl/SwitchIndicator.qml @@ -0,0 +1,49 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.Material +import QtQuick.Controls.Material.impl + +Item { + id: indicator + implicitWidth: 38 + implicitHeight: 32 + + property T.AbstractButton control + property alias handle: handle + + Material.elevation: 1 + + Rectangle { + width: parent.width + height: 14 + radius: height / 2 + y: parent.height / 2 - height / 2 + color: indicator.control.enabled ? (indicator.control.checked ? indicator.control.Material.switchCheckedTrackColor : indicator.control.Material.switchUncheckedTrackColor) + : indicator.control.Material.switchDisabledTrackColor + } + + Rectangle { + id: handle + x: Math.max(0, Math.min(parent.width - width, indicator.control.visualPosition * parent.width - (width / 2))) + y: (parent.height - height) / 2 + width: 20 + height: 20 + radius: width / 2 + color: indicator.control.enabled ? (indicator.control.checked ? indicator.control.Material.switchCheckedHandleColor : indicator.control.Material.switchUncheckedHandleColor) + : indicator.control.Material.switchDisabledHandleColor + + Behavior on x { + enabled: !indicator.control.pressed + SmoothedAnimation { + duration: 300 + } + } + layer.enabled: indicator.Material.elevation > 0 + layer.effect: ElevationEffect { + elevation: indicator.Material.elevation + } + } +} diff --git a/src/quickcontrols/material/impl/qquickmaterialbusyindicator.cpp b/src/quickcontrols/material/impl/qquickmaterialbusyindicator.cpp new file mode 100644 index 0000000000..4d5b4e13e7 --- /dev/null +++ b/src/quickcontrols/material/impl/qquickmaterialbusyindicator.cpp @@ -0,0 +1,214 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquickmaterialbusyindicator_p.h" + +#include <QtCore/qmath.h> +#include <QtCore/qeasingcurve.h> +#include <QtGui/qpainter.h> +#include <QtQuick/qsgimagenode.h> +#include <QtQuick/qquickwindow.h> +#include <QtQuickControls2Impl/private/qquickanimatednode_p.h> + +QT_BEGIN_NAMESPACE + +/* + Relevant Android code: + + - core/res/res/anim/progress_indeterminate_rotation_material.xml contains + the rotation animation data. + - core/res/res/anim/progress_indeterminate_material.xml contains the trim + animation data. + - core/res/res/interpolator/trim_start_interpolator.xml and + core/res/res/interpolator/trim_end_interpolator.xml contain the start + and end trim path interpolators. + - addCommand() in core/java/android/util/PathParser.java has a list of the + different path commands available. +*/ + +static const int SpanAnimationDuration = 700; +static const int RotationAnimationDuration = SpanAnimationDuration * 6; +static const int TargetRotation = 720; +static const int OneDegree = 16; +static const qreal MinSweepSpan = 10 * OneDegree; +static const qreal MaxSweepSpan = 300 * OneDegree; + +class QQuickMaterialBusyIndicatorNode : public QQuickAnimatedNode +{ +public: + QQuickMaterialBusyIndicatorNode(QQuickMaterialBusyIndicator *item); + + void sync(QQuickItem *item) override; + +protected: + void updateCurrentTime(int time) override; + +private: + int m_lastStartAngle = 0; + int m_lastEndAngle = 0; + qreal m_width = 0; + qreal m_height = 0; + qreal m_devicePixelRatio = 1; + QColor m_color; +}; + +QQuickMaterialBusyIndicatorNode::QQuickMaterialBusyIndicatorNode(QQuickMaterialBusyIndicator *item) + : QQuickAnimatedNode(item) +{ + setLoopCount(Infinite); + setCurrentTime(item->elapsed()); + setDuration(RotationAnimationDuration); + + QSGImageNode *textureNode = item->window()->createImageNode(); + textureNode->setOwnsTexture(true); + appendChildNode(textureNode); + + // A texture seems to be required here, but we don't have one yet, as we haven't drawn anything, + // so just use a blank image. + QImage blankImage(item->width(), item->height(), QImage::Format_ARGB32_Premultiplied); + blankImage.fill(Qt::transparent); + textureNode->setTexture(item->window()->createTextureFromImage(blankImage)); +} + +void QQuickMaterialBusyIndicatorNode::updateCurrentTime(int time) +{ + const qreal w = m_width; + const qreal h = m_height; + const qreal size = qMin(w, h); + const qreal dx = (w - size) / 2; + const qreal dy = (h - size) / 2; + + QImage image(size * m_devicePixelRatio, size * m_devicePixelRatio, QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + + QPainter painter(&image); + painter.setRenderHint(QPainter::Antialiasing); + + QPen pen; + QSGImageNode *textureNode = static_cast<QSGImageNode *>(firstChild()); + pen.setColor(m_color); + pen.setWidth(qCeil(size / 12) * m_devicePixelRatio); + painter.setPen(pen); + + const qreal percentageComplete = time / qreal(RotationAnimationDuration); + const qreal spanPercentageComplete = (time % SpanAnimationDuration) / qreal(SpanAnimationDuration); + const int iteration = time / SpanAnimationDuration; + int startAngle = 0; + int endAngle = 0; + + if (iteration % 2 == 0) { + if (m_lastStartAngle > 360 * OneDegree) + m_lastStartAngle -= 360 * OneDegree; + + // The start angle is only affected by the rotation animation for the "grow" phase. + startAngle = m_lastStartAngle; + QEasingCurve angleCurve(QEasingCurve::OutQuad); + const qreal percentage = angleCurve.valueForProgress(spanPercentageComplete); + endAngle = m_lastStartAngle + MinSweepSpan + percentage * (MaxSweepSpan - MinSweepSpan); + m_lastEndAngle = endAngle; + } else { + // Both the start angle *and* the span are affected by the "shrink" phase. + QEasingCurve angleCurve(QEasingCurve::InQuad); + const qreal percentage = angleCurve.valueForProgress(spanPercentageComplete); + startAngle = m_lastEndAngle - MaxSweepSpan + percentage * (MaxSweepSpan - MinSweepSpan); + endAngle = m_lastEndAngle; + m_lastStartAngle = startAngle; + } + + const int halfPen = pen.width() / 2; + const QRectF arcBounds = QRectF(halfPen, halfPen, + m_devicePixelRatio * size - pen.width(), + m_devicePixelRatio * size - pen.width()); + // The current angle of the rotation animation. + const qreal rotation = OneDegree * percentageComplete * -TargetRotation; + startAngle -= rotation; + endAngle -= rotation; + const int angleSpan = endAngle - startAngle; + painter.drawArc(arcBounds, -startAngle, -angleSpan); + painter.end(); + + textureNode->setRect(QRectF(dx, dy, size, size)); + textureNode->setTexture(window()->createTextureFromImage(image)); +} + +void QQuickMaterialBusyIndicatorNode::sync(QQuickItem *item) +{ + QQuickMaterialBusyIndicator *indicator = static_cast<QQuickMaterialBusyIndicator *>(item); + m_color = indicator->color(); + m_width = indicator->width(); + m_height = indicator->height(); + m_devicePixelRatio = indicator->window()->effectiveDevicePixelRatio(); +} + +QQuickMaterialBusyIndicator::QQuickMaterialBusyIndicator(QQuickItem *parent) : + QQuickItem(parent) +{ + setFlag(ItemHasContents); +} + +QColor QQuickMaterialBusyIndicator::color() const +{ + return m_color; +} + +void QQuickMaterialBusyIndicator::setColor(const QColor &color) +{ + if (m_color == color) + return; + + m_color = color; + update(); +} + +bool QQuickMaterialBusyIndicator::isRunning() const +{ + return isVisible(); +} + +void QQuickMaterialBusyIndicator::setRunning(bool running) +{ + if (running) + setVisible(true); +} + +int QQuickMaterialBusyIndicator::elapsed() const +{ + return m_elapsed; +} + +void QQuickMaterialBusyIndicator::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) +{ + QQuickItem::itemChange(change, data); + switch (change) { + case ItemOpacityHasChanged: + if (qFuzzyIsNull(data.realValue)) + setVisible(false); + break; + case ItemVisibleHasChanged: + update(); + break; + default: + break; + } +} + +QSGNode *QQuickMaterialBusyIndicator::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) +{ + QQuickMaterialBusyIndicatorNode *node = static_cast<QQuickMaterialBusyIndicatorNode *>(oldNode); + if (isRunning() && width() > 0 && height() > 0) { + if (!node) { + node = new QQuickMaterialBusyIndicatorNode(this); + node->start(); + } + node->sync(this); + } else { + m_elapsed = node ? node->currentTime() : 0; + delete node; + node = nullptr; + } + return node; +} + +QT_END_NAMESPACE + +#include "moc_qquickmaterialbusyindicator_p.cpp" diff --git a/src/quickcontrols/material/impl/qquickmaterialbusyindicator_p.h b/src/quickcontrols/material/impl/qquickmaterialbusyindicator_p.h new file mode 100644 index 0000000000..b6bbd925c7 --- /dev/null +++ b/src/quickcontrols/material/impl/qquickmaterialbusyindicator_p.h @@ -0,0 +1,56 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUICKMATERIALBUSYINDICATOR_P_H +#define QQUICKMATERIALBUSYINDICATOR_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 <QtGui/qcolor.h> +#include <QtQuick/qquickitem.h> +#include <QtCore/private/qglobal_p.h> + +QT_BEGIN_NAMESPACE + +class QQuickMaterialBusyIndicator : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor FINAL) + Q_PROPERTY(bool running READ isRunning WRITE setRunning FINAL) + QML_NAMED_ELEMENT(BusyIndicatorImpl) + QML_ADDED_IN_VERSION(2, 0) + +public: + explicit QQuickMaterialBusyIndicator(QQuickItem *parent = nullptr); + + QColor color() const; + void setColor(const QColor &color); + + bool isRunning() const; + void setRunning(bool running); + + int elapsed() const; + +protected: + void itemChange(ItemChange change, const ItemChangeData &data) override; + QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override; + +private: + int m_elapsed = 0; + QColor m_color = Qt::black; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickMaterialBusyIndicator) + +#endif // QQUICKMATERIALBUSYINDICATOR_P_H diff --git a/src/quickcontrols/material/impl/qquickmaterialprogressbar.cpp b/src/quickcontrols/material/impl/qquickmaterialprogressbar.cpp new file mode 100644 index 0000000000..67c9f614f5 --- /dev/null +++ b/src/quickcontrols/material/impl/qquickmaterialprogressbar.cpp @@ -0,0 +1,214 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquickmaterialprogressbar_p.h" + +#include <QtCore/qmath.h> +#include <QtCore/qeasingcurve.h> +#include <QtQuick/private/qquickitem_p.h> +#include <QtQuick/private/qsgadaptationlayer_p.h> +#include <QtQuick/qsgrectanglenode.h> +#include <QtQuick/qsgimagenode.h> +#include <QtQuick/qquickwindow.h> +#include <QtQuickControls2Impl/private/qquickanimatednode_p.h> + +QT_BEGIN_NAMESPACE + +static const int PauseDuration = 520; +static const int SlideDuration = 1240; +static const int TotalDuration = SlideDuration + PauseDuration; + +class QQuickMaterialProgressBarNode : public QQuickAnimatedNode +{ +public: + QQuickMaterialProgressBarNode(QQuickMaterialProgressBar *item); + + void updateCurrentTime(int time) override; + void sync(QQuickItem *item) override; + +private: + void moveNode(QSGTransformNode *node, const QRectF &geometry, qreal progress); + + bool m_indeterminate = false; + QEasingCurve m_easing = QEasingCurve::OutCubic; +}; + +QQuickMaterialProgressBarNode::QQuickMaterialProgressBarNode(QQuickMaterialProgressBar *item) + : QQuickAnimatedNode(item) +{ + setLoopCount(Infinite); + setDuration(TotalDuration); +} + +void QQuickMaterialProgressBarNode::updateCurrentTime(int time) +{ + QSGRectangleNode *geometryNode = static_cast<QSGRectangleNode *>(firstChild()); + Q_ASSERT(geometryNode->type() == QSGNode::GeometryNodeType); + const QRectF geometry = geometryNode->rect(); + + QSGTransformNode *firstNode = static_cast<QSGTransformNode *>(geometryNode->firstChild()); + if (firstNode) { + Q_ASSERT(firstNode->type() == QSGNode::TransformNodeType); + + const qreal progress = qMin<qreal>(1.0, static_cast<qreal>(time) / SlideDuration); + moveNode(static_cast<QSGTransformNode *>(firstNode), geometry, progress); + } + + QSGTransformNode *secondNode = static_cast<QSGTransformNode *>(geometryNode->lastChild()); + if (secondNode) { + Q_ASSERT(secondNode->type() == QSGNode::TransformNodeType); + + const qreal progress = qMax<qreal>(0.0, static_cast<qreal>(time - PauseDuration) / SlideDuration); + moveNode(static_cast<QSGTransformNode *>(secondNode), geometry, progress); + } +} + +void QQuickMaterialProgressBarNode::sync(QQuickItem *item) +{ + QQuickMaterialProgressBar *bar = static_cast<QQuickMaterialProgressBar *>(item); + if (m_indeterminate != bar->isIndeterminate()) { + m_indeterminate = bar->isIndeterminate(); + if (m_indeterminate) + start(); + else + stop(); + } + + QQuickItemPrivate *d = QQuickItemPrivate::get(item); + + QRectF bounds = item->boundingRect(); + bounds.setHeight(item->implicitHeight()); + bounds.moveTop((item->height() - bounds.height()) / 2.0); + + QSGRectangleNode *geometryNode = static_cast<QSGRectangleNode *>(firstChild()); + if (!geometryNode) { + geometryNode = item->window()->createRectangleNode(); + geometryNode->setColor(Qt::transparent); + appendChildNode(geometryNode); + } + geometryNode->setRect(bounds); + + const int count = m_indeterminate ? 2 : 1; + const qreal w = m_indeterminate ? 0 : bar->progress() * item->width(); + const QRectF rect(0, bounds.y(), w, bounds.height()); + + QSGNode *transformNode = geometryNode->firstChild(); + for (int i = 0; i < count; ++i) { + if (!transformNode) { + transformNode = new QSGTransformNode; + geometryNode->appendChildNode(transformNode); + + QSGInternalRectangleNode *rectNode = d->sceneGraphContext()->createInternalRectangleNode(); + rectNode->setAntialiasing(true); + transformNode->appendChildNode(rectNode); + } + Q_ASSERT(transformNode->type() == QSGNode::TransformNodeType); + static_cast<QSGTransformNode *>(transformNode)->setMatrix(QMatrix4x4()); + + QSGInternalRectangleNode *rectNode = static_cast<QSGInternalRectangleNode *>(transformNode->firstChild()); + Q_ASSERT(rectNode->type() == QSGNode::GeometryNodeType); + + rectNode->setRect(rect); + rectNode->setColor(bar->color()); + rectNode->update(); + + transformNode = transformNode->nextSibling(); + } + + while (transformNode) { + QSGNode *nextSibling = transformNode->nextSibling(); + delete transformNode; + transformNode = nextSibling; + } +} + +void QQuickMaterialProgressBarNode::moveNode(QSGTransformNode *transformNode, const QRectF &geometry, qreal progress) +{ + const qreal value = m_easing.valueForProgress(progress); + const qreal x = value * geometry.width(); + + QMatrix4x4 matrix; + matrix.translate(x, 0); + transformNode->setMatrix(matrix); + + QSGInternalRectangleNode *rectNode = static_cast<QSGInternalRectangleNode *>(transformNode->firstChild()); + Q_ASSERT(rectNode->type() == QSGNode::GeometryNodeType); + + QRectF r = geometry; + r.setWidth(value * (geometry.width() - x)); + rectNode->setRect(r); + rectNode->update(); +} + +QQuickMaterialProgressBar::QQuickMaterialProgressBar(QQuickItem *parent) + : QQuickItem(parent) +{ + setFlag(ItemHasContents); +} + +QColor QQuickMaterialProgressBar::color() const +{ + return m_color; +} + +void QQuickMaterialProgressBar::setColor(const QColor &color) +{ + if (color == m_color) + return; + + m_color = color; + update(); +} + +qreal QQuickMaterialProgressBar::progress() const +{ + return m_progress; +} + +void QQuickMaterialProgressBar::setProgress(qreal progress) +{ + if (progress == m_progress) + return; + + m_progress = progress; + update(); +} + +bool QQuickMaterialProgressBar::isIndeterminate() const +{ + return m_indeterminate; +} + +void QQuickMaterialProgressBar::setIndeterminate(bool indeterminate) +{ + if (indeterminate == m_indeterminate) + return; + + m_indeterminate = indeterminate; + update(); +} + +void QQuickMaterialProgressBar::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) +{ + QQuickItem::itemChange(change, data); + if (change == ItemVisibleHasChanged) + update(); +} + +QSGNode *QQuickMaterialProgressBar::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *) +{ + QQuickMaterialProgressBarNode *node = static_cast<QQuickMaterialProgressBarNode *>(oldNode); + if (isVisible() && width() > 0 && height() > 0) { + if (!node) + node = new QQuickMaterialProgressBarNode(this); + node->sync(this); + } else { + delete node; + node = nullptr; + } + return node; +} + +QT_END_NAMESPACE + +#include "moc_qquickmaterialprogressbar_p.cpp" diff --git a/src/quickcontrols/material/impl/qquickmaterialprogressbar_p.h b/src/quickcontrols/material/impl/qquickmaterialprogressbar_p.h new file mode 100644 index 0000000000..d250c2f76d --- /dev/null +++ b/src/quickcontrols/material/impl/qquickmaterialprogressbar_p.h @@ -0,0 +1,59 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUICKMATERIALPROGRESSBAR_P_H +#define QQUICKMATERIALPROGRESSBAR_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 <QtGui/qcolor.h> +#include <QtQuick/qquickitem.h> +#include <QtCore/private/qglobal_p.h> + +QT_BEGIN_NAMESPACE + +class QQuickMaterialProgressBar : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor FINAL) + Q_PROPERTY(qreal progress READ progress WRITE setProgress FINAL) + Q_PROPERTY(bool indeterminate READ isIndeterminate WRITE setIndeterminate FINAL) + QML_NAMED_ELEMENT(ProgressBarImpl) + QML_ADDED_IN_VERSION(2, 0) + +public: + explicit QQuickMaterialProgressBar(QQuickItem *parent = nullptr); + + QColor color() const; + void setColor(const QColor &color); + + qreal progress() const; + void setProgress(qreal progress); + + bool isIndeterminate() const; + void setIndeterminate(bool indeterminate); + +protected: + void itemChange(ItemChange change, const ItemChangeData &data) override; + QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override; + +private: + QColor m_color = Qt::black; + qreal m_progress = 0.0; + bool m_indeterminate = false; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickMaterialProgressBar) + +#endif // QQUICKMATERIALPROGRESSBAR_P_H diff --git a/src/quickcontrols/material/impl/qquickmaterialripple.cpp b/src/quickcontrols/material/impl/qquickmaterialripple.cpp new file mode 100644 index 0000000000..04e171f190 --- /dev/null +++ b/src/quickcontrols/material/impl/qquickmaterialripple.cpp @@ -0,0 +1,411 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquickmaterialripple_p.h" + +#include <QtCore/qmath.h> +#include <QtQuick/private/qquickitem_p.h> +#include <QtQuick/private/qsgadaptationlayer_p.h> +#include <QtQuickControls2Impl/private/qquickanimatednode_p.h> +#include <QtQuickTemplates2/private/qquickabstractbutton_p.h> +#include <QtQuickTemplates2/private/qquickabstractbutton_p_p.h> + +QT_BEGIN_NAMESPACE + +namespace { + enum WavePhase { WaveEnter, WaveExit }; +} + +static const int RIPPLE_ENTER_DELAY = 80; +static const int OPACITY_ENTER_DURATION_FAST = 120; +static const int WAVE_OPACITY_DECAY_DURATION = 333; +static const qreal WAVE_TOUCH_DOWN_ACCELERATION = 1024.0; + +class QQuickMaterialRippleWaveNode : public QQuickAnimatedNode +{ +public: + QQuickMaterialRippleWaveNode(QQuickMaterialRipple *ripple); + + void exit(); + void updateCurrentTime(int time) override; + void sync(QQuickItem *item) override; + +private: + qreal m_from = 0; + qreal m_to = 0; + qreal m_value = 0; + WavePhase m_phase = WaveEnter; + QPointF m_anchor; + QRectF m_bounds; +}; + +QQuickMaterialRippleWaveNode::QQuickMaterialRippleWaveNode(QQuickMaterialRipple *ripple) + : QQuickAnimatedNode(ripple) +{ + start(qRound(1000.0 * qSqrt(ripple->diameter() / 2.0 / WAVE_TOUCH_DOWN_ACCELERATION))); + + QSGOpacityNode *opacityNode = new QSGOpacityNode; + appendChildNode(opacityNode); + + QQuickItemPrivate *d = QQuickItemPrivate::get(ripple); + QSGInternalRectangleNode *rectNode = d->sceneGraphContext()->createInternalRectangleNode(); + rectNode->setAntialiasing(true); + opacityNode->appendChildNode(rectNode); +} + +void QQuickMaterialRippleWaveNode::exit() +{ + m_phase = WaveExit; + m_from = m_value; + setDuration(WAVE_OPACITY_DECAY_DURATION); + restart(); + connect(this, &QQuickAnimatedNode::stopped, this, &QObject::deleteLater); +} + +void QQuickMaterialRippleWaveNode::updateCurrentTime(int time) +{ + qreal p = 1.0; + if (duration() > 0) + p = time / static_cast<qreal>(duration()); + + m_value = m_from + (m_to - m_from) * p; + p = m_value / m_to; + + const qreal dx = (1.0 - p) * (m_anchor.x() - m_bounds.width() / 2); + const qreal dy = (1.0 - p) * (m_anchor.y() - m_bounds.height() / 2); + + QMatrix4x4 m; + m.translate(qRound((m_bounds.width() - m_value) / 2 + dx), + qRound((m_bounds.height() - m_value) / 2 + dy)); + setMatrix(m); + + QSGOpacityNode *opacityNode = static_cast<QSGOpacityNode *>(firstChild()); + Q_ASSERT(opacityNode->type() == QSGNode::OpacityNodeType); + qreal opacity = 1.0; + if (m_phase == WaveExit) + opacity -= static_cast<qreal>(time) / WAVE_OPACITY_DECAY_DURATION; + opacityNode->setOpacity(opacity); + + QSGInternalRectangleNode *rectNode = static_cast<QSGInternalRectangleNode *>(opacityNode->firstChild()); + Q_ASSERT(rectNode->type() == QSGNode::GeometryNodeType); + rectNode->setRect(QRectF(0, 0, m_value, m_value)); + rectNode->setRadius(m_value / 2); + rectNode->update(); +} + +void QQuickMaterialRippleWaveNode::sync(QQuickItem *item) +{ + QQuickMaterialRipple *ripple = static_cast<QQuickMaterialRipple *>(item); + m_to = ripple->diameter(); + m_anchor = ripple->anchorPoint(); + m_bounds = ripple->boundingRect(); + + QSGOpacityNode *opacityNode = static_cast<QSGOpacityNode *>(firstChild()); + Q_ASSERT(opacityNode->type() == QSGNode::OpacityNodeType); + + QSGInternalRectangleNode *rectNode = static_cast<QSGInternalRectangleNode *>(opacityNode->firstChild()); + Q_ASSERT(rectNode->type() == QSGNode::GeometryNodeType); + rectNode->setColor(ripple->color()); +} + +class QQuickMaterialRippleBackgroundNode : public QQuickAnimatedNode +{ + Q_OBJECT + +public: + QQuickMaterialRippleBackgroundNode(QQuickMaterialRipple *ripple); + + void updateCurrentTime(int time) override; + void sync(QQuickItem *item) override; + +private: + bool m_active = false; +}; + +QQuickMaterialRippleBackgroundNode::QQuickMaterialRippleBackgroundNode(QQuickMaterialRipple *ripple) + : QQuickAnimatedNode(ripple) +{ + setDuration(OPACITY_ENTER_DURATION_FAST); + + QSGOpacityNode *opacityNode = new QSGOpacityNode; + opacityNode->setOpacity(0.0); + appendChildNode(opacityNode); + + QQuickItemPrivate *d = QQuickItemPrivate::get(ripple); + QSGInternalRectangleNode *rectNode = d->sceneGraphContext()->createInternalRectangleNode(); + rectNode->setAntialiasing(true); + opacityNode->appendChildNode(rectNode); +} + +void QQuickMaterialRippleBackgroundNode::updateCurrentTime(int time) +{ + qreal opacity = time / static_cast<qreal>(duration()); + if (!m_active) + opacity = 1.0 - opacity; + + QSGOpacityNode *opacityNode = static_cast<QSGOpacityNode *>(firstChild()); + Q_ASSERT(opacityNode->type() == QSGNode::OpacityNodeType); + opacityNode->setOpacity(opacity); +} + +void QQuickMaterialRippleBackgroundNode::sync(QQuickItem *item) +{ + QQuickMaterialRipple *ripple = static_cast<QQuickMaterialRipple *>(item); + if (m_active != ripple->isActive()) { + m_active = ripple->isActive(); + setDuration(m_active ? OPACITY_ENTER_DURATION_FAST : WAVE_OPACITY_DECAY_DURATION); + restart(); + } + + QSGOpacityNode *opacityNode = static_cast<QSGOpacityNode *>(firstChild()); + Q_ASSERT(opacityNode->type() == QSGNode::OpacityNodeType); + + QSGInternalRectangleNode *rectNode = static_cast<QSGInternalRectangleNode *>(opacityNode->firstChild()); + Q_ASSERT(rectNode->type() == QSGNode::GeometryNodeType); + + const qreal w = ripple->width(); + const qreal h = ripple->height(); + const qreal sz = qSqrt(w * w + h * h); + + QMatrix4x4 matrix; + if (qFuzzyIsNull(ripple->clipRadius())) { + matrix.translate(qRound((w - sz) / 2), qRound((h - sz) / 2)); + rectNode->setRect(QRectF(0, 0, sz, sz)); + rectNode->setRadius(sz / 2); + } else { + rectNode->setRect(QRectF(0, 0, w, h)); + rectNode->setRadius(ripple->clipRadius()); + } + + setMatrix(matrix); + rectNode->setColor(ripple->color()); + rectNode->update(); +} + +QQuickMaterialRipple::QQuickMaterialRipple(QQuickItem *parent) + : QQuickItem(parent) +{ + setFlag(ItemHasContents); +} + +bool QQuickMaterialRipple::isActive() const +{ + return m_active; +} + +void QQuickMaterialRipple::setActive(bool active) +{ + if (active == m_active) + return; + + m_active = active; + update(); +} + +QColor QQuickMaterialRipple::color() const +{ + return m_color; +} + +void QQuickMaterialRipple::setColor(const QColor &color) +{ + if (m_color == color) + return; + + m_color = color; + update(); +} + +qreal QQuickMaterialRipple::clipRadius() const +{ + return m_clipRadius; +} + +void QQuickMaterialRipple::setClipRadius(qreal radius) +{ + if (qFuzzyCompare(m_clipRadius, radius)) + return; + + m_clipRadius = radius; + setClip(!qFuzzyIsNull(radius)); + update(); +} + +bool QQuickMaterialRipple::isPressed() const +{ + return m_pressed; +} + +void QQuickMaterialRipple::setPressed(bool pressed) +{ + if (pressed == m_pressed) + return; + + m_pressed = pressed; + + if (!isEnabled()) { + exitWave(); + return; + } + + if (pressed) { + if (m_trigger == Press) + prepareWave(); + else + exitWave(); + } else { + if (m_trigger == Release) + enterWave(); + else + exitWave(); + } +} + +QQuickMaterialRipple::Trigger QQuickMaterialRipple::trigger() const +{ + return m_trigger; +} + +void QQuickMaterialRipple::setTrigger(Trigger trigger) +{ + m_trigger = trigger; +} + +QPointF QQuickMaterialRipple::anchorPoint() const +{ + const QRectF bounds = boundingRect(); + const QPointF center = bounds.center(); + if (!m_anchor) + return center; + + QPointF anchorPoint = bounds.center(); + if (QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton *>(m_anchor)) + anchorPoint = QQuickAbstractButtonPrivate::get(button)->pressPoint; + anchorPoint = mapFromItem(m_anchor, anchorPoint); + + // calculate whether the anchor point is within the ripple circle bounds, + // that is, whether waves should start expanding from the anchor point + const qreal r = qSqrt(bounds.width() * bounds.width() + bounds.height() * bounds.height()) / 2; + if (QLineF(center, anchorPoint).length() < r) + return anchorPoint; + + // if the anchor point is outside the ripple circle bounds, start expanding + // from the intersection point of the ripple circle and a line from its center + // to the the anchor point + const qreal p = qAtan2(anchorPoint.y() - center.y(), anchorPoint.x() - center.x()); + return QPointF(center.x() + r * qCos(p), center.y() + r * qSin(p)); +} + +QQuickItem *QQuickMaterialRipple::anchor() const +{ + return m_anchor; +} + +void QQuickMaterialRipple::setAnchor(QQuickItem *item) +{ + m_anchor = item; +} + +qreal QQuickMaterialRipple::diameter() const +{ + const qreal w = width(); + const qreal h = height(); + return qSqrt(w * w + h * h); +} + +void QQuickMaterialRipple::itemChange(ItemChange change, const ItemChangeData &data) +{ + QQuickItem::itemChange(change, data); +} + +QSGNode *QQuickMaterialRipple::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) +{ + QQuickItemPrivate *d = QQuickItemPrivate::get(this); + QQuickDefaultClipNode *clipNode = d->clipNode(); + if (clipNode) { + // TODO: QTBUG-51894 + // clipNode->setRadius(m_clipRadius); + clipNode->setRect(boundingRect()); + clipNode->update(); + } + + QSGNode *container = oldNode; + if (!container) + container = new QSGNode; + + QQuickMaterialRippleBackgroundNode *backgroundNode = static_cast<QQuickMaterialRippleBackgroundNode *>(container->firstChild()); + if (!backgroundNode) { + backgroundNode = new QQuickMaterialRippleBackgroundNode(this); + backgroundNode->setObjectName(objectName()); + container->appendChildNode(backgroundNode); + } + backgroundNode->sync(this); + + // enter new waves + int i = m_waves; + QQuickMaterialRippleWaveNode *enterNode = static_cast<QQuickMaterialRippleWaveNode *>(backgroundNode->nextSibling()); + while (i-- > 0) { + if (!enterNode) { + enterNode = new QQuickMaterialRippleWaveNode(this); + container->appendChildNode(enterNode); + } + enterNode->sync(this); + enterNode = static_cast<QQuickMaterialRippleWaveNode *>(enterNode->nextSibling()); + } + + // exit old waves + int j = container->childCount() - 1 - m_waves; + while (j-- > 0) { + QQuickMaterialRippleWaveNode *exitNode = static_cast<QQuickMaterialRippleWaveNode *>(backgroundNode->nextSibling()); + if (exitNode) { + exitNode->exit(); + exitNode->sync(this); + } + } + + return container; +} + +void QQuickMaterialRipple::timerEvent(QTimerEvent *event) +{ + QQuickItem::timerEvent(event); + + if (event->timerId() == m_enterDelay) + enterWave(); +} + +void QQuickMaterialRipple::prepareWave() +{ + if (m_enterDelay <= 0) + m_enterDelay = startTimer(RIPPLE_ENTER_DELAY); +} + +void QQuickMaterialRipple::enterWave() +{ + if (m_enterDelay > 0) { + killTimer(m_enterDelay); + m_enterDelay = 0; + } + + ++m_waves; + update(); +} + +void QQuickMaterialRipple::exitWave() +{ + if (m_enterDelay > 0) { + killTimer(m_enterDelay); + m_enterDelay = 0; + } + + if (m_waves > 0) { + --m_waves; + update(); + } +} + +QT_END_NAMESPACE + +#include "moc_qquickmaterialripple_p.cpp" + +#include "qquickmaterialripple.moc" diff --git a/src/quickcontrols/material/impl/qquickmaterialripple_p.h b/src/quickcontrols/material/impl/qquickmaterialripple_p.h new file mode 100644 index 0000000000..cdaaf13b1a --- /dev/null +++ b/src/quickcontrols/material/impl/qquickmaterialripple_p.h @@ -0,0 +1,88 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUICKMATERIALRIPPLE_P_H +#define QQUICKMATERIALRIPPLE_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 <QtQuick/qquickitem.h> +#include <QtGui/qcolor.h> +#include <QtCore/private/qglobal_p.h> + +QT_BEGIN_NAMESPACE + +class QQuickMaterialRipple : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor FINAL) + Q_PROPERTY(qreal clipRadius READ clipRadius WRITE setClipRadius FINAL) + Q_PROPERTY(bool pressed READ isPressed WRITE setPressed FINAL) + Q_PROPERTY(bool active READ isActive WRITE setActive FINAL) + Q_PROPERTY(QQuickItem *anchor READ anchor WRITE setAnchor FINAL) + Q_PROPERTY(Trigger trigger READ trigger WRITE setTrigger FINAL) + QML_NAMED_ELEMENT(Ripple) + QML_ADDED_IN_VERSION(2, 0) + +public: + QQuickMaterialRipple(QQuickItem *parent = nullptr); + + QColor color() const; + void setColor(const QColor &color); + + qreal clipRadius() const; + void setClipRadius(qreal radius); + + bool isActive() const; + void setActive(bool active); + + bool isPressed() const; + void setPressed(bool pressed); + + enum Trigger { Press, Release }; + Q_ENUM (Trigger) + + Trigger trigger() const; + void setTrigger(Trigger trigger); + + QPointF anchorPoint() const; + + QQuickItem *anchor() const; + void setAnchor(QQuickItem *anchor); + + qreal diameter() const; + +protected: + void itemChange(ItemChange change, const ItemChangeData &data) override; + QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override; + void timerEvent(QTimerEvent *event) override; + + void prepareWave(); + void enterWave(); + void exitWave(); + +private: + bool m_active = false; + bool m_pressed = false; + int m_waves = 0; + int m_enterDelay = 0; + Trigger m_trigger = Press; + qreal m_clipRadius = 0.0; + QColor m_color; + QQuickItem *m_anchor = nullptr; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickMaterialRipple) + +#endif // QQUICKMATERIALRIPPLE_P_H |