aboutsummaryrefslogtreecommitdiffstats
path: root/tests/auto/quick/doc/how-tos/how-to-qml
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/quick/doc/how-tos/how-to-qml')
-rw-r--r--tests/auto/quick/doc/how-tos/how-to-qml/CMakeLists.txt33
-rw-r--r--tests/auto/quick/doc/how-tos/how-to-qml/active-focus-debugging/ActiveFocusDebuggingMain.qml25
-rw-r--r--tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimeComponentLabel.qml29
-rw-r--r--tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePicker.qml325
-rw-r--r--tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerDialog.qml88
-rw-r--r--tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerLabel.qml126
-rw-r--r--tests/auto/quick/doc/how-tos/how-to-qml/time-picker/TimePickerMain.qml59
-rw-r--r--tests/auto/quick/doc/how-tos/how-to-qml/tst_how-to-qml.cpp368
8 files changed, 1053 insertions, 0 deletions
diff --git a/tests/auto/quick/doc/how-tos/how-to-qml/CMakeLists.txt b/tests/auto/quick/doc/how-tos/how-to-qml/CMakeLists.txt
new file mode 100644
index 0000000000..bdf84439c8
--- /dev/null
+++ b/tests/auto/quick/doc/how-tos/how-to-qml/CMakeLists.txt
@@ -0,0 +1,33 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_how-to-qml LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+qt_internal_add_test(tst_how-to-qml
+ SOURCES
+ tst_how-to-qml.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::Quick
+ Qt::QuickPrivate
+ Qt::QuickControlsTestUtilsPrivate
+ Qt::QuickTemplates2Private
+)
+
+qt_policy(SET QTP0001 NEW)
+
+qt_add_qml_module(tst_how-to-qml
+ URI HowToQml
+ QML_FILES
+ active-focus-debugging/ActiveFocusDebuggingMain.qml
+ time-picker/TimeComponentLabel.qml
+ time-picker/TimePickerDialog.qml
+ time-picker/TimePickerMain.qml
+ time-picker/TimePickerLabel.qml
+ time-picker/TimePicker.qml
+)
diff --git a/tests/auto/quick/doc/how-tos/how-to-qml/active-focus-debugging/ActiveFocusDebuggingMain.qml b/tests/auto/quick/doc/how-tos/how-to-qml/active-focus-debugging/ActiveFocusDebuggingMain.qml
new file mode 100644
index 0000000000..b0f48cdb59
--- /dev/null
+++ b/tests/auto/quick/doc/how-tos/how-to-qml/active-focus-debugging/ActiveFocusDebuggingMain.qml
@@ -0,0 +1,25 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+//! [file]
+import QtQuick
+import QtQuick.Controls
+
+ApplicationWindow {
+ width: 400
+ height: 400
+ visible: true
+ title: qsTr("Active focus debugging example")
+
+ onActiveFocusItemChanged: print("activeFocusItem: " + activeFocusItem)
+
+ Row {
+ TextField {
+ objectName: "textField1"
+ }
+ TextField {
+ objectName: "textField2"
+ }
+ }
+}
+//! [file]
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]
diff --git a/tests/auto/quick/doc/how-tos/how-to-qml/tst_how-to-qml.cpp b/tests/auto/quick/doc/how-tos/how-to-qml/tst_how-to-qml.cpp
new file mode 100644
index 0000000000..66dcb208fc
--- /dev/null
+++ b/tests/auto/quick/doc/how-tos/how-to-qml/tst_how-to-qml.cpp
@@ -0,0 +1,368 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore/qregularexpression.h>
+#include <QtTest/QtTest>
+#include <QtQml/qqmlapplicationengine.h>
+#include <QtQuick/qquickitem.h>
+#include <QtQuick/qquickwindow.h>
+#include <QtQuick/private/qquickrepeater_p.h>
+#include <QtQuickTemplates2/private/qquickdialog_p.h>
+#include <QtQuickTemplates2/private/qquickdialogbuttonbox_p.h>
+#include <QtQuickTemplates2/private/qquicklabel_p.h>
+#include <QtQuickTemplates2/private/qquicktextfield_p.h>
+#include <QtQuickControlsTestUtils/private/controlstestutils_p.h>
+#include <QtQuickControlsTestUtils/private/dialogstestutils_p.h>
+
+QT_BEGIN_NAMESPACE
+
+using namespace QQuickVisualTestUtils;
+using namespace QQuickControlsTestUtils;
+using namespace QQuickDialogTestUtils;
+
+// Allows us to use test macros outside of test functions.
+#define RETURN_IF_FAILED(expression) \
+expression; \
+if (QTest::currentTestFailed()) \
+ return
+
+class tst_HowToQml : public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_HowToQml();
+
+private slots:
+ void init();
+ void activeFocusDebugging();
+ void timePicker();
+
+private:
+ QScopedPointer<QPointingDevice> touchScreen = QScopedPointer<QPointingDevice>(QTest::createTouchDevice());
+};
+
+tst_HowToQml::tst_HowToQml()
+{
+ qputenv("QML_NO_TOUCH_COMPRESSION", "1");
+}
+
+void tst_HowToQml::init()
+{
+// QTest::failOnWarning(QRegularExpression(QStringLiteral(".?")));
+}
+
+void tst_HowToQml::activeFocusDebugging()
+{
+ QQmlApplicationEngine engine;
+ engine.loadFromModule("HowToQml", "ActiveFocusDebuggingMain");
+ QCOMPARE(engine.rootObjects().size(), 1);
+
+ auto *window = qobject_cast<QQuickWindow*>(engine.rootObjects().at(0));
+ window->show();
+ QTest::ignoreMessage(QtDebugMsg, QRegularExpression("activeFocusItem: .*\"ActiveFocusDebuggingMain\""));
+ QVERIFY(QTest::qWaitForWindowActive(window));
+
+ QTest::ignoreMessage(QtDebugMsg, QRegularExpression("activeFocusItem: .*\"textField1\""));
+ auto *textField1 = window->findChild<QQuickTextField*>("textField1");
+ QVERIFY(textField1);
+ textField1->forceActiveFocus();
+ QVERIFY(textField1->hasActiveFocus());
+
+ QTest::ignoreMessage(QtDebugMsg, QRegularExpression("activeFocusItem: .*\"textField2\""));
+ auto *textField2 = window->findChild<QQuickTextField*>("textField2");
+ QVERIFY(textField2);
+ QTest::keyClick(window, Qt::Key_Tab);
+ QVERIFY(textField2->hasActiveFocus());
+}
+
+void tst_HowToQml::timePicker()
+{
+ QQmlApplicationEngine engine;
+ engine.loadFromModule("HowToQml", "TimePickerMain");
+ QCOMPARE(engine.rootObjects().size(), 1);
+
+ auto *window = qobject_cast<QQuickWindow*>(engine.rootObjects().at(0));
+ window->show();
+ QVERIFY(QTest::qWaitForWindowExposed(window));
+
+ auto *dialog = window->findChild<QQuickDialog *>();
+ QVERIFY(dialog);
+
+ auto *timePicker = dialog->findChild<QQuickItem *>("timePicker");
+ QVERIFY(timePicker);
+
+ auto *contentContainer = timePicker->findChild<QQuickItem *>("contentContainer");
+ QVERIFY(contentContainer);
+ const int contentRadius = contentContainer->property("radius").toReal();
+
+ auto *labelRepeater = timePicker->findChild<QQuickRepeater *>();
+ QVERIFY(labelRepeater);
+
+ auto *selectionArm = timePicker->findChild<QQuickItem *>("selectionArm");
+ QVERIFY(selectionArm);
+
+ auto *selectionIndicator = selectionArm->findChild<QQuickItem *>("selectionIndicator");
+ QVERIFY(selectionIndicator);
+ const int selectionIndicatorHeight = selectionIndicator->height();
+
+ auto angleForValue = [](int value) -> int {
+ return int((value / 60.0) * 360) % 360;
+ };
+
+ // Note that is24HourValue should be true if "value" is a 24-hour value,
+ // not if the picker's is24Hour property is true.
+ auto labelCenterPosForValue = [&](int value, bool is24HourValue = false) -> QPoint {
+ if (value < 0 || value > 60)
+ return {};
+
+ const qreal angle = angleForValue(value);
+
+ QTransform transform;
+ // Translate to the center.
+ transform.translate(contentRadius, contentRadius);
+ // Rotate to the correct angle.
+ transform.rotate(angle);
+ // Go outward.
+ const int labelDistance = is24HourValue ? selectionIndicatorHeight * 1.5 : selectionIndicatorHeight * 0.5;
+ transform.translate(0, -contentRadius + labelDistance);
+
+ const auto centerPos = transform.map(QPoint(0, 0));
+ return centerPos;
+ };
+
+ enum Mode {
+ Hours,
+ Minutes
+ };
+
+ const int valuesPerLabelStep = 5;
+ const bool TwelveHour = false;
+ const bool TwentyFourHour = true;
+
+ // Checks that all the labels are in their expected positions and that they have the correct text.
+ auto verifyLabels = [&](Mode expectedMode, bool is24Hour, int callerLineNumber) {
+ // When not in 24 hour mode, there are always 12 labels, regardless of whether it's showing hours or minutes.
+ const int expectedLabelCount = expectedMode == Hours && is24Hour ? 24 : 12;
+ QCOMPARE(labelRepeater->count(), expectedLabelCount);
+ for (int i = 0; i < expectedLabelCount; ++i) {
+ auto *labelDelegate = labelRepeater->itemAt(i);
+ QVERIFY2(labelDelegate, qPrintable(QString::fromLatin1("Expected valid label delegate item at index %1 (caller line %2)")
+ .arg(i).arg(callerLineNumber)));
+ // Use the waiting variant of the macro because there are opacity animations.
+ // TODO: is this causing the failure on line 224?
+ QTRY_VERIFY2(qFuzzyCompare(labelDelegate->opacity(), 1.0), qPrintable(QString::fromLatin1(
+ "Expected opacity of label delegate %1 at index %2 to be 1 but it's %3 (caller line %4) - QTBUG-118056: actual label delegate at this index is now %5")
+ .arg(QDebug::toString(labelDelegate)).arg(i).arg(labelDelegate->opacity()).arg(callerLineNumber).arg(QDebug::toString(labelRepeater->itemAt(i)))));
+
+ const int expectedValue = (i * valuesPerLabelStep) % 60;
+ const int actualValue = labelDelegate->property("value").toInt();
+ QVERIFY2(expectedValue == actualValue, qPrintable(QString::fromLatin1(
+ "Expected label's value at index %1 to be %2 but it's %3 (caller line %4)")
+ .arg(i).arg(expectedValue).arg(actualValue).arg(callerLineNumber)));
+
+ const QString expectedText = QString::number(expectedMode == Hours
+ ? (i == 0 ? 12 : (i == 12 ? 0 : i)) : i * valuesPerLabelStep);
+ // Use QTRY_VERIFY2 rather than QVERIFY2, because text changes are animated.
+ QTRY_VERIFY2(expectedText == labelDelegate->property("text").toString(), qPrintable(QString::fromLatin1(
+ "Expected label's text at index %1 to be %2 but it's %3 (caller line %4)").arg(i)
+ .arg(expectedText, labelDelegate->property("text").toString(), QString::number(callerLineNumber))));
+ }
+ };
+
+ auto verifySelectionIndicator = [&](int expectedValue, bool expect24HourValue, int callerLineNumber) {
+ const qreal actualRotation = int(selectionArm->rotation()) % 360;
+ const qreal expectedRotation = angleForValue(expectedValue);
+ QVERIFY2(qFuzzyCompare(actualRotation, expectedRotation), qPrintable(QString::fromLatin1(
+ "Expected selection arm's rotation to be %1 for expectedValue %2 but it's %3 (caller line %4)")
+ .arg(expectedRotation).arg(expectedValue).arg(actualRotation).arg(callerLineNumber)));
+
+ const QPoint expectedIndicatorCenterPos = labelCenterPosForValue(expectedValue, expect24HourValue);
+ const QPoint actualIndicatorCenterPos = selectionIndicator->mapToItem(
+ contentContainer, selectionIndicator->boundingRect().center().toPoint()).toPoint();
+ const QPoint difference = actualIndicatorCenterPos - expectedIndicatorCenterPos;
+ QVERIFY2(difference.x() <= 2 && difference.y() <= 2, qPrintable(QString::fromLatin1(
+ "Expected selection indicator's center position to be %1 (with 2 pixels of tolerance) but it's %2 (caller line %3)")
+ .arg(QDebug::toString(expectedIndicatorCenterPos), QDebug::toString(actualIndicatorCenterPos)).arg(callerLineNumber)));
+ };
+
+ auto valueForHour = [&](int hour) {
+ return (hour * valuesPerLabelStep) % 60;
+ };
+
+ // Open the picker to hours mode by clicking on the label.
+ auto *openDialogLabel = window->findChild<QQuickLabel *>(QString(), Qt::FindDirectChildrenOnly);
+ QVERIFY(openDialogLabel);
+ QCOMPARE(openDialogLabel->text(), "12:00");
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(openDialogLabel));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(openDialogLabel));
+ QTRY_COMPARE(dialog->property("opened").toBool(), true);
+ // It should show hours.
+ RETURN_IF_FAILED(verifyLabels(Hours, TwelveHour, __LINE__));
+ RETURN_IF_FAILED(verifySelectionIndicator(0, TwelveHour, __LINE__));
+
+ // Select the 3rd hour.
+ const QPoint thirdHourPos = labelCenterPosForValue(valueForHour(3));
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, thirdHourPos));
+ RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(3), TwelveHour, __LINE__));
+ QCOMPARE(timePicker->property("mode").toInt(), Hours);
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, thirdHourPos));
+ QCOMPARE(timePicker->property("hours").toInt(), 3);
+ QCOMPARE(timePicker->property("minutes").toInt(), 0);
+ // The dialog's values shouldn't change until the dialog has been accepted.
+ QCOMPARE(dialog->property("hours").toInt(), 12);
+ QCOMPARE(dialog->property("minutes").toInt(), 0);
+ // It should be showing minutes now.
+ QCOMPARE(timePicker->property("mode").toInt(), Minutes);
+ RETURN_IF_FAILED(verifyLabels(Minutes, TwelveHour, __LINE__));
+ RETURN_IF_FAILED(verifySelectionIndicator(0, TwelveHour, __LINE__));
+ auto *minutesLabel = dialog->findChild<QQuickLabel *>("minutesLabel");
+ QVERIFY(minutesLabel);
+ QCOMPARE(minutesLabel->property("dim").toBool(), false);
+
+ // Select the 59th minute.
+ const QPoint fiftyNinthMinutePos = labelCenterPosForValue(59);
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, fiftyNinthMinutePos));
+ RETURN_IF_FAILED(verifySelectionIndicator(59, TwelveHour, __LINE__));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, fiftyNinthMinutePos));
+ QCOMPARE(timePicker->property("hours").toInt(), 3);
+ QCOMPARE(timePicker->property("minutes").toInt(), 59);
+ QCOMPARE(dialog->property("hours").toInt(), 12);
+ QCOMPARE(dialog->property("minutes").toInt(), 0);
+ // It shouldn't be closed until the OK or Cancel buttons are clicked.
+ QCOMPARE(dialog->property("opened").toBool(), true);
+
+ // Accept the dialog to make the changes.
+ auto *dialogButtonBox = qobject_cast<QQuickDialogButtonBox *>(dialog->footer());
+ QVERIFY(dialogButtonBox);
+ auto *okButton = findDialogButton(dialogButtonBox, "OK");
+ QTest::ignoreMessage(QtDebugMsg, "A time was chosen - do something here!");
+ QVERIFY(clickButton(okButton));
+ QTRY_COMPARE(dialog->property("visible").toBool(), false);
+ QCOMPARE(dialog->property("hours").toInt(), 3);
+ QCOMPARE(dialog->property("minutes").toInt(), 59);
+ QCOMPARE(openDialogLabel->text(), "03:59");
+
+ // Open it again.
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(openDialogLabel));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(openDialogLabel));
+ QTRY_COMPARE(dialog->property("opened"), true);
+ RETURN_IF_FAILED(verifyLabels(Hours, TwelveHour, __LINE__));
+ RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(3), TwelveHour, __LINE__));
+ // The time label should be unchanged.
+ QCOMPARE(openDialogLabel->text(), "03:59");
+
+ // Switch from hours to minutes by clicking on the minutes label.
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(minutesLabel));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(minutesLabel));
+ RETURN_IF_FAILED(verifyLabels(Minutes, TwelveHour, __LINE__));
+ RETURN_IF_FAILED(verifySelectionIndicator(59, TwelveHour, __LINE__));
+
+ // Select the 1st minute.
+ const QPoint firstMinutePos = labelCenterPosForValue(1);
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, firstMinutePos));
+ RETURN_IF_FAILED(verifySelectionIndicator(1, TwelveHour, __LINE__));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, firstMinutePos));
+ QCOMPARE(timePicker->property("hours").toInt(), 3);
+ QCOMPARE(timePicker->property("minutes").toInt(), 1);
+ // It shouldn't be closed until the OK or Cancel buttons are clicked.
+ QCOMPARE(dialog->property("opened").toBool(), true);
+
+ // Accept the dialog to make the changes.
+ QTest::ignoreMessage(QtDebugMsg, "A time was chosen - do something here!");
+ QVERIFY(clickButton(okButton));
+ QTRY_COMPARE(dialog->property("visible").toBool(), false);
+ QCOMPARE(dialog->property("hours").toInt(), 3);
+ QCOMPARE(dialog->property("minutes").toInt(), 1);
+ QCOMPARE(openDialogLabel->text(), "03:01");
+
+ // Check that hours and minutes set programmatically on the picker and dialog are respected.
+ QVERIFY(dialog->setProperty("hours", QVariant::fromValue(7)));
+ QVERIFY(dialog->setProperty("minutes", QVariant::fromValue(8)));
+ QCOMPARE(openDialogLabel->text(), "07:08");
+ // Open the picker to hours mode by clicking on the label.
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(openDialogLabel));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(openDialogLabel));
+ QTRY_COMPARE(dialog->property("opened").toBool(), true);
+ RETURN_IF_FAILED(verifyLabels(Hours, TwelveHour, __LINE__));
+ RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(7), TwelveHour, __LINE__));
+ QCOMPARE(timePicker->property("hours").toInt(), 7);
+ QCOMPARE(timePicker->property("minutes").toInt(), 8);
+
+ // Check that cancelling the dialog cancels any changes.
+ // Select the fourth hour.
+ const QPoint fourthHourPos = labelCenterPosForValue(20);
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, fourthHourPos));
+ RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(4), TwelveHour, __LINE__));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, fourthHourPos));
+ QCOMPARE(timePicker->property("hours").toInt(), 4);
+ QCOMPARE(timePicker->property("minutes").toInt(), 8);
+ auto *cancelButton = findDialogButton(dialogButtonBox, "Cancel");
+ QVERIFY(clickButton(cancelButton));
+ QTRY_COMPARE(dialog->property("visible").toBool(), false);
+ QCOMPARE(dialog->property("hours").toInt(), 7);
+ QCOMPARE(dialog->property("minutes").toInt(), 8);
+
+ // Test that the 24 hour mode works.
+ const bool isCi = qgetenv("QTEST_ENVIRONMENT") == "ci"; // QTBUG-122679
+ if (isCi)
+ qDebug() << "about to set is24hour to true";
+ QVERIFY(dialog->setProperty("is24Hour", QVariant::fromValue(true)));
+ if (isCi)
+ qDebug() << "about to open picker";
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(openDialogLabel));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(openDialogLabel));
+ QTRY_COMPARE(dialog->property("opened").toBool(), true);
+ QCOMPARE(timePicker->property("mode").toInt(), Hours);
+ if (isCi)
+ qDebug() << "about to verify labels after switching to 24hr";
+ RETURN_IF_FAILED(verifyLabels(Hours, TwentyFourHour, __LINE__));
+ // TwelveHour because the selected value (7) is on the 12 hour "ring".
+ RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(7), TwelveHour, __LINE__));
+ // It should still show the old time.
+ QCOMPARE(timePicker->property("hours").toInt(), 7);
+ QCOMPARE(timePicker->property("minutes").toInt(), 8);
+ QCOMPARE(dialog->property("hours").toInt(), 7);
+ QCOMPARE(dialog->property("minutes").toInt(), 8);
+
+ // Select the 23rd hour.
+ const QPoint twentyThirdHourPos = labelCenterPosForValue(valueForHour(11), TwentyFourHour);
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, twentyThirdHourPos));
+ RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(23), TwentyFourHour, __LINE__));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, twentyThirdHourPos));
+ QCOMPARE(timePicker->property("hours").toInt(), 23);
+ QCOMPARE(timePicker->property("minutes").toInt(), 8);
+ QCOMPARE(dialog->property("hours").toInt(), 7);
+ QCOMPARE(dialog->property("minutes").toInt(), 8);
+ RETURN_IF_FAILED(verifyLabels(Minutes, TwentyFourHour, __LINE__));
+ RETURN_IF_FAILED(verifySelectionIndicator(8, TwelveHour, __LINE__));
+
+ // Select the 20th minute.
+ const QPoint twentiethMinutePos = labelCenterPosForValue(20);
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, twentiethMinutePos));
+ RETURN_IF_FAILED(verifySelectionIndicator(20, TwelveHour, __LINE__));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, twentiethMinutePos));
+ QCOMPARE(timePicker->property("hours").toInt(), 23);
+ QCOMPARE(timePicker->property("minutes").toInt(), 20);
+
+ // Go back to hours and make sure that the selection indicator is correct.
+ auto *hoursLabel = dialog->findChild<QQuickLabel *>("hoursLabel");
+ QVERIFY(hoursLabel);
+ QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(hoursLabel));
+ QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(hoursLabel));
+ RETURN_IF_FAILED(verifyLabels(Hours, TwentyFourHour, __LINE__));
+ RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(23), TwentyFourHour, __LINE__));
+
+ // Accept.
+ QTest::ignoreMessage(QtDebugMsg, "A time was chosen - do something here!");
+ QVERIFY(clickButton(okButton));
+ QTRY_COMPARE(dialog->property("visible").toBool(), false);
+ QCOMPARE(dialog->property("hours").toInt(), 23);
+ QCOMPARE(dialog->property("minutes").toInt(), 20);
+}
+
+QT_END_NAMESPACE
+
+QTEST_MAIN(tst_HowToQml)
+
+#include "tst_how-to-qml.moc"