diff options
author | Mitch Curtis <[email protected]> | 2025-06-19 13:56:17 +0800 |
---|---|---|
committer | Mitch Curtis <[email protected]> | 2025-06-30 13:31:50 +0800 |
commit | 57325020f65665d91e63dc300d674a8e8dc411f1 (patch) | |
tree | 9cf538f41d7b47fb33aa4508353016690bd9b155 /src | |
parent | aa1d3e416edd092f3dc8f8a20f15bd92baa93485 (diff) |
When a QDate is passed to QML, it becomes to a JavaScript Date.
As Dates are stored in local time, the QDates we were passing to
QML had the potential to be one day off in certain timezones.
For example: 00:00 UTC converted to UTC-8 is 20:00 the day before.
Fix this by storing and providing dates as QDateTime so that we can
give it a time of day that can't possibly result in a different day
when converted to local time. It's fine to change the C++ API since
it's private, and nothing will change for the type that users see,
since they always get a Date.
Add a SystemEnvironment singleton to QQuickControlsTestUtils
(Qt.test.controls) to allow reading and writing environment
variables from QML.
Fixes: QTBUG-72208
Pick-to: 6.8 6.9 6.10
Change-Id: Idb4ab26568d8f1eddd5ab4cebe691e38173d02a9
Reviewed-by: Ulf Hermann <[email protected]>
Diffstat (limited to 'src')
-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 |
6 files changed, 70 insertions, 18 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, |