diff options
Diffstat (limited to 'tests/auto/quick/doc/how-tos/how-to-qml/time-picker')
5 files changed, 627 insertions, 0 deletions
diff --git a/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimeComponentLabel.qml b/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimeComponentLabel.qml new file mode 100644 index 0000000000..6b1653bde5 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimeComponentLabel.qml @@ -0,0 +1,29 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Material + +Label { + id: root + fontSizeMode: Label.Fit + horizontalAlignment: Label.AlignHCenter + verticalAlignment: Label.AlignVCenter + + Material.foreground: Material.theme === Material.Light + ? Material.color(Material.Indigo, !dim ? Material.Shade500 : Material.Shade100) + : Material.color(Material.Indigo, dim ? Material.Shade300 : Material.Shade100) + + Layout.fillHeight: true + + property bool dim: false + property alias interactive: tapHandler.enabled + + signal tapped + + TapHandler { + id: tapHandler + onTapped: root.tapped() + } +} 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] diff --git a/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerDialog.qml b/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerDialog.qml new file mode 100644 index 0000000000..fa02234eb8 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerDialog.qml @@ -0,0 +1,88 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//! [file] +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Material + +Dialog { + id: root + standardButtons: Dialog.Ok | Dialog.Cancel + closePolicy: Dialog.NoAutoClose + // We don't want so much space between the picker and dialog buttons. + bottomPadding: 8 + + property int hours: 12 + property int minutes: 0 + property alias is24Hour: timePicker.is24Hour + + property int __initialHours + property int __initialMinutes + + signal timeAccepted + signal timeRejected + + function openWithMode(mode) { + timePicker.openWith(mode !== undefined ? mode : TimePicker.Mode.Hours, hours, minutes) + + __initialHours = hours + __initialMinutes = minutes + + open() + } + + onAccepted: { + root.hours = timePicker.hours + root.minutes = timePicker.minutes + root.timeAccepted() + } + onRejected: { + hours = __initialHours + minutes = __initialMinutes + // Also reset the picker's time so that the onIs24HourChanged handler below works as expected. + timePicker.hours = __initialHours + timePicker.minutes = __initialMinutes + root.timeRejected() + } + + // If is24Hour changes programmatically (only while we're not open), + // make sure we adapt to any possible clamping it did in the transition from 24 hours to 12. + onIs24HourChanged: { + if (!opened) + root.hours = timePicker.hours + } + + ColumnLayout { + anchors.fill: parent + spacing: 12 + + TimePickerLabel { + id: timeLabel + // Use TimePicker's time, because that is updated live, whereas our values + // are only changed once we've been accepted. + time: new Date(1970, 1, 1, timePicker.hours, timePicker.minutes) + hoursActive: timePicker.mode === TimePicker.Mode.Hours + showAmPm: !timePicker.is24Hour + + Layout.fillWidth: true + // Push us down a bit so we're not so close to the top of the dialog. + Layout.topMargin: 8 + + onHoursSelected: timePicker.mode = TimePicker.Mode.Hours + onMinutesSelected: timePicker.mode = TimePicker.Mode.Minutes + } + + TimePicker { + id: timePicker + objectName: "timePicker" + // Our TapHandler may handle the click event on the Label if we don't do this, + // causing an hour to be inadvertently selected. + interactive: root.opened + + Layout.fillWidth: true + Layout.fillHeight: true + } + } +} +//! [file] diff --git a/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerLabel.qml b/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerLabel.qml new file mode 100644 index 0000000000..66b651c0c5 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerLabel.qml @@ -0,0 +1,126 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//! [file] +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Material + +Item { + id: root + // Use TextMetrics' boundingRect.width rather than a RowLayout's implicitWidth + // because the latter can cause TimePickerDialog to jump around when the label text changes. + implicitWidth: fullTextMetrics.boundingRect.width + amPmLayout.implicitWidth + 80 + implicitHeight: fullTextMetrics.boundingRect.height + + property var time + property bool am: true + property bool showAmPm: true + + property bool hoursActive: true + + property int fontPixelSize: Qt.application.font.pixelSize * 4 + + signal hoursSelected + signal minutesSelected + signal amSelected + signal pmSelected + + TextMetrics { + id: fullTextMetrics + font: hoursLabel.font + text: "99:99" + } + + TextMetrics { + id: hourTextMetrics + font.family: hoursLabel.font.family + font.pixelSize: hoursLabel.fontInfo.pixelSize + text: "99" + } + + TimeComponentLabel { + id: hoursLabel + objectName: "hoursLabel" + text: Qt.formatTime(root.time, "hh") + font.pixelSize: root.fontPixelSize + // Avoid any jumping around by using a dedicated TextMetrics object + // for our label too, and position ourselves based on its width. + x: colonLabel.x - hourTextMetrics.boundingRect.width - 4 + anchors.verticalCenter: parent.verticalCenter + dim: !root.hoursActive + + onTapped: root.hoursSelected() + } + + TimeComponentLabel { + id: colonLabel + text: ":" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: root.fontPixelSize + dim: true + } + + TimeComponentLabel { + id: minutesLabel + objectName: "minutesLabel" + text: Qt.formatTime(root.time, "mm") + font.pixelSize: root.fontPixelSize + anchors.left: colonLabel.right + anchors.leftMargin: 4 + anchors.verticalCenter: parent.verticalCenter + dim: root.hoursActive + + onTapped: root.minutesSelected() + } + + ColumnLayout { + id: amPmLayout + visible: root.showAmPm + // We also need to avoid the AM/PM label jumping around, + // so rather than anchor it to the right of the minutes label, + // we use colonLabel's horizontal center (which never changes), and fullTextMetrics. + anchors.left: colonLabel.horizontalCenter + anchors.leftMargin: fullTextMetrics.boundingRect.width / 2 + 12 + anchors.verticalCenter: minutesLabel.verticalCenter + + spacing: 0 + + ToolButton { + objectName: "amButton" + text: qsTr("AM") + autoExclusive: true + + Material.foreground: Material.color(Material.Indigo, + root.am ? Material.Shade500 : Material.Shade100) + + // Set the size explicitly to ensure that the buttons aren't too large. + // We also add 1 to the width because we want square ripple effects, not round. + Layout.preferredWidth: (implicitWidth * 0.7) + 1 + Layout.preferredHeight: (implicitHeight * 0.7) + + onClicked: { + root.am = true + root.amSelected() + } + } + ToolButton { + objectName: "pmButton" + text: qsTr("PM") + autoExclusive: true + + Material.foreground: Material.color(Material.Indigo, + !root.am ? Material.Shade500 : Material.Shade100) + + Layout.preferredWidth: (implicitWidth * 0.7) + 1 + Layout.preferredHeight: (implicitHeight * 0.7) + + onClicked: { + root.am = false + root.pmSelected() + } + } + } +} +//! [file] diff --git a/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerMain.qml b/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerMain.qml new file mode 100644 index 0000000000..c7c304613d --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerMain.qml @@ -0,0 +1,59 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//! [file] +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Material + +ApplicationWindow { + id: window + width: 600 + height: 600 + visible: true + title: qsTr("Time Picker Example") + + Material.theme: darkThemeSwitch.checked ? Material.Dark : Material.Light + + // Shows the selected time and opens the dialog. + TimeComponentLabel { + id: openDialogLabel + width: parent.width - 80 + anchors.centerIn: parent + font.pixelSize: Qt.application.font.pixelSize * 8 + renderTypeQuality: Text.VeryHighRenderTypeQuality + interactive: !timePickerDialog.opened + + text: Qt.formatTime(new Date(1970, 1, 1, timePickerDialog.hours, timePickerDialog.minutes), "hh:mm") + + onTapped: timePickerDialog.openWithMode(TimePicker.Mode.Hours) + } + + ColumnLayout { + // We always want the openDialogLabel to be centered in the window, not us. + // For that reason, we use anchors rather than putting the root items into a ColumnLayout. + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: openDialogLabel.bottom + anchors.topMargin: 24 + spacing: 12 + + Switch { + id: is24HourSwitch + text: qsTr("24 Hour") + checked: timePickerDialog.is24Hour + } + Switch { + id: darkThemeSwitch + text: qsTr("Dark") + } + } + + TimePickerDialog { + id: timePickerDialog + anchors.centerIn: parent + is24Hour: is24HourSwitch.checked + + onTimeAccepted: print("A time was chosen - do something here!") + } +} +//! [file] |