From d535dfea1f2d801268396f328c5c99968ff164ec Mon Sep 17 00:00:00 2001 From: Mauro Persano Date: Fri, 13 Mar 2020 21:40:34 -0300 Subject: [PATCH 1/4] Doc: replace QFileInfo::created with birthTime Change-Id: Ia497febcbf28e4b3babe50fc6f05e12f3d686b91 Reviewed-by: Volker Hilsheimer --- src/corelib/io/qfileinfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/corelib/io/qfileinfo.cpp b/src/corelib/io/qfileinfo.cpp index 89834de29f..9f58458d4f 100644 --- a/src/corelib/io/qfileinfo.cpp +++ b/src/corelib/io/qfileinfo.cpp @@ -272,7 +272,7 @@ QDateTime &QFileInfoPrivate::getFileTime(QAbstractFileEngine::FileTime request) info objects, just append one to the file name given to the constructors or setFile(). - The file's dates are returned by created(), lastModified(), lastRead() and + The file's dates are returned by birthTime(), lastModified(), lastRead() and fileTime(). Information about the file's access permissions is obtained with isReadable(), isWritable() and isExecutable(). The file's ownership is available from owner(), ownerId(), group() and From 2af04860f6536bbbf82ee21d6aa95ca33a60fbf5 Mon Sep 17 00:00:00 2001 From: Robin Burchell Date: Fri, 3 May 2019 20:02:10 +0200 Subject: [PATCH 2/4] qtimezoneprivate_tz: Apply a cache over the top of timezone data Constantly re-reading the timezone information only to be told the exact same thing is wildly expensive, which can hurt in operations that cause a lot of QTimeZone creation, for example, V4's DateObject - which creates them a lot (in DaylightSavingTA). This performance problem was identified when I noticed that a QDateTime binding updated once per frame was causing >100% CPU usage (on a desktop!) thanks to a QtQuickControls 1 Calendar (which has a number of bindings to the date's properties like getMonth() and so on). The newly added tst_QTimeZone::systemTimeZone benchmark gets a ~90% decrease in instruction count: --- before +++ after PASS : tst_QTimeZone::systemTimeZone() RESULT : tst_QTimeZone::systemTimeZone(): - 0.024 msecs per iteration (total: 51, iterations: 2048) + 0.0036 msecs per iteration (total: 59, iterations: 16384) Also impacted (over in QDateTime) is tst_QDateTime::setMSecsSinceEpochTz(). The results here are - on the surface - less impressive (~0.17% drop), however, it isn't even creating QTimeZone on a hot path to begin with, so a large drop would have been a surprise. Added several further benchmarks to cover non-system zones and traverse transitions. Done-With: Edward Welbourne Task-number: QTBUG-75585 Change-Id: I044a84fc2d3a2dc965f63cd3a3299fc509750bf7 Reviewed-by: Ulf Hermann Reviewed-by: Simon Hausmann --- src/corelib/time/qtimezoneprivate_p.h | 16 +- src/corelib/time/qtimezoneprivate_tz.cpp | 141 +++++++++++------- .../corelib/time/qtimezone/main.cpp | 122 +++++++++++++++ 3 files changed, 222 insertions(+), 57 deletions(-) diff --git a/src/corelib/time/qtimezoneprivate_p.h b/src/corelib/time/qtimezoneprivate_p.h index 5f6491ef81..c3e9064b8c 100644 --- a/src/corelib/time/qtimezoneprivate_p.h +++ b/src/corelib/time/qtimezoneprivate_p.h @@ -287,6 +287,16 @@ Q_DECL_CONSTEXPR inline bool operator==(const QTzTransitionRule &lhs, const QTzT Q_DECL_CONSTEXPR inline bool operator!=(const QTzTransitionRule &lhs, const QTzTransitionRule &rhs) noexcept { return !operator==(lhs, rhs); } +// These are stored separately from QTzTimeZonePrivate so that they can be +// cached, avoiding the need to re-parse them from disk constantly. +struct QTzTimeZoneCacheEntry +{ + QVector m_tranTimes; + QVector m_tranRules; + QList m_abbreviations; + QByteArray m_posixRule; +}; + class Q_AUTOTEST_EXPORT QTzTimeZonePrivate final : public QTimeZonePrivate { QTzTimeZonePrivate(const QTzTimeZonePrivate &) = default; @@ -334,13 +344,11 @@ private: QVector getPosixTransitions(qint64 msNear) const; Data dataForTzTransition(QTzTransitionTime tran) const; - QVector m_tranTimes; - QVector m_tranRules; - QList m_abbreviations; #if QT_CONFIG(icu) mutable QSharedDataPointer m_icu; #endif - QByteArray m_posixRule; + QTzTimeZoneCacheEntry cached_data; + QVector tranCache() const { return cached_data.m_tranTimes; } }; #endif // Q_OS_UNIX diff --git a/src/corelib/time/qtimezoneprivate_tz.cpp b/src/corelib/time/qtimezoneprivate_tz.cpp index 3c2695a789..a94ed6b7a9 100644 --- a/src/corelib/time/qtimezoneprivate_tz.cpp +++ b/src/corelib/time/qtimezoneprivate_tz.cpp @@ -1,5 +1,6 @@ /**************************************************************************** ** +** Copyright (C) 2019 Crimson AS ** Copyright (C) 2013 John Layt ** Contact: https://www.qt.io/licensing/ ** @@ -42,6 +43,7 @@ #include "private/qlocale_tools_p.h" #include +#include #include #include #include @@ -649,14 +651,26 @@ QTzTimeZonePrivate *QTzTimeZonePrivate::clone() const return new QTzTimeZonePrivate(*this); } -void QTzTimeZonePrivate::init(const QByteArray &ianaId) +class QTzTimeZoneCache { +public: + QTzTimeZoneCacheEntry fetchEntry(const QByteArray &ianaId); + +private: + QTzTimeZoneCacheEntry findEntry(const QByteArray &ianaId); + QHash m_cache; + QMutex m_mutex; +}; + +QTzTimeZoneCacheEntry QTzTimeZoneCache::findEntry(const QByteArray &ianaId) +{ + QTzTimeZoneCacheEntry ret; QFile tzif; if (ianaId.isEmpty()) { // Open system tz tzif.setFileName(QStringLiteral("/etc/localtime")); if (!tzif.open(QIODevice::ReadOnly)) - return; + return ret; } else { // Open named tz, try modern path first, if fails try legacy path tzif.setFileName(QLatin1String("/usr/share/zoneinfo/") + QString::fromLocal8Bit(ianaId)); @@ -669,9 +683,9 @@ void QTzTimeZonePrivate::init(const QByteArray &ianaId) if (PosixZone::parse(begin, zoneInfo.constEnd()).hasValidOffset() && (begin == zoneInfo.constEnd() || PosixZone::parse(begin, zoneInfo.constEnd()).hasValidOffset())) { - m_id = m_posixRule = ianaId; + ret.m_posixRule = ianaId; } - return; + return ret; } } } @@ -682,59 +696,59 @@ void QTzTimeZonePrivate::init(const QByteArray &ianaId) bool ok = false; QTzHeader hdr = parseTzHeader(ds, &ok); if (!ok || ds.status() != QDataStream::Ok) - return; + return ret; QVector tranList = parseTzTransitions(ds, hdr.tzh_timecnt, false); if (ds.status() != QDataStream::Ok) - return; + return ret; QVector typeList = parseTzTypes(ds, hdr.tzh_typecnt); if (ds.status() != QDataStream::Ok) - return; + return ret; QMap abbrevMap = parseTzAbbreviations(ds, hdr.tzh_charcnt, typeList); if (ds.status() != QDataStream::Ok) - return; + return ret; parseTzLeapSeconds(ds, hdr.tzh_leapcnt, false); if (ds.status() != QDataStream::Ok) - return; + return ret; typeList = parseTzIndicators(ds, typeList, hdr.tzh_ttisstdcnt, hdr.tzh_ttisgmtcnt); if (ds.status() != QDataStream::Ok) - return; + return ret; // If version 2 then parse the second block of data if (hdr.tzh_version == '2' || hdr.tzh_version == '3') { ok = false; QTzHeader hdr2 = parseTzHeader(ds, &ok); if (!ok || ds.status() != QDataStream::Ok) - return; + return ret; tranList = parseTzTransitions(ds, hdr2.tzh_timecnt, true); if (ds.status() != QDataStream::Ok) - return; + return ret; typeList = parseTzTypes(ds, hdr2.tzh_typecnt); if (ds.status() != QDataStream::Ok) - return; + return ret; abbrevMap = parseTzAbbreviations(ds, hdr2.tzh_charcnt, typeList); if (ds.status() != QDataStream::Ok) - return; + return ret; parseTzLeapSeconds(ds, hdr2.tzh_leapcnt, true); if (ds.status() != QDataStream::Ok) - return; + return ret; typeList = parseTzIndicators(ds, typeList, hdr2.tzh_ttisstdcnt, hdr2.tzh_ttisgmtcnt); if (ds.status() != QDataStream::Ok) - return; - m_posixRule = parseTzPosixRule(ds); + return ret; + ret.m_posixRule = parseTzPosixRule(ds); if (ds.status() != QDataStream::Ok) - return; + return ret; } // Translate the TZ file into internal format // Translate the array index based tz_abbrind into list index const int size = abbrevMap.size(); - m_abbreviations.clear(); - m_abbreviations.reserve(size); + ret.m_abbreviations.clear(); + ret.m_abbreviations.reserve(size); QVector abbrindList; abbrindList.reserve(size); for (auto it = abbrevMap.cbegin(), end = abbrevMap.cend(); it != end; ++it) { - m_abbreviations.append(it.value()); + ret.m_abbreviations.append(it.value()); abbrindList.append(it.key()); } for (int i = 0; i < typeList.size(); ++i) @@ -752,7 +766,7 @@ void QTzTimeZonePrivate::init(const QByteArray &ianaId) // Now for each transition time calculate and store our rule: const int tranCount = tranList.count();; - m_tranTimes.reserve(tranCount); + ret.m_tranTimes.reserve(tranCount); // The DST offset when in effect: usually stable, usually an hour: int lastDstOff = 3600; for (int i = 0; i < tranCount; i++) { @@ -806,24 +820,45 @@ void QTzTimeZonePrivate::init(const QByteArray &ianaId) rule.abbreviationIndex = tz_type.tz_abbrind; // If the rule already exist then use that, otherwise add it - int ruleIndex = m_tranRules.indexOf(rule); + int ruleIndex = ret.m_tranRules.indexOf(rule); if (ruleIndex == -1) { - m_tranRules.append(rule); - tran.ruleIndex = m_tranRules.size() - 1; + ret.m_tranRules.append(rule); + tran.ruleIndex = ret.m_tranRules.size() - 1; } else { tran.ruleIndex = ruleIndex; } tran.atMSecsSinceEpoch = tz_tran.tz_time * 1000; - m_tranTimes.append(tran); + ret.m_tranTimes.append(tran); } - if (m_tranTimes.isEmpty() && m_posixRule.isEmpty()) + + return ret; +} + +QTzTimeZoneCacheEntry QTzTimeZoneCache::fetchEntry(const QByteArray &ianaId) +{ + QMutexLocker locker(&m_mutex); + + // search the cache... + const auto& it = m_cache.find(ianaId); + if (it != m_cache.constEnd()) + return *it; + + // ... or build a new entry from scratch + QTzTimeZoneCacheEntry ret = findEntry(ianaId); + m_cache[ianaId] = ret; + return ret; +} + +void QTzTimeZonePrivate::init(const QByteArray &ianaId) +{ + static QTzTimeZoneCache tzCache; + const auto &entry = tzCache.fetchEntry(ianaId); + if (entry.m_tranTimes.isEmpty() && entry.m_posixRule.isEmpty()) return; // Invalid after all ! - if (ianaId.isEmpty()) - m_id = systemTimeZoneId(); - else - m_id = ianaId; + cached_data = std::move(entry); + m_id = ianaId.isEmpty() ? systemTimeZoneId() : ianaId; } QLocale::Country QTzTimeZonePrivate::country() const @@ -903,12 +938,12 @@ QString QTzTimeZonePrivate::displayName(QTimeZone::TimeType timeType, } // Otherwise is strange sequence, so work backwards through trans looking for first match, if any - auto it = std::partition_point(m_tranTimes.cbegin(), m_tranTimes.cend(), + auto it = std::partition_point(tranCache().cbegin(), tranCache().cend(), [currentMSecs](const QTzTransitionTime &at) { return at.atMSecsSinceEpoch <= currentMSecs; }); - while (it != m_tranTimes.cbegin()) { + while (it != tranCache().cbegin()) { --it; tran = dataForTzTransition(*it); int offset = tran.daylightTimeOffset; @@ -944,7 +979,7 @@ int QTzTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const bool QTzTimeZonePrivate::hasDaylightTime() const { // TODO Perhaps cache as frequently accessed? - for (const QTzTransitionRule &rule : m_tranRules) { + for (const QTzTransitionRule &rule : cached_data.m_tranRules) { if (rule.dstOffset != 0) return true; } @@ -960,11 +995,11 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::dataForTzTransition(QTzTransitionTime { QTimeZonePrivate::Data data; data.atMSecsSinceEpoch = tran.atMSecsSinceEpoch; - QTzTransitionRule rule = m_tranRules.at(tran.ruleIndex); + QTzTransitionRule rule = cached_data.m_tranRules.at(tran.ruleIndex); data.standardTimeOffset = rule.stdOffset; data.daylightTimeOffset = rule.dstOffset; data.offsetFromUtc = rule.stdOffset + rule.dstOffset; - data.abbreviation = QString::fromUtf8(m_abbreviations.at(rule.abbreviationIndex)); + data.abbreviation = QString::fromUtf8(cached_data.m_abbreviations.at(rule.abbreviationIndex)); return data; } @@ -972,37 +1007,37 @@ QVector QTzTimeZonePrivate::getPosixTransitions(qint64 m { const int year = QDateTime::fromMSecsSinceEpoch(msNear, Qt::UTC).date().year(); // The Data::atMSecsSinceEpoch of the single entry if zone is constant: - qint64 atTime = m_tranTimes.isEmpty() ? msNear : m_tranTimes.last().atMSecsSinceEpoch; - return calculatePosixTransitions(m_posixRule, year - 1, year + 1, atTime); + qint64 atTime = tranCache().isEmpty() ? msNear : tranCache().last().atMSecsSinceEpoch; + return calculatePosixTransitions(cached_data.m_posixRule, year - 1, year + 1, atTime); } QTimeZonePrivate::Data QTzTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const { // If the required time is after the last transition (or there were none) // and we have a POSIX rule, then use it: - if (!m_posixRule.isEmpty() - && (m_tranTimes.isEmpty() || m_tranTimes.last().atMSecsSinceEpoch < forMSecsSinceEpoch)) { + if (!cached_data.m_posixRule.isEmpty() + && (tranCache().isEmpty() || tranCache().last().atMSecsSinceEpoch < forMSecsSinceEpoch)) { QVector posixTrans = getPosixTransitions(forMSecsSinceEpoch); auto it = std::partition_point(posixTrans.cbegin(), posixTrans.cend(), [forMSecsSinceEpoch] (const QTimeZonePrivate::Data &at) { return at.atMSecsSinceEpoch <= forMSecsSinceEpoch; }); // Use most recent, if any in the past; or the first if we have no other rules: - if (it > posixTrans.cbegin() || (m_tranTimes.isEmpty() && it < posixTrans.cend())) { + if (it > posixTrans.cbegin() || (tranCache().isEmpty() && it < posixTrans.cend())) { QTimeZonePrivate::Data data = *(it > posixTrans.cbegin() ? it - 1 : it); data.atMSecsSinceEpoch = forMSecsSinceEpoch; return data; } } - if (m_tranTimes.isEmpty()) // Only possible if !isValid() + if (tranCache().isEmpty()) // Only possible if !isValid() return invalidData(); // Otherwise, use the rule for the most recent or first transition: - auto last = std::partition_point(m_tranTimes.cbegin(), m_tranTimes.cend(), + auto last = std::partition_point(tranCache().cbegin(), tranCache().cend(), [forMSecsSinceEpoch] (const QTzTransitionTime &at) { return at.atMSecsSinceEpoch <= forMSecsSinceEpoch; }); - if (last > m_tranTimes.cbegin()) + if (last > tranCache().cbegin()) --last; Data data = dataForTzTransition(*last); data.atMSecsSinceEpoch = forMSecsSinceEpoch; @@ -1018,8 +1053,8 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::nextTransition(qint64 afterMSecsSince { // If the required time is after the last transition (or there were none) // and we have a POSIX rule, then use it: - if (!m_posixRule.isEmpty() - && (m_tranTimes.isEmpty() || m_tranTimes.last().atMSecsSinceEpoch < afterMSecsSinceEpoch)) { + if (!cached_data.m_posixRule.isEmpty() + && (tranCache().isEmpty() || tranCache().last().atMSecsSinceEpoch < afterMSecsSinceEpoch)) { QVector posixTrans = getPosixTransitions(afterMSecsSinceEpoch); auto it = std::partition_point(posixTrans.cbegin(), posixTrans.cend(), [afterMSecsSinceEpoch] (const QTimeZonePrivate::Data &at) { @@ -1030,19 +1065,19 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::nextTransition(qint64 afterMSecsSince } // Otherwise, if we can find a valid tran, use its rule: - auto last = std::partition_point(m_tranTimes.cbegin(), m_tranTimes.cend(), + auto last = std::partition_point(tranCache().cbegin(), tranCache().cend(), [afterMSecsSinceEpoch] (const QTzTransitionTime &at) { return at.atMSecsSinceEpoch <= afterMSecsSinceEpoch; }); - return last != m_tranTimes.cend() ? dataForTzTransition(*last) : invalidData(); + return last != tranCache().cend() ? dataForTzTransition(*last) : invalidData(); } QTimeZonePrivate::Data QTzTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const { // If the required time is after the last transition (or there were none) // and we have a POSIX rule, then use it: - if (!m_posixRule.isEmpty() - && (m_tranTimes.isEmpty() || m_tranTimes.last().atMSecsSinceEpoch < beforeMSecsSinceEpoch)) { + if (!cached_data.m_posixRule.isEmpty() + && (tranCache().isEmpty() || tranCache().last().atMSecsSinceEpoch < beforeMSecsSinceEpoch)) { QVector posixTrans = getPosixTransitions(beforeMSecsSinceEpoch); auto it = std::partition_point(posixTrans.cbegin(), posixTrans.cend(), [beforeMSecsSinceEpoch] (const QTimeZonePrivate::Data &at) { @@ -1051,15 +1086,15 @@ QTimeZonePrivate::Data QTzTimeZonePrivate::previousTransition(qint64 beforeMSecs if (it > posixTrans.cbegin()) return *--it; // It fell between the last transition (if any) and the first of the POSIX rule: - return m_tranTimes.isEmpty() ? invalidData() : dataForTzTransition(m_tranTimes.last()); + return tranCache().isEmpty() ? invalidData() : dataForTzTransition(tranCache().last()); } // Otherwise if we can find a valid tran then use its rule - auto last = std::partition_point(m_tranTimes.cbegin(), m_tranTimes.cend(), + auto last = std::partition_point(tranCache().cbegin(), tranCache().cend(), [beforeMSecsSinceEpoch] (const QTzTransitionTime &at) { return at.atMSecsSinceEpoch < beforeMSecsSinceEpoch; }); - return last > m_tranTimes.cbegin() ? dataForTzTransition(*--last) : invalidData(); + return last > tranCache().cbegin() ? dataForTzTransition(*--last) : invalidData(); } static long getSymloopMax() diff --git a/tests/benchmarks/corelib/time/qtimezone/main.cpp b/tests/benchmarks/corelib/time/qtimezone/main.cpp index 65455a7261..133e6451bc 100644 --- a/tests/benchmarks/corelib/time/qtimezone/main.cpp +++ b/tests/benchmarks/corelib/time/qtimezone/main.cpp @@ -1,5 +1,6 @@ /**************************************************************************** ** +** Copyright (C) 2019 Crimson AS ** Copyright (C) 2018 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure ** Contact: https://www.qt.io/licensing/ ** @@ -30,14 +31,57 @@ #include #include +// Enable to test *every* zone, rather than a hand-picked few, in some _data() sets: +// #define EXHAUSTIVE + class tst_QTimeZone : public QObject { Q_OBJECT private Q_SLOTS: void isTimeZoneIdAvailable(); + void systemTimeZone(); + void zoneByName_data(); + void zoneByName(); + void transitionList_data(); + void transitionList(); + void transitionsForward_data() { transitionList_data(); } + void transitionsForward(); + void transitionsReverse_data() { transitionList_data(); } + void transitionsReverse(); }; +static QVector enoughZones() +{ +#ifdef EXHAUSTIVE + auto available = QTimeZone::availableTimeZoneIds(); + QVector result; + result.reserve(available.size() + 1); + for (conat auto &name : available) + result << name; +#else + QVector result{ + QByteArray("UTC"), + // Those named overtly in tst_QDateTime: + QByteArray("Europe/Oslo"), + QByteArray("America/Vancouver"), + QByteArray("Europe/Berlin"), + QByteArray("America/Sao_Paulo"), + QByteArray("Pacific/Auckland"), + QByteArray("Australia/Eucla"), + QByteArray("Asia/Kathmandu"), + QByteArray("Pacific/Kiritimati"), + QByteArray("Pacific/Apia"), + QByteArray("UTC+12:00"), + QByteArray("Australia/Sydney"), + QByteArray("Asia/Singapore"), + QByteArray("Australia/Brisbane") + }; +#endif + result << QByteArray("Vulcan/ShiKahr"); // invalid: also worth testing + return result; +} + void tst_QTimeZone::isTimeZoneIdAvailable() { const QList available = QTimeZone::availableTimeZoneIds(); @@ -47,6 +91,84 @@ void tst_QTimeZone::isTimeZoneIdAvailable() } } +void tst_QTimeZone::systemTimeZone() +{ + QBENCHMARK { + QTimeZone::systemTimeZone(); + } +} + +void tst_QTimeZone::zoneByName_data() +{ + QTest::addColumn("name"); + + const auto names = enoughZones(); + for (const auto &name : names) + QTest::newRow(name.constData()) << name; +} + +void tst_QTimeZone::zoneByName() +{ + QFETCH(QByteArray, name); + QTimeZone zone; + QBENCHMARK { + zone = QTimeZone(name); + } + Q_UNUSED(zone); +} + +void tst_QTimeZone::transitionList_data() +{ + QTest::addColumn("name"); + QTest::newRow("system") << QByteArray(); // Handled specially in the test. + + const auto names = enoughZones(); + for (const auto &name : names) { + QTimeZone zone(name); + if (zone.isValid() && zone.hasTransitions()) + QTest::newRow(name.constData()) << name; + } +} + +void tst_QTimeZone::transitionList() +{ + QFETCH(QByteArray, name); + const QTimeZone zone = name.isEmpty() ? QTimeZone::systemTimeZone() : QTimeZone(name); + const QDateTime early = QDate(1625, 6, 8).startOfDay(Qt::UTC); // Cassini's birth date + const QDateTime late // End of 32-bit signed time_t + = QDateTime::fromSecsSinceEpoch(std::numeric_limits::max(), Qt::UTC); + QTimeZone::OffsetDataList seq; + QBENCHMARK { + seq = zone.transitions(early, late); + } + Q_UNUSED(seq); +} + +void tst_QTimeZone::transitionsForward() +{ + QFETCH(QByteArray, name); + const QTimeZone zone = name.isEmpty() ? QTimeZone::systemTimeZone() : QTimeZone(name); + const QDateTime early = QDate(1625, 6, 8).startOfDay(Qt::UTC); // Cassini's birth date + QBENCHMARK { + QTimeZone::OffsetData tran = zone.nextTransition(early); + while (tran.atUtc.isValid()) + tran = zone.nextTransition(tran.atUtc); + } +} + +void tst_QTimeZone::transitionsReverse() +{ + QFETCH(QByteArray, name); + const QTimeZone zone = name.isEmpty() ? QTimeZone::systemTimeZone() : QTimeZone(name); + const QDateTime late // End of 32-bit signed time_t + = QDateTime::fromSecsSinceEpoch(std::numeric_limits::max(), Qt::UTC); + QBENCHMARK { + QTimeZone::OffsetData tran = zone.previousTransition(late); + while (tran.atUtc.isValid()) + tran = zone.previousTransition(tran.atUtc); + } +} + QTEST_MAIN(tst_QTimeZone) #include "main.moc" From cb509f3aeeea2a905b696f3914fa6d03c9106f71 Mon Sep 17 00:00:00 2001 From: Joni Poikelin Date: Mon, 17 Feb 2020 14:40:26 +0200 Subject: [PATCH 3/4] Doc: fix copy paste errors in border-*-style documentation Change-Id: I442513ec87e25610898c2102170a5f2bfec5ee77 Reviewed-by: Shawn Rutledge --- src/gui/doc/src/richtext.qdoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gui/doc/src/richtext.qdoc b/src/gui/doc/src/richtext.qdoc index 31a2ebf05b..1ef34455ab 100644 --- a/src/gui/doc/src/richtext.qdoc +++ b/src/gui/doc/src/richtext.qdoc @@ -1197,16 +1197,16 @@ \li none | dotted | dashed | dot-dash | dot-dot-dash | solid | double | groove | ridge | inset | outset \li Border style for text tables and table cells. \row \li \c border-top-style - \li + \li \li Top border style for table cells. \row \li \c border-bottom-style - \li + \li \li Bottom border style for table cells. \row \li \c border-left-style - \li + \li \li Left border style for table cells. \row \li \c border-right-style - \li + \li \li Right border style for table cells. \row \li \c border-width \li px From cc1d891b8ed1c4f23183e4b06f46e5840a993e35 Mon Sep 17 00:00:00 2001 From: Alexander Volkov Date: Mon, 16 Mar 2020 20:54:10 +0300 Subject: [PATCH 4/4] xcb: Add support for XdndActionList property This allows to pass and receive possible drop actions from other processes, including GTK applications. Fixes: QTBUG-75744 Change-Id: I944edc6fa00f8801a25912e70eb104a647a9fc0e Reviewed-by: JiDe Zhang Reviewed-by: Liang Qi --- src/plugins/platforms/xcb/qxcbatom.cpp | 1 + src/plugins/platforms/xcb/qxcbatom.h | 1 + src/plugins/platforms/xcb/qxcbdrag.cpp | 108 +++++++++++++++++++++++-- src/plugins/platforms/xcb/qxcbdrag.h | 15 +++- 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/src/plugins/platforms/xcb/qxcbatom.cpp b/src/plugins/platforms/xcb/qxcbatom.cpp index d366564dd6..a73d28319d 100644 --- a/src/plugins/platforms/xcb/qxcbatom.cpp +++ b/src/plugins/platforms/xcb/qxcbatom.cpp @@ -182,6 +182,7 @@ static const char *xcb_atomnames = { "XdndActionCopy\0" "XdndActionLink\0" "XdndActionMove\0" + "XdndActionAsk\0" "XdndActionPrivate\0" // Xkb diff --git a/src/plugins/platforms/xcb/qxcbatom.h b/src/plugins/platforms/xcb/qxcbatom.h index 80b5887395..9cf93ec314 100644 --- a/src/plugins/platforms/xcb/qxcbatom.h +++ b/src/plugins/platforms/xcb/qxcbatom.h @@ -183,6 +183,7 @@ public: XdndActionCopy, XdndActionLink, XdndActionMove, + XdndActionAsk, XdndActionPrivate, // Xkb diff --git a/src/plugins/platforms/xcb/qxcbdrag.cpp b/src/plugins/platforms/xcb/qxcbdrag.cpp index 3d525598ca..d82129c6bc 100644 --- a/src/plugins/platforms/xcb/qxcbdrag.cpp +++ b/src/plugins/platforms/xcb/qxcbdrag.cpp @@ -216,6 +216,22 @@ void QXcbDrag::endDrag() initiatorWindow.clear(); } +Qt::DropAction QXcbDrag::defaultAction(Qt::DropActions possibleActions, Qt::KeyboardModifiers modifiers) const +{ + if (currentDrag() || drop_actions.isEmpty()) + return QBasicDrag::defaultAction(possibleActions, modifiers); + + return toDropAction(drop_actions.first()); +} + +void QXcbDrag::handlePropertyNotifyEvent(const xcb_property_notify_event_t *event) +{ + if (event->window != xdnd_dragsource || event->atom != atom(QXcbAtom::XdndActionList)) + return; + + readActionList(); +} + static bool windowInteractsWithPosition(xcb_connection_t *connection, const QPoint & pos, xcb_window_t w, xcb_shape_sk_t shapeType) { @@ -470,16 +486,20 @@ void QXcbDrag::move(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardMod move.data.data32[1] = 0; // flags move.data.data32[2] = (globalPos.x() << 16) + globalPos.y(); move.data.data32[3] = connection()->time(); - move.data.data32[4] = toXdndAction(defaultAction(currentDrag()->supportedActions(), mods)); + const auto supportedActions = currentDrag()->supportedActions(); + const auto requestedAction = defaultAction(supportedActions, mods); + move.data.data32[4] = toXdndAction(requestedAction); qCDebug(lcQpaXDnd) << "sending XdndPosition to target:" << target; source_time = connection()->time(); - if (w) + if (w) { handle_xdnd_position(w, &move, b, mods); - else + } else { + setActionList(requestedAction, supportedActions); xcb_send_event(xcb_connection(), false, proxy_target, XCB_EVENT_MASK_NO_EVENT, (const char *)&move); + } } static const bool isUnity = qgetenv("XDG_CURRENT_DESKTOP").toLower() == "unity"; @@ -560,6 +580,16 @@ Qt::DropAction QXcbDrag::toDropAction(xcb_atom_t a) const return Qt::CopyAction; } +Qt::DropActions QXcbDrag::toDropActions(const QVector &atoms) const +{ + Qt::DropActions actions; + for (const auto actionAtom : atoms) { + if (actionAtom != atom(QXcbAtom::XdndActionAsk)) + actions |= toDropAction(actionAtom); + } + return actions; +} + xcb_atom_t QXcbDrag::toXdndAction(Qt::DropAction a) const { switch (a) { @@ -577,6 +607,60 @@ xcb_atom_t QXcbDrag::toXdndAction(Qt::DropAction a) const } } +void QXcbDrag::readActionList() +{ + drop_actions.clear(); + auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, xdnd_dragsource, + atom(QXcbAtom::XdndActionList), XCB_ATOM_ATOM, + 0, 1024); + if (reply && reply->type != XCB_NONE && reply->format == 32) { + int length = xcb_get_property_value_length(reply.get()) / 4; + + xcb_atom_t *atoms = (xcb_atom_t *)xcb_get_property_value(reply.get()); + for (int i = 0; i < length; ++i) + drop_actions.append(atoms[i]); + } +} + +void QXcbDrag::setActionList(Qt::DropAction requestedAction, Qt::DropActions supportedActions) +{ +#ifndef QT_NO_CLIPBOARD + QVector actions; + if (requestedAction != Qt::IgnoreAction) + actions.append(toXdndAction(requestedAction)); + + auto checkAppend = [this, requestedAction, supportedActions, &actions](Qt::DropAction action) { + if (requestedAction != action && supportedActions & action) + actions.append(toXdndAction(action)); + }; + + checkAppend(Qt::CopyAction); + checkAppend(Qt::MoveAction); + checkAppend(Qt::LinkAction); + + if (current_actions != actions) { + xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, connection()->clipboard()->owner(), + atom(QXcbAtom::XdndActionList), + XCB_ATOM_ATOM, 32, actions.size(), actions.constData()); + current_actions = actions; + } +#endif +} + +void QXcbDrag::startListeningForActionListChanges() +{ + connection()->addWindowEventListener(xdnd_dragsource, this); + const uint32_t event_mask[] = { XCB_EVENT_MASK_PROPERTY_CHANGE }; + xcb_change_window_attributes(xcb_connection(), xdnd_dragsource, XCB_CW_EVENT_MASK, event_mask); +} + +void QXcbDrag::stopListeningForActionListChanges() +{ + const uint32_t event_mask[] = { XCB_EVENT_MASK_NO_EVENT }; + xcb_change_window_attributes(xcb_connection(), xdnd_dragsource, XCB_CW_EVENT_MASK, event_mask); + connection()->removeWindowEventListener(xdnd_dragsource); +} + int QXcbDrag::findTransactionByWindow(xcb_window_t window) { int at = -1; @@ -657,6 +741,9 @@ void QXcbDrag::handleEnter(QPlatformWindow *, const xcb_client_message_event_t * return; xdnd_dragsource = event->data.data32[0]; + startListeningForActionListChanges(); + readActionList(); + if (!proxy) proxy = xdndProxy(connection(), xdnd_dragsource); current_proxy_target = proxy ? proxy : xdnd_dragsource; @@ -723,7 +810,9 @@ void QXcbDrag::handle_xdnd_position(QPlatformWindow *w, const xcb_client_message supported_actions = currentDrag()->supportedActions(); } else { dropData = m_dropData; - supported_actions = Qt::DropActions(toDropAction(e->data.data32[4])); + supported_actions = toDropActions(drop_actions); + if (e->data.data32[4] != atom(QXcbAtom::XdndActionAsk)) + supported_actions |= Qt::DropActions(toDropAction(e->data.data32[4])); } auto buttons = currentDrag() ? b : connection()->queryMouseButtons(); @@ -867,8 +956,10 @@ void QXcbDrag::handleLeave(QPlatformWindow *w, const xcb_client_message_event_t // If the target receives XdndLeave, it frees any cached data and forgets the whole incident. qCDebug(lcQpaXDnd) << "target:" << event->window << "received XdndLeave"; - if (!currentWindow || w != currentWindow.data()->handle()) + if (!currentWindow || w != currentWindow.data()->handle()) { + stopListeningForActionListChanges(); return; // sanity + } // ### // if (checkEmbedded(current_embedding_widget, event)) { @@ -883,6 +974,8 @@ void QXcbDrag::handleLeave(QPlatformWindow *w, const xcb_client_message_event_t event->data.data32[0], xdnd_dragsource); } + stopListeningForActionListChanges(); + QWindowSystemInterface::handleDrag(w->window(), nullptr, QPoint(), Qt::IgnoreAction, 0, 0); } @@ -929,6 +1022,7 @@ void QXcbDrag::handleDrop(QPlatformWindow *, const xcb_client_message_event_t *e qCDebug(lcQpaXDnd) << "target:" << event->window << "received XdndDrop"; if (!currentWindow) { + stopListeningForActionListChanges(); xdnd_dragsource = 0; return; // sanity } @@ -951,7 +1045,7 @@ void QXcbDrag::handleDrop(QPlatformWindow *, const xcb_client_message_event_t *e supported_drop_actions = Qt::DropActions(l[4]); } else { dropData = m_dropData; - supported_drop_actions = accepted_drop_action; + supported_drop_actions = accepted_drop_action | toDropActions(drop_actions); } if (!dropData) @@ -986,6 +1080,8 @@ void QXcbDrag::handleDrop(QPlatformWindow *, const xcb_client_message_event_t *e xcb_send_event(xcb_connection(), false, current_proxy_target, XCB_EVENT_MASK_NO_EVENT, (char *)&finished); + stopListeningForActionListChanges(); + dropped = true; } diff --git a/src/plugins/platforms/xcb/qxcbdrag.h b/src/plugins/platforms/xcb/qxcbdrag.h index 1388e68acc..b6371041e6 100644 --- a/src/plugins/platforms/xcb/qxcbdrag.h +++ b/src/plugins/platforms/xcb/qxcbdrag.h @@ -68,7 +68,7 @@ class QXcbScreen; class QDrag; class QShapedPixmapWindow; -class QXcbDrag : public QXcbObject, public QBasicDrag +class QXcbDrag : public QXcbObject, public QBasicDrag, public QXcbWindowEventListener { public: QXcbDrag(QXcbConnection *c); @@ -82,6 +82,10 @@ public: void drop(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) override; void endDrag() override; + Qt::DropAction defaultAction(Qt::DropActions possibleActions, Qt::KeyboardModifiers modifiers) const override; + + void handlePropertyNotifyEvent(const xcb_property_notify_event_t *event) override; + void handleEnter(QPlatformWindow *window, const xcb_client_message_event_t *event, xcb_window_t proxy = 0); void handlePosition(QPlatformWindow *w, const xcb_client_message_event_t *event); void handleLeave(QPlatformWindow *w, const xcb_client_message_event_t *event); @@ -114,8 +118,14 @@ private: void send_leave(); Qt::DropAction toDropAction(xcb_atom_t atom) const; + Qt::DropActions toDropActions(const QVector &atoms) const; xcb_atom_t toXdndAction(Qt::DropAction a) const; + void readActionList(); + void setActionList(Qt::DropAction requestedAction, Qt::DropActions supportedActions); + void startListeningForActionListChanges(); + void stopListeningForActionListChanges(); + QPointer initiatorWindow; QPointer currentWindow; QPoint currentPosition; @@ -159,6 +169,9 @@ private: QVector drag_types; + QVector current_actions; + QVector drop_actions; + struct Transaction { xcb_timestamp_t timestamp;