diff options
Diffstat (limited to 'tests/auto/quick/doc/how-tos')
20 files changed, 1348 insertions, 0 deletions
diff --git a/tests/auto/quick/doc/how-tos/CMakeLists.txt b/tests/auto/quick/doc/how-tos/CMakeLists.txt new file mode 100644 index 0000000000..202ff38bb5 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +add_subdirectory(how-to-cpp-button) +add_subdirectory(how-to-cpp-enum-js) +add_subdirectory(how-to-qml) diff --git a/tests/auto/quick/doc/how-tos/how-to-cpp-button/CMakeLists.txt b/tests/auto/quick/doc/how-tos/how-to-cpp-button/CMakeLists.txt new file mode 100644 index 0000000000..34f370c7b3 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-cpp-button/CMakeLists.txt @@ -0,0 +1,31 @@ +# 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-cpp-button LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_how-to-cpp-button + SOURCES + tst_how-to-cpp-button.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-cpp-button + URI MyModule + QML_FILES + Main.qml + SOURCES + backend.h + backend.cpp +) diff --git a/tests/auto/quick/doc/how-tos/how-to-cpp-button/Main.qml b/tests/auto/quick/doc/how-tos/how-to-cpp-button/Main.qml new file mode 100644 index 0000000000..5150f6c6d6 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-cpp-button/Main.qml @@ -0,0 +1,19 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//! [file] +import QtQuick.Controls + +import MyModule + +ApplicationWindow { + width: 400 + height: 400 + title: qsTr("C++ Button example") + + Button { + text: qsTr("Click me") + onClicked: Backend.doStuff() + } +} +//! [file] diff --git a/tests/auto/quick/doc/how-tos/how-to-cpp-button/backend.cpp b/tests/auto/quick/doc/how-tos/how-to-cpp-button/backend.cpp new file mode 100644 index 0000000000..4143aea9e6 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-cpp-button/backend.cpp @@ -0,0 +1,13 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//! [file] +#include "backend.h" + +#include <QDebug> + +void Backend::doStuff() +{ + qDebug() << "Did stuff!"; +} +//! [file] diff --git a/tests/auto/quick/doc/how-tos/how-to-cpp-button/backend.h b/tests/auto/quick/doc/how-tos/how-to-cpp-button/backend.h new file mode 100644 index 0000000000..10249f5416 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-cpp-button/backend.h @@ -0,0 +1,17 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//! [file] +#include <QObject> +#include <QQmlEngine> + +class Backend : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + Q_INVOKABLE void doStuff(); +}; +//! [file] diff --git a/tests/auto/quick/doc/how-tos/how-to-cpp-button/tst_how-to-cpp-button.cpp b/tests/auto/quick/doc/how-tos/how-to-cpp-button/tst_how-to-cpp-button.cpp new file mode 100644 index 0000000000..792f84ffc2 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-cpp-button/tst_how-to-cpp-button.cpp @@ -0,0 +1,53 @@ +// 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/qquickwindow.h> +#include <QtQuickTemplates2/private/qquickbutton_p.h> +#include <QtQuickControlsTestUtils/private/controlstestutils_p.h> + +QT_BEGIN_NAMESPACE + +using namespace QQuickControlsTestUtils; + +class tst_HowToCppButton : public QObject +{ + Q_OBJECT + +public: + tst_HowToCppButton(); + +private slots: + void example(); +}; + +tst_HowToCppButton::tst_HowToCppButton() +{ +} + +void tst_HowToCppButton::example() +{ + QTest::failOnWarning(QRegularExpression(QStringLiteral(".?"))); + + QQmlApplicationEngine engine; + engine.loadFromModule("MyModule", "Main"); + QCOMPARE(engine.rootObjects().size(), 1); + + auto *window = qobject_cast<QQuickWindow*>(engine.rootObjects().at(0)); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + auto *button = window->findChild<QQuickButton*>(); + QVERIFY(button); + QCOMPARE(button->text(), "Click me"); + QTest::ignoreMessage(QtDebugMsg, "Did stuff!"); + QVERIFY(clickButton(button)); +} + +QT_END_NAMESPACE + +QTEST_MAIN(tst_HowToCppButton) + +#include "tst_how-to-cpp-button.moc" diff --git a/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/CMakeLists.txt b/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/CMakeLists.txt new file mode 100644 index 0000000000..5adc456b4b --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/CMakeLists.txt @@ -0,0 +1,25 @@ +# 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-cpp-enum-js LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_how-to-cpp-enum-js + SOURCES + backend.cpp + backend.h + tst_how-to-cpp-enum-js.cpp + LIBRARIES + Qt::Core + Qt::Qml +) + +qt_add_resources(tst_how-to-cpp-enum-js "js" + PREFIX + / + FILES + script.mjs +) diff --git a/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/backend.cpp b/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/backend.cpp new file mode 100644 index 0000000000..a78678cc18 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/backend.cpp @@ -0,0 +1,36 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//! [file] +#include "backend.h" + +Backend::Backend(QJSEngine *engine) : + mEngine(engine) +{ +} + +bool Backend::load() +{ + // Do some loading here... + + const QJSValue module = mEngine->importModule(":/script.mjs"); + if (module.isError()) { + qWarning() << "Error loading script.mjs:" << module.toString(); + return false; + } + + const QJSValue function = module.property("backendStatusUpdate"); + if (!function.isCallable()) { + qWarning() << "backendStatusUpdate script function is not callable!"; + return false; + } + + const QJSValue functionResult = function.call(QJSValueList() << Loaded); + if (functionResult.isError()) { + qWarning() << "backendStatusUpdate script function had errors:" << functionResult.toString(); + return false; + } + + return true; +} +//! [file] diff --git a/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/backend.h b/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/backend.h new file mode 100644 index 0000000000..5e723d406e --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/backend.h @@ -0,0 +1,29 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//! [file] +#include <QObject> +#include <QJSEngine> + +class Backend : public QObject +{ + Q_OBJECT + +public: + Backend(QJSEngine *engine); + + enum Status { + Unknown, + Error, + Loading, + Loaded + }; + + Q_ENUM(Status) + + bool load(); + +private: + QJSEngine *mEngine = nullptr; +}; +//! [file] diff --git a/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/script.mjs b/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/script.mjs new file mode 100644 index 0000000000..a4f929222a --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/script.mjs @@ -0,0 +1,13 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//! [file] +export function backendStatusUpdate(backendStatus) { + if (backendStatus === Backend.Error) { + console.warn("Error!") + return + } + + console.log("Backend loaded successfully") +} +//! [file] diff --git a/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/tst_how-to-cpp-enum-js.cpp b/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/tst_how-to-cpp-enum-js.cpp new file mode 100644 index 0000000000..933a1cf3e3 --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-cpp-enum-js/tst_how-to-cpp-enum-js.cpp @@ -0,0 +1,47 @@ +// 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/qjsengine.h> + +#include "backend.h" + +QT_BEGIN_NAMESPACE + +class tst_HowToCppEnumJs : public QObject +{ + Q_OBJECT + +public: + tst_HowToCppEnumJs(); + +private slots: + void example(); +}; + +tst_HowToCppEnumJs::tst_HowToCppEnumJs() +{ +} + +void tst_HowToCppEnumJs::example() +{ + QTest::failOnWarning(QRegularExpression(QStringLiteral(".?"))); + + QJSEngine engine; + engine.installExtensions(QJSEngine::AllExtensions); + + QJSValue backendJsMetaObject = engine.newQMetaObject(&Backend::staticMetaObject); + engine.globalObject().setProperty("Backend", backendJsMetaObject); + + QTest::ignoreMessage(QtDebugMsg, "Backend loaded successfully"); + Backend backend(&engine); + const bool loaded = backend.load(); + QVERIFY(loaded); +} + +QT_END_NAMESPACE + +QTEST_MAIN(tst_HowToCppEnumJs) + +#include "tst_how-to-cpp-enum-js.moc" diff --git a/tests/auto/quick/doc/how-tos/how-to-qml/BLACKLIST b/tests/auto/quick/doc/how-tos/how-to-qml/BLACKLIST new file mode 100644 index 0000000000..c878ca06be --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-qml/BLACKLIST @@ -0,0 +1,5 @@ +# QTBUG-126222 +[activeFocusDebugging] +macOS +ubuntu-22.04 + 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..9b750a59ef --- /dev/null +++ b/tests/auto/quick/doc/how-tos/how-to-qml/tst_how-to-qml.cpp @@ -0,0 +1,369 @@ +// 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(); + window->requestActivate(); + 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" |