diff options
-rw-r--r-- | src/quickcontrolstestutils/controlstestutils.cpp | 15 | ||||
-rw-r--r-- | src/quickcontrolstestutils/controlstestutils_p.h | 11 | ||||
-rw-r--r-- | src/quicktemplates/qquickmonthgrid.cpp | 10 | ||||
-rw-r--r-- | src/quicktemplates/qquickmonthgrid_p.h | 8 | ||||
-rw-r--r-- | src/quicktemplates/qquickmonthmodel.cpp | 40 | ||||
-rw-r--r-- | src/quicktemplates/qquickmonthmodel_p.h | 4 | ||||
-rw-r--r-- | tests/auto/quickcontrols/controls/data/tst_monthgrid.qml | 86 |
7 files changed, 151 insertions, 23 deletions
diff --git a/src/quickcontrolstestutils/controlstestutils.cpp b/src/quickcontrolstestutils/controlstestutils.cpp index fb88d00d29..f6b02abbe3 100644 --- a/src/quickcontrolstestutils/controlstestutils.cpp +++ b/src/quickcontrolstestutils/controlstestutils.cpp @@ -186,6 +186,21 @@ QString QQuickControlsTestUtils::StyleInfo::styleName() const return QQuickStyle::name(); } +/*! + It's recommended to use try-finally (see tst_monthgrid.qml for an example) + or init/initTestCase and cleanup/cleanupTestCase if setting environment + variables, in order to restore previous values. +*/ +QString QQuickControlsTestUtils::SystemEnvironment::value(const QString &name) +{ + return QString::fromLocal8Bit(qgetenv(name.toLocal8Bit())); +} + +bool QQuickControlsTestUtils::SystemEnvironment::setValue(const QString &name, const QString &value) +{ + return qputenv(name.toLocal8Bit(), value.toLocal8Bit()); +} + QString QQuickControlsTestUtils::visualFocusFailureMessage(QQuickControl *control) { QString message; diff --git a/src/quickcontrolstestutils/controlstestutils_p.h b/src/quickcontrolstestutils/controlstestutils_p.h index 839c376e9e..b4ecc58b73 100644 --- a/src/quickcontrolstestutils/controlstestutils_p.h +++ b/src/quickcontrolstestutils/controlstestutils_p.h @@ -96,6 +96,17 @@ namespace QQuickControlsTestUtils Qt::ColorScheme m_colorScheme = QGuiApplication::styleHints()->colorScheme(); }; + class SystemEnvironment : public QObject + { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + public: + Q_INVOKABLE QString value(const QString &name); + Q_INVOKABLE bool setValue(const QString &name, const QString &value); + }; + [[nodiscard]] bool arePopupWindowsSupported(); } diff --git a/src/quicktemplates/qquickmonthgrid.cpp b/src/quicktemplates/qquickmonthgrid.cpp index 07717512d0..1fc81b514f 100644 --- a/src/quicktemplates/qquickmonthgrid.cpp +++ b/src/quicktemplates/qquickmonthgrid.cpp @@ -83,7 +83,7 @@ public: void resizeItems(); QQuickItem *cellAt(const QPointF &pos) const; - QDate dateOf(QQuickItem *cell) const; + QDateTime dateOf(QQuickItem *cell) const; void updatePress(const QPointF &pos); void clearPress(bool clicked); @@ -95,7 +95,7 @@ public: QString title; QVariant source; - QDate pressedDate; + QDateTime pressedDate; int pressTimer; QQuickItem *pressedItem; QQuickMonthModel *model; @@ -128,11 +128,11 @@ QQuickItem *QQuickMonthGridPrivate::cellAt(const QPointF &pos) const return nullptr; } -QDate QQuickMonthGridPrivate::dateOf(QQuickItem *cell) const +QDateTime QQuickMonthGridPrivate::dateOf(QQuickItem *cell) const { if (contentItem) return model->dateAt(contentItem->childItems().indexOf(cell)); - return QDate(); + return {}; } void QQuickMonthGridPrivate::updatePress(const QPointF &pos) @@ -153,7 +153,7 @@ void QQuickMonthGridPrivate::clearPress(bool clicked) if (clicked) emit q->clicked(pressedDate); } - pressedDate = QDate(); + pressedDate = {}; pressedItem = nullptr; } diff --git a/src/quicktemplates/qquickmonthgrid_p.h b/src/quicktemplates/qquickmonthgrid_p.h index e7e4421e50..87f10813c6 100644 --- a/src/quicktemplates/qquickmonthgrid_p.h +++ b/src/quicktemplates/qquickmonthgrid_p.h @@ -58,10 +58,10 @@ Q_SIGNALS: void titleChanged(); void delegateChanged(); - void pressed(QDate date); - void released(QDate date); - void clicked(QDate date); - void pressAndHold(QDate date); + void pressed(QDateTime date); + void released(QDateTime date); + void clicked(QDateTime date); + void pressAndHold(QDateTime date); protected: void componentComplete() override; diff --git a/src/quicktemplates/qquickmonthmodel.cpp b/src/quicktemplates/qquickmonthmodel.cpp index 60d5e6fce2..0cbdb448e1 100644 --- a/src/quicktemplates/qquickmonthmodel.cpp +++ b/src/quicktemplates/qquickmonthmodel.cpp @@ -4,6 +4,8 @@ #include "qquickmonthmodel_p.h" #include <QtCore/private/qabstractitemmodel_p.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qtimezone.h> namespace { static const int daysInAWeek = 7; @@ -13,6 +15,8 @@ namespace { QT_BEGIN_NAMESPACE +Q_STATIC_LOGGING_CATEGORY(lcMonthModel, "qt.quick.controls.monthmodel") + class QQuickMonthModelPrivate : public QAbstractItemModelPrivate { Q_DECLARE_PUBLIC(QQuickMonthModel) @@ -31,7 +35,7 @@ public: int year; QString title; QLocale locale; - QVector<QDate> dates; + QVector<QDateTime> dates; QDate today; }; @@ -42,13 +46,23 @@ bool QQuickMonthModelPrivate::populate(int m, int y, const QLocale &l, bool forc return false; // The actual first (1st) day of the month. - QDate firstDayOfMonthDate(y, m, 1); + const QDate firstDayOfMonthDate = QDate(y, m, 1); + // QDate is converted to local time when converted to a JavaScript Date, + // so if we stored our dates as QDates, it's possible that the date provided + // to delegates will be wrong in certain timezones: + // e.g. 00:00 UTC converted to UTC-8 is 20:00 the day before. + // To account for this, we pick a time of day that can't possibly result + // in a different day when converted to local time. + QDateTime firstDayOfMonthDateTime(firstDayOfMonthDate, QTime(0, 0), QTimeZone(QTimeZone::UTC)); + const int localTimeOffsetFromUtc = QDateTime(firstDayOfMonthDate, QTime(0, 0), QTimeZone(QTimeZone::LocalTime)).offsetFromUtc(); + const int timeOffsetAdjustment = localTimeOffsetFromUtc * -1; + firstDayOfMonthDateTime.setSecsSinceEpoch(firstDayOfMonthDateTime.toSecsSinceEpoch() + timeOffsetAdjustment); int difference = ((firstDayOfMonthDate.dayOfWeek() - l.firstDayOfWeek()) + 7) % 7; // The first day to display should never be the 1st of the month, as we want some days from // the previous month to be visible. if (difference == 0) difference += 7; - QDate firstDateToDisplay = firstDayOfMonthDate.addDays(-difference); + QDateTime firstDateToDisplay = firstDayOfMonthDateTime.addDays(-difference); today = QDate::currentDate(); for (int i = 0; i < daysOnACalendarMonth; ++i) @@ -56,6 +70,13 @@ bool QQuickMonthModelPrivate::populate(int m, int y, const QLocale &l, bool forc q->setTitle(l.standaloneMonthName(m) + QStringLiteral(" ") + QString::number(y)); + qCDebug(lcMonthModel) << "populated model for month" << m << "year" << y << "locale" << locale + << "initial firstDayOfMonthDateTime" << QDateTime(firstDayOfMonthDate, QTime(0, 0), QTimeZone(QTimeZone::UTC)) + << "localTimeOffsetFromUtc" << localTimeOffsetFromUtc / 60 / 60 + << "timeOffsetAdjustment" << timeOffsetAdjustment / 60 / 60 + << "firstDayOfMonthDateTime" << firstDayOfMonthDateTime + << "firstDayOfMonthDateTime.toLocalTime()" << firstDayOfMonthDateTime.toLocalTime(); + return true; } @@ -132,13 +153,13 @@ void QQuickMonthModel::setTitle(const QString &title) } } -QDate QQuickMonthModel::dateAt(int index) const +QDateTime QQuickMonthModel::dateAt(int index) const { Q_D(const QQuickMonthModel); return d->dates.value(index); } -int QQuickMonthModel::indexOf(QDate date) const +int QQuickMonthModel::indexOf(QDateTime date) const { Q_D(const QQuickMonthModel); if (date < d->dates.first() || date > d->dates.last()) @@ -150,10 +171,15 @@ QVariant QQuickMonthModel::data(const QModelIndex &index, int role) const { Q_D(const QQuickMonthModel); if (index.isValid() && index.row() < daysOnACalendarMonth) { - const QDate date = d->dates.at(index.row()); + const QDateTime dateTime = d->dates.at(index.row()); + // As mentioned in populate, we store dates whose time is adjusted + // by the timezone offset, so we need to convert back to local time + // to get the correct date if the conversion to JavaScript's Date + // isn't being done for us. + const QDate date = d->dates.at(index.row()).toLocalTime().date(); switch (role) { case DateRole: - return date; + return dateTime; case DayRole: return date.day(); case TodayRole: diff --git a/src/quicktemplates/qquickmonthmodel_p.h b/src/quicktemplates/qquickmonthmodel_p.h index e629a7f8d1..b988489162 100644 --- a/src/quicktemplates/qquickmonthmodel_p.h +++ b/src/quicktemplates/qquickmonthmodel_p.h @@ -49,8 +49,8 @@ public: QString title() const; void setTitle(const QString &title); - Q_INVOKABLE QDate dateAt(int index) const; - Q_INVOKABLE int indexOf(QDate date) const; + Q_INVOKABLE QDateTime dateAt(int index) const; + Q_INVOKABLE int indexOf(QDateTime date) const; enum { DateRole = Qt::UserRole + 1, diff --git a/tests/auto/quickcontrols/controls/data/tst_monthgrid.qml b/tests/auto/quickcontrols/controls/data/tst_monthgrid.qml index 7f45025393..39a0dca142 100644 --- a/tests/auto/quickcontrols/controls/data/tst_monthgrid.qml +++ b/tests/auto/quickcontrols/controls/data/tst_monthgrid.qml @@ -4,6 +4,7 @@ import QtQuick import QtQuick.Controls import QtTest +import Qt.test.controls TestCase { id: testCase @@ -21,7 +22,14 @@ TestCase { Component { id: delegateGrid MonthGrid { - delegate: Item { + id: grid + delegate: Text { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + opacity: month === grid.month ? 1 : 0.5 + text: day + font: grid.font + readonly property date date: model.date readonly property int day: model.day readonly property bool today: model.today @@ -111,28 +119,28 @@ TestCase { compare(control.month, 0) - ignoreWarning(/tst_monthgrid.qml:18:9: QML (Abstract)?MonthGrid: month -1 is out of range \[0...11\]$/) + ignoreWarning(/.*MonthGrid: month -1 is out of range \[0...11\]$/) control.month = -1 compare(control.month, 0) control.month = 11 compare(control.month, 11) - ignoreWarning(/tst_monthgrid.qml:18:9: QML (Abstract)?MonthGrid: month 12 is out of range \[0...11\]$/) + ignoreWarning(/.*MonthGrid: month 12 is out of range \[0...11\]$/) control.month = 12 compare(control.month, 11) control.year = -271820 compare(control.year, -271820) - ignoreWarning(/tst_monthgrid.qml:18:9: QML (Abstract)?MonthGrid: year -271821 is out of range \[-271820...275759\]$/) + ignoreWarning(/.*MonthGrid: year -271821 is out of range \[-271820...275759\]$/) control.year = -271821 compare(control.year, -271820) control.year = 275759 compare(control.year, 275759) - ignoreWarning(/tst_monthgrid.qml:18:9: QML (Abstract)?MonthGrid: year 275760 is out of range \[-271820...275759\]$/) + ignoreWarning(/.*MonthGrid: year 275760 is out of range \[-271820...275759\]$/) control.year = 275760 compare(control.year, 275759) @@ -268,4 +276,72 @@ TestCase { control.delegate = delegateComponent1 verify(delegateComponent2) } + + function test_timezone_data() { + return [ + { tag: "UTC-12", tz: "Etc/GMT+12" }, + { tag: "UTC", tz: "Etc/UTC" }, + { tag: "UTC+14", tz: "Etc/GMT-14" } + ] + } + + function test_timezone(data) { + const oldTZValue = SystemEnvironment.value("TZ") + + // Ensure that even if the test fails, the old timezone is restored. + try { + SystemEnvironment.setValue("TZ", data.tz) + + // Pass a locale that ensures that the week starts with Monday. + let control = createTemporaryObject(delegateGrid, testCase, { locale: Qt.locale("en_AU"), month: 0, year: 2022 }) + verify(control) + + // M T W T F S S + // [27, 28, 29, 30, 31, 1, 2, + // 3, 4, 5, 6, 7, 8, 9, + // 10, 11, 12, 13, 14, 15, 16, + // 17, 18, 19, 20, 21, 22, 23, + // 24, 25, 26, 27, 28, 29, 30, + // 31, 1, 2, 3, 4, 5, 6] + + // Get the delegate item for 1st January. + let expectedDate = new Date(2022, 0, 1) + let delegateItem = control.contentItem.children[5] + verify(delegateItem) + // Test that the date is always correct; it shouldn't be affected by the + // conversion to local timezone that Date performs. + compare(delegateItem.date, expectedDate) + compare(delegateItem.day, expectedDate.getDate()) + compare(delegateItem.today, expectedDate === new Date()) + compare(delegateItem.month, expectedDate.getMonth()) + compare(delegateItem.year, expectedDate.getFullYear()) + + // Check that the signals emit the correct dates, too. + let pressedSpy = signalSpy.createObject(control, { target: control, signalName: "pressed" }) + verify(pressedSpy.valid) + let pressAndHoldSpy = signalSpy.createObject(control, { target: control, signalName: "pressAndHold" }) + verify(pressAndHoldSpy.valid) + let releasedSpy = signalSpy.createObject(control, { target: control, signalName: "released" }) + verify(releasedSpy.valid) + let clickedSpy = signalSpy.createObject(control, { target: control, signalName: "clicked" }) + verify(clickedSpy.valid) + + mousePress(delegateItem) + compare(pressedSpy.count, 1) + compare(pressedSpy.signalArguments[0][0], expectedDate) + // Testing this once is all we need, and it slows down tests otherwise. + if (data.tag === "UTC-12") { + tryCompare(pressAndHoldSpy, "count", 1) + compare(pressAndHoldSpy.signalArguments[0][0], expectedDate) + } + + mouseRelease(delegateItem) + compare(releasedSpy.count, 1) + compare(releasedSpy.signalArguments[0][0], expectedDate) + compare(clickedSpy.count, 1) + compare(clickedSpy.signalArguments[0][0], expectedDate) + } finally { + verify(SystemEnvironment.setValue("TZ", oldTZValue)) + } + } } |