diff options
Diffstat (limited to 'tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePicker.qml')
-rw-r--r-- | tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePicker.qml | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePicker.qml b/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePicker.qml new file mode 100644 index 0000000000..edcf864784 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePicker.qml @@ -0,0 +1,325 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//! [file] +import QtQuick +import QtQuick.Controls.Material + +Item { + id: root + implicitWidth: 250 + implicitHeight: 250 + + enum Mode { + Hours, + Minutes + } + + property int mode: TimePicker.Mode.Hours + property int hours + property int minutes + property bool is24Hour + property bool interactive: true + + // The mode that the label delegates see, so that we can + // animate their opacity before their text changes. + property int __effectiveMode: TimePicker.Mode.Hours + // For 12 hour pickers, we can use 0 to 60 to represent all values. + property int __value: 0 + // For 24 hour pickers, we need to store this extra flag. + property bool __is24HourValueSelected + // How many values the arm should snap to at a time. + readonly property int __stepSize: __getStepSize(mode) + readonly property int __to: 60 + readonly property int __labelAngleStepSize: 360 / 12 + property bool __switchingModes + + // This signal could be used if TimePicker is used as a standalone component. + // It's emitted when the minute is selected. + signal accepted() + + // Convenience for setting each property individually, but also + // ensures that the selector arm is properly rotated when there is + // a programmatic change in hours or minutes but not mode. + function openWith(mode, hours, minutes) { + root.mode = mode + root.hours = hours + root.minutes = minutes + __updateAfterModeOrTimeChange() + } + + // Until QML gets private properties (QTBUG-11984), use the traditional + // double-underscore convention. + function __angleForValue(value: int): real { + return (value / __to) * 360 + } + + function __getStepSize(mode) { + return mode === TimePicker.Mode.Hours ? 5 : 1 + } + + function __updateAfterModeOrTimeChange() { + // We use a function for this rather than a binding, because we could be called before the + // __stepSize binding is evaluated. + if (mode === TimePicker.Mode.Hours) { + // modulo the hours value by __to because we want 60 (12) to be 0. + __value = hours * __getStepSize(mode) % __to + } else { + __value = minutes + } + + __is24HourValueSelected = mode === TimePicker.Mode.Hours && hours >= 13 + } + + onModeChanged: __updateAfterModeOrTimeChange() + + onIs24HourChanged: { + // Don't allow 24-hour values when we're not a 24-hour picker. + if (!is24Hour && hours > 12) + hours = 12 + } + + // Center dot. + Rectangle { + width: 6 + height: 6 + radius: width / 2 + color: Material.primary + anchors.centerIn: parent + z: 1 + } + + Rectangle { + id: contentContainer + objectName: "contentContainer" + width: Math.min(parent.width, parent.height) + height: width + radius: width / 2 + anchors.centerIn: parent + color: Material.theme === Material.Light ? "#eeeeee" : "#626262" + + // Animate this so that we don't need an intermediate parent item for the + // labels to animate the opacity of that instead. That item would be required + // because we don't want to change the opacity of the contentContainer Rectangle. + property real labelOpacity: 1 + + function updateValueAfterPressPointChange() { + const y1 = height / 2 + const x1 = width / 2 + const y2 = tapHandler.point.position.y + const x2 = tapHandler.point.position.x + const yDistance = y2 - y1 + const xDistance = x2 - x1 + const angle = Math.atan2(yDistance, xDistance) + + let angleInDegrees = (angle * (180 / Math.PI)) + 90.0 + if (angleInDegrees < 0) + angleInDegrees = 360 + angleInDegrees + + const normalisedAngle = angleInDegrees / 360.0 + const rawValue = normalisedAngle * __to + // Snap to each step. + const steppedValue = Math.round(rawValue / __stepSize) * __stepSize + root.__value = steppedValue + // Account for the area where the angle wraps around from 360 to 0, + // otherwise values from 59.5 to 59.999[...] will register as 60 instead of 0. + if (rawValue > __to - __stepSize / 2) + root.__value = 0 + + const distanceFromCenter = Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2)) + // Only allow selecting 24 hour values when it's in the correct mode. + root.__is24HourValueSelected = root.is24Hour && root.__effectiveMode === TimePicker.Mode.Hours + && distanceFromCenter < distanceFromCenterForLabels(true) + selectionIndicator.height * 0.5 + } + + // Returns the distance from our center at which a label should be centered over given is24Hour. + function distanceFromCenterForLabels(is24Hour) { + return contentContainer.radius - (is24Hour + ? selectionIndicator.height * 1.5 : selectionIndicator.height * 0.5) + } + + states: [ + State { + name: "hours" + when: root.mode === TimePicker.Mode.Hours + }, + State { + name: "minutes" + when: root.mode === TimePicker.Mode.Minutes + } + ] + + transitions: [ + Transition { + // When the picker isn't interactive (e.g. when a dialog is opening), + // we shouldn't animate the opacity of the labels, as it looks wrong, + // and should only happen when switching between modes while the + // picker was already visible. + enabled: root.interactive + + SequentialAnimation { + NumberAnimation { + target: contentContainer + property: "labelOpacity" + from: 1 + to: 0 + duration: 100 + } + + ScriptAction { + script: root.__effectiveMode = root.mode + } + + NumberAnimation { + target: contentContainer + property: "labelOpacity" + from: 0 + to: 1 + duration: 100 + } + } + }, + Transition { + enabled: !root.interactive + + // Since the transition above doesn't run when we're not interactive, + // we need to do the immediate property change here. + // See QTBUG-13268 for why we use a ScriptAction and not PropertyAction. + ScriptAction { + script: root.__effectiveMode = root.mode + } + } + + ] + + TapHandler { + id: tapHandler + gesturePolicy: TapHandler.ReleaseWithinBounds + // Don't allow input while switching modes, or a click on an hour could go through to a minute. + enabled: root.interactive && root.__effectiveMode === root.mode + + onPointChanged: { + if (pressed) { + // Don't call this when not pressed, as the position will be invalid. + contentContainer.updateValueAfterPressPointChange() + + // Update the value (like a "live" Slider) while the pointer position changes. + if (mode === TimePicker.Mode.Hours) { + root.hours = root.__value / root.__stepSize + + if (root.hours === 0) { + // A value of 0 (when it's not a 24-hour picker) is 12. + // When it is a 24-hour picker, it's 0. + if (!root.__is24HourValueSelected) + root.hours = 12 + } else if (root.__is24HourValueSelected) { + root.hours += 12 + } + } else { + root.minutes = root.__value + } + } else { + // Select the value that was chosen in the press code above. + if (mode === TimePicker.Mode.Hours) { + mode = TimePicker.Mode.Minutes + } else { + // Does nothing in our example, but could be used to hide the dialog + // if it didn't have an OK button to accept it. + root.accepted() + } + } + } + } + + // The line connecting the center dot to the selection indicator. + Rectangle { + id: selectionArm + objectName: "selectionArm" + width: 2 + height: contentContainer.distanceFromCenterForLabels(root.__is24HourValueSelected) + color: Material.primary + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.verticalCenter + rotation: root.__angleForValue(root.__value) + transformOrigin: Item.Bottom + antialiasing: true + + Rectangle { + id: selectionIndicator + objectName: "selectionIndicator" + width: 40 + height: 40 + radius: width / 2 + color: Material.primary + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.top + + Rectangle { + width: 4 + height: 4 + radius: width / 2 + color: Material.color(Material.Indigo, Material.Shade100) + anchors.centerIn: parent + // Only show the circle within the indicator between minute labels. + visible: root.__effectiveMode === TimePicker.Mode.Minutes + && root.__value % 5 !== 0 + } + } + } + + Repeater { + id: labelRepeater + model: root.mode === TimePicker.Mode.Hours && root.is24Hour ? 24 : 12 + delegate: Label { + id: labelDelegate + text: displayValue + font.pixelSize: Qt.application.font.pixelSize * (is24HourValue ? 0.85 : 1) + rotation: -rotationTransform.angle + opacity: contentContainer.labelOpacity + anchors.centerIn: parent + + // TODO: remove me - QTBUG-122679 + Component.onCompleted: print("created", labelDelegate, "at index", index) + Component.onDestruction: print("destroyed", labelDelegate, "at index", index) + + required property int index + // From 0 to 60. + readonly property int value: (index * 5) % root.__to + property int displayValue: root.__effectiveMode === TimePicker.Mode.Hours + ? index === 0 + ? 12 + : index === 12 + ? 0 + : index + : value + // The picker's current value can equal ours but we still might not be current - + // it depends on whether it's a 24 hour value or not. + readonly property bool current: root.__value === value && root.__is24HourValueSelected == is24HourValue + readonly property bool is24HourValue: index >= 12 + + Material.foreground: current + // When the selection arm is over us, invert our color so it's legible. + ? Material.color(Material.Indigo, Material.Shade100) + : root.Material.theme === Material.Light + ? is24HourValue ? "#686868" : "#484848" + : Material.color(Material.Indigo, is24HourValue ? Material.Shade300 : Material.Shade100) + + transform: [ + Translate { + // We're already centered in our parent, so we can use this function + // to determine our position, which doesn't need to be aware of our height - + // it just needs to tell us where our center position should be. + y: -contentContainer.distanceFromCenterForLabels(labelDelegate.is24HourValue) + }, + Rotation { + id: rotationTransform + angle: root.__angleForValue(labelDelegate.value) + origin.x: labelDelegate.width / 2 + origin.y: labelDelegate.height / 2 + } + ] + } + } + } +} +//! [file] |