aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMitch Curtis <[email protected]>2025-06-19 13:56:17 +0800
committerMitch Curtis <[email protected]>2025-06-30 13:31:50 +0800
commit57325020f65665d91e63dc300d674a8e8dc411f1 (patch)
tree9cf538f41d7b47fb33aa4508353016690bd9b155
parentaa1d3e416edd092f3dc8f8a20f15bd92baa93485 (diff)
MonthGrid: fix dates being incorrect for certain timezonesHEADdev
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]>
-rw-r--r--src/quickcontrolstestutils/controlstestutils.cpp15
-rw-r--r--src/quickcontrolstestutils/controlstestutils_p.h11
-rw-r--r--src/quicktemplates/qquickmonthgrid.cpp10
-rw-r--r--src/quicktemplates/qquickmonthgrid_p.h8
-rw-r--r--src/quicktemplates/qquickmonthmodel.cpp40
-rw-r--r--src/quicktemplates/qquickmonthmodel_p.h4
-rw-r--r--tests/auto/quickcontrols/controls/data/tst_monthgrid.qml86
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))
+ }
+ }
}